Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 33a11fb53a | |||
| d1d41ca49a | |||
| c06e0ee5e9 | |||
| 83f80ccc40 | |||
| 069065ea3a | |||
| 1672e78041 | |||
| 34401de3d0 | |||
| f687b81b70 | |||
| bbb11e611e | |||
| 90919411e9 | |||
| 1491b017a0 | |||
| 1655150092 | |||
| ceb6c9a100 | |||
| 00bbce08fb | |||
| 91a4b86860 | |||
| 5aece6fa06 | |||
| a70b94afe2 | |||
| 9ebc2719d3 | |||
| b46aae1833 | |||
| 30e6a40865 | |||
| 403df36af6 | |||
| 5ca769807e | |||
| 72a71a605b | |||
| 24d833a5d1 | |||
| 44b30380f7 | |||
| 261cd086c7 | |||
| 9e19b526b9 | |||
| 6c617a0ef1 | |||
| a812650fea | |||
| fec7a77d6f | |||
| 971e261067 | |||
| 27407ba021 | |||
| ef8e58ede4 | |||
| f13260c10a | |||
| e1183d6049 | |||
| f1b275aedc | |||
| b647af256c | |||
| d2e9ed3467 | |||
| 5b39986835 | |||
| a97b08e8ea | |||
| 56d1be9447 | |||
| a241cb5215 | |||
| 042b1a8294 | |||
| 6d79b8cd4c | |||
| 88d766aeae | |||
| 119b471b84 | |||
| 7f079e0ec3 | |||
| 46fe3eecff | |||
| 3061a701d5 | |||
| e45109e1d7 | |||
| e138e6d51e | |||
| aef1363b04 | |||
| 766dd03d81 | |||
| ab393651ec | |||
| ffd265d0ae | |||
| 72155472dd | |||
| 9b7170834e | |||
| 3e57a87bf7 | |||
| a15d1c9f1c | |||
| a095a64f90 | |||
| 2374d6b1c4 | |||
| 5ed3ff9084 | |||
| 385be55510 | |||
| 3082d0d128 | |||
| f7e242f054 | |||
| 2874505004 | |||
| ed9e0c13d7 | |||
| 10be94fa99 | |||
| ee79c46145 | |||
| 384010a2c0 | |||
| 1e47019122 | |||
| 536c65e82c | |||
| cdf14932ec |
@@ -32,6 +32,7 @@
|
||||
"message": "Use local parameter instead."
|
||||
}
|
||||
],
|
||||
"no-param-reassign": [2, { "props": false }],
|
||||
"array-bracket-newline": [2, { "minItems": 2 }],
|
||||
"array-element-newline": [2, { "minItems": 2 }]
|
||||
},
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"demo": {
|
||||
"sass": {
|
||||
"demo.css": "demo/src/sass/bundles/demo.scss",
|
||||
"error.css": "demo/src/sass/bundles/error.csss"
|
||||
"error.css": "demo/src/sass/bundles/error.scss"
|
||||
},
|
||||
"js": {
|
||||
"demo.js": "demo/src/js/demo.js"
|
||||
|
||||
+109
@@ -1,3 +1,112 @@
|
||||
# v3.3.9
|
||||
|
||||
Again, more changes from @friday!
|
||||
|
||||
* Restore window reference in `utils.is.cue()`
|
||||
* Fix InvalidStateError and IE11 issues
|
||||
* Respect storage being disabled for storage getter
|
||||
|
||||
# v3.3.8
|
||||
|
||||
Many changes here thanks to @friday:
|
||||
|
||||
* Added missing URL polyfill
|
||||
* Pause while seeking to mimic default HTML5 behaviour
|
||||
* Add `seeked` event listener to update progress (fixes #966)
|
||||
* Trigger seeked event in youtube plugin if either playing or paused (fixes #921)
|
||||
* Fix for YouTube and Vimeo autoplays on seek (fixes #876)
|
||||
* Toggle controls improvements
|
||||
* Cleanup unused code
|
||||
* Poster image loading improvements
|
||||
* Fix for seek tooltip vs click accuracy
|
||||
|
||||
# v3.3.7
|
||||
|
||||
* Poster fixes (thanks @friday)
|
||||
* Grid tweak
|
||||
|
||||
# v3.3.6
|
||||
|
||||
* Vimeo fixes for mute state
|
||||
* Vimeo ID fix (fixes #945)
|
||||
* Use `<div>` for poster container
|
||||
* Tooltip fixes for unicode languages (fixes #943)
|
||||
|
||||
# v3.3.5
|
||||
|
||||
* Removed `.load()` call as it breaks HLS (see #870)
|
||||
|
||||
# v3.3.4
|
||||
|
||||
* Fix for controls sometimes not showing while video is playing
|
||||
* Fixed logic for show home tab on option select
|
||||
|
||||
# v3.3.3
|
||||
|
||||
* Reverted change to show home tab on option select due to usability regression
|
||||
|
||||
# v3.3.2
|
||||
|
||||
* Fix for ads running in audio
|
||||
* Fix for setting poster on source change
|
||||
|
||||
## v3.3.0
|
||||
|
||||
* Now using a custom poster image element to hide the YouTube play button and give more control over when the poster image shows
|
||||
* Renamed `showPosterOnEnd` to `resetOnEnd` as it makes more sense and now works for all players and does not reload media
|
||||
* Fix for same domain SVG URLs (raised by Jochem in Slack)
|
||||
* [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/Window/URL) is polyfill now required
|
||||
* Added pause className (fixes #941)
|
||||
* Button height set in CSS (auto) (fixes #928)
|
||||
* Don't autoplay cloned original media (fixes #936)
|
||||
* Return to the home menu pane after selecting an option
|
||||
|
||||
## v3.2.4
|
||||
|
||||
* Fix issue wher player never reports as ready if controls is empty array
|
||||
* Fix issue where screen reader labels were removed from time displays
|
||||
* Fix issue where custom controls placeholders were not populated
|
||||
* Custom controls HTML example updated
|
||||
* Fix for aria-label being set to the initial state on toggle buttons, overriding the inner labels
|
||||
* Fix for hidden mute button on iOS (not functional for Vimeo due to API limitations) (fixes #656)
|
||||
|
||||
## v3.2.3
|
||||
|
||||
* Fix for iOS 9 throwing error for `name` property in fullscreen API (fixes #908)
|
||||
|
||||
## v3.2.2
|
||||
|
||||
* Fix for regression in 3.2.1 resulting in hidden buffer display (fixes #920)
|
||||
* Cleaned up incorrect use of `aria-hidden` attribute
|
||||
|
||||
## v3.2.1
|
||||
|
||||
* Accessibility improvements for the controls (part of #905 fixes)
|
||||
* Fix for context menu showing on YouTube (thanks Anthony Recenello in Slack)
|
||||
* Vimeo fix for their API not returning the right duration until playback begins (fixes #891)
|
||||
|
||||
## v3.2.0
|
||||
|
||||
* Fullscreen fixes (thanks @friday)
|
||||
* Menu fix for if speed not in config
|
||||
* Menu z-index fix (thanks @danielsarin)
|
||||
* i18n fix for missing "Normal" string (thanks @danielsarin)
|
||||
* Safer check for active caption (thanks @Antonio-Laguna)
|
||||
* Add custom property fallback (thanks @friday)
|
||||
* Fixed bug for captions with no srclang and labels and improved logic (fixes #875)
|
||||
* Fix for `playing` false positive (fixes #898)
|
||||
* Fix for IE issue with navigator.language (thanks @nicolasthy) (fixes #893)
|
||||
* Fix for Vimeo controls missing on iOS (thanks @verde-io) (fixes #807)
|
||||
* Fix for double vimeo caption rendering (fixes #877)
|
||||
|
||||
## v3.1.0
|
||||
|
||||
* Styling fixes
|
||||
|
||||
## v3.1.0-beta.2
|
||||
|
||||
* YouTube playback speed fixes
|
||||
|
||||
## v3.1.0-beta.1
|
||||
|
||||
* HTML5 quality selection
|
||||
|
||||
+3
-1
@@ -59,6 +59,7 @@ i18n: {
|
||||
captions: 'Captions',
|
||||
settings: 'Settings',
|
||||
speed: 'Speed',
|
||||
normal: 'Normal',
|
||||
quality: 'Quality',
|
||||
loop: 'Loop',
|
||||
start: 'Start',
|
||||
@@ -120,7 +121,8 @@ const controls = `
|
||||
<progress class="plyr__progress--buffer" min="0" max="100" value="0">% buffered</progress>
|
||||
<span role="tooltip" class="plyr__tooltip">00:00</span>
|
||||
</div>
|
||||
<div class="plyr__time">00:00</div>
|
||||
<div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
|
||||
<div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
|
||||
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
|
||||
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
|
||||
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+201
-33
@@ -1,4 +1,4 @@
|
||||
(function () {
|
||||
typeof navigator === "object" && (function () {
|
||||
'use strict';
|
||||
|
||||
var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
||||
@@ -97,7 +97,7 @@ function isObject(what) {
|
||||
// Yanked from https://git.io/vS8DV re-used under CC0
|
||||
// with some tiny modifications
|
||||
function isError(value) {
|
||||
switch ({}.toString.call(value)) {
|
||||
switch (Object.prototype.toString.call(value)) {
|
||||
case '[object Error]':
|
||||
return true;
|
||||
case '[object Exception]':
|
||||
@@ -110,7 +110,15 @@ function isError(value) {
|
||||
}
|
||||
|
||||
function isErrorEvent(value) {
|
||||
return supportsErrorEvent() && {}.toString.call(value) === '[object ErrorEvent]';
|
||||
return Object.prototype.toString.call(value) === '[object ErrorEvent]';
|
||||
}
|
||||
|
||||
function isDOMError(value) {
|
||||
return Object.prototype.toString.call(value) === '[object DOMError]';
|
||||
}
|
||||
|
||||
function isDOMException(value) {
|
||||
return Object.prototype.toString.call(value) === '[object DOMException]';
|
||||
}
|
||||
|
||||
function isUndefined(what) {
|
||||
@@ -153,6 +161,24 @@ function supportsErrorEvent() {
|
||||
}
|
||||
}
|
||||
|
||||
function supportsDOMError() {
|
||||
try {
|
||||
new DOMError(''); // eslint-disable-line no-new
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function supportsDOMException() {
|
||||
try {
|
||||
new DOMException(''); // eslint-disable-line no-new
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function supportsFetch() {
|
||||
if (!('fetch' in _window)) return false;
|
||||
|
||||
@@ -245,7 +271,13 @@ function objectFrozen(obj) {
|
||||
}
|
||||
|
||||
function truncate(str, max) {
|
||||
return !max || str.length <= max ? str : str.substr(0, max) + '\u2026';
|
||||
if (typeof max !== 'number') {
|
||||
throw new Error('2nd argument to `truncate` function should be a number');
|
||||
}
|
||||
if (typeof str !== 'string' || max === 0) {
|
||||
return str;
|
||||
}
|
||||
return str.length <= max ? str : str.substr(0, max) + '\u2026';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -544,10 +576,9 @@ function jsonSize(value) {
|
||||
}
|
||||
|
||||
function serializeValue(value) {
|
||||
var maxLength = 40;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value.length <= maxLength ? value : value.substr(0, maxLength - 1) + '\u2026';
|
||||
var maxLength = 40;
|
||||
return truncate(value, maxLength);
|
||||
} else if (
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean' ||
|
||||
@@ -663,6 +694,8 @@ var utils = {
|
||||
isObject: isObject,
|
||||
isError: isError,
|
||||
isErrorEvent: isErrorEvent,
|
||||
isDOMError: isDOMError,
|
||||
isDOMException: isDOMException,
|
||||
isUndefined: isUndefined,
|
||||
isFunction: isFunction,
|
||||
isPlainObject: isPlainObject,
|
||||
@@ -670,6 +703,8 @@ var utils = {
|
||||
isArray: isArray,
|
||||
isEmptyObject: isEmptyObject,
|
||||
supportsErrorEvent: supportsErrorEvent,
|
||||
supportsDOMError: supportsDOMError,
|
||||
supportsDOMException: supportsDOMException,
|
||||
supportsFetch: supportsFetch,
|
||||
supportsReferrerPolicy: supportsReferrerPolicy,
|
||||
supportsPromiseRejectionEvent: supportsPromiseRejectionEvent,
|
||||
@@ -724,10 +759,24 @@ var ERROR_TYPES_RE = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Ran
|
||||
|
||||
function getLocationHref() {
|
||||
if (typeof document === 'undefined' || document.location == null) return '';
|
||||
|
||||
return document.location.href;
|
||||
}
|
||||
|
||||
function getLocationOrigin() {
|
||||
if (typeof document === 'undefined' || document.location == null) return '';
|
||||
|
||||
// Oh dear IE10...
|
||||
if (!document.location.origin) {
|
||||
document.location.origin =
|
||||
document.location.protocol +
|
||||
'//' +
|
||||
document.location.hostname +
|
||||
(document.location.port ? ':' + document.location.port : '');
|
||||
}
|
||||
|
||||
return document.location.origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* TraceKit.report: cross-browser processing of unhandled exceptions
|
||||
*
|
||||
@@ -1135,6 +1184,44 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
|
||||
element.func = UNKNOWN_FUNCTION;
|
||||
}
|
||||
|
||||
if (element.url && element.url.substr(0, 5) === 'blob:') {
|
||||
// Special case for handling JavaScript loaded into a blob.
|
||||
// We use a synchronous AJAX request here as a blob is already in
|
||||
// memory - it's not making a network request. This will generate a warning
|
||||
// in the browser console, but there has already been an error so that's not
|
||||
// that much of an issue.
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', element.url, false);
|
||||
xhr.send(null);
|
||||
|
||||
// If we failed to download the source, skip this patch
|
||||
if (xhr.status === 200) {
|
||||
var source = xhr.responseText || '';
|
||||
|
||||
// We trim the source down to the last 300 characters as sourceMappingURL is always at the end of the file.
|
||||
// Why 300? To be in line with: https://github.com/getsentry/sentry/blob/4af29e8f2350e20c28a6933354e4f42437b4ba42/src/sentry/lang/javascript/processor.py#L164-L175
|
||||
source = source.slice(-300);
|
||||
|
||||
// Now we dig out the source map URL
|
||||
var sourceMaps = source.match(/\/\/# sourceMappingURL=(.*)$/);
|
||||
|
||||
// If we don't find a source map comment or we find more than one, continue on to the next element.
|
||||
if (sourceMaps) {
|
||||
var sourceMapAddress = sourceMaps[1];
|
||||
|
||||
// Now we check to see if it's a relative URL.
|
||||
// If it is, convert it to an absolute one.
|
||||
if (sourceMapAddress.charAt(0) === '~') {
|
||||
sourceMapAddress = getLocationOrigin() + sourceMapAddress.slice(1);
|
||||
}
|
||||
|
||||
// Now we strip the '.map' off of the end of the URL and update the
|
||||
// element so that Sentry can match the map to the blob.
|
||||
element.url = sourceMapAddress.slice(0, -4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stack.push(element);
|
||||
}
|
||||
|
||||
@@ -1646,10 +1733,12 @@ var console$1 = {
|
||||
|
||||
|
||||
|
||||
var isErrorEvent$1 = utils.isErrorEvent;
|
||||
var isDOMError$1 = utils.isDOMError;
|
||||
var isDOMException$1 = utils.isDOMException;
|
||||
var isError$1 = utils.isError;
|
||||
var isObject$1 = utils.isObject;
|
||||
var isPlainObject$1 = utils.isPlainObject;
|
||||
var isErrorEvent$1 = utils.isErrorEvent;
|
||||
var isUndefined$1 = utils.isUndefined;
|
||||
var isFunction$1 = utils.isFunction;
|
||||
var isString$1 = utils.isString;
|
||||
@@ -1777,7 +1866,7 @@ Raven.prototype = {
|
||||
// webpack (using a build step causes webpack #1617). Grunt verifies that
|
||||
// this value matches package.json during build.
|
||||
// See: https://github.com/getsentry/raven-js/issues/465
|
||||
VERSION: '3.24.0',
|
||||
VERSION: '3.25.2',
|
||||
|
||||
debug: false,
|
||||
|
||||
@@ -2066,7 +2155,11 @@ Raven.prototype = {
|
||||
*/
|
||||
_promiseRejectionHandler: function(event) {
|
||||
this._logDebug('debug', 'Raven caught unhandled promise rejection:', event);
|
||||
this.captureException(event.reason);
|
||||
this.captureException(event.reason, {
|
||||
extra: {
|
||||
unhandledPromiseRejection: true
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -2105,6 +2198,23 @@ Raven.prototype = {
|
||||
if (isErrorEvent$1(ex) && ex.error) {
|
||||
// If it is an ErrorEvent with `error` property, extract it to get actual Error
|
||||
ex = ex.error;
|
||||
} else if (isDOMError$1(ex) || isDOMException$1(ex)) {
|
||||
// If it is a DOMError or DOMException (which are legacy APIs, but still supported in some browsers)
|
||||
// then we just extract the name and message, as they don't provide anything else
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/DOMError
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/DOMException
|
||||
var name = ex.name || (isDOMError$1(ex) ? 'DOMError' : 'DOMException');
|
||||
var message = ex.message ? name + ': ' + ex.message : name;
|
||||
|
||||
return this.captureMessage(
|
||||
message,
|
||||
objectMerge$1(options, {
|
||||
// neither DOMError or DOMException provide stack trace and we most likely wont get it this way as well
|
||||
// but it's barely any overhead so we may at least try
|
||||
stacktrace: true,
|
||||
trimHeadFrames: options.trimHeadFrames + 1
|
||||
})
|
||||
);
|
||||
} else if (isError$1(ex)) {
|
||||
// we have a real Error object
|
||||
ex = ex;
|
||||
@@ -2116,6 +2226,7 @@ Raven.prototype = {
|
||||
ex = new Error(options.message);
|
||||
} else {
|
||||
// If none of previous checks were valid, then it means that
|
||||
// it's not a DOMError/DOMException
|
||||
// it's not a plain Object
|
||||
// it's not a valid ErrorEvent (one with an error property)
|
||||
// it's not an Error
|
||||
@@ -2207,6 +2318,14 @@ Raven.prototype = {
|
||||
|
||||
// stack[0] is `throw new Error(msg)` call itself, we are interested in the frame that was just before that, stack[1]
|
||||
var initialCall = isArray$1(stack.stack) && stack.stack[1];
|
||||
|
||||
// if stack[1] is `Raven.captureException`, it means that someone passed a string to it and we redirected that call
|
||||
// to be handled by `captureMessage`, thus `initialCall` is the 3rd one, not 2nd
|
||||
// initialCall => captureException(string) => captureMessage(string)
|
||||
if (initialCall && initialCall.func === 'Raven.captureException') {
|
||||
initialCall = stack.stack[2];
|
||||
}
|
||||
|
||||
var fileurl = (initialCall && initialCall.url) || '';
|
||||
|
||||
if (
|
||||
@@ -3004,17 +3123,30 @@ Raven.prototype = {
|
||||
status_code: null
|
||||
};
|
||||
|
||||
return origFetch.apply(this, args).then(function(response) {
|
||||
fetchData.status_code = response.status;
|
||||
return origFetch
|
||||
.apply(this, args)
|
||||
.then(function(response) {
|
||||
fetchData.status_code = response.status;
|
||||
|
||||
self.captureBreadcrumb({
|
||||
type: 'http',
|
||||
category: 'fetch',
|
||||
data: fetchData
|
||||
self.captureBreadcrumb({
|
||||
type: 'http',
|
||||
category: 'fetch',
|
||||
data: fetchData
|
||||
});
|
||||
|
||||
return response;
|
||||
})
|
||||
['catch'](function(err) {
|
||||
// if there is an error performing the request
|
||||
self.captureBreadcrumb({
|
||||
type: 'http',
|
||||
category: 'fetch',
|
||||
data: fetchData,
|
||||
level: 'error'
|
||||
});
|
||||
|
||||
throw err;
|
||||
});
|
||||
|
||||
return response;
|
||||
});
|
||||
};
|
||||
},
|
||||
wrappedBuiltIns
|
||||
@@ -3027,7 +3159,7 @@ Raven.prototype = {
|
||||
if (_document.addEventListener) {
|
||||
_document.addEventListener('click', self._breadcrumbEventHandler('click'), false);
|
||||
_document.addEventListener('keypress', self._keypressEventHandler(), false);
|
||||
} else {
|
||||
} else if (_document.attachEvent) {
|
||||
// IE8 Compatibility
|
||||
_document.attachEvent('onclick', self._breadcrumbEventHandler('click'));
|
||||
_document.attachEvent('onkeypress', self._keypressEventHandler());
|
||||
@@ -3043,8 +3175,8 @@ Raven.prototype = {
|
||||
var hasPushAndReplaceState =
|
||||
!isChromePackagedApp &&
|
||||
_window$2.history &&
|
||||
history.pushState &&
|
||||
history.replaceState;
|
||||
_window$2.history.pushState &&
|
||||
_window$2.history.replaceState;
|
||||
if (autoBreadcrumbs.location && hasPushAndReplaceState) {
|
||||
// TODO: remove onpopstate handler on uninstall()
|
||||
var oldOnPopState = _window$2.onpopstate;
|
||||
@@ -3073,8 +3205,8 @@ Raven.prototype = {
|
||||
};
|
||||
};
|
||||
|
||||
fill$1(history, 'pushState', historyReplacementFunction, wrappedBuiltIns);
|
||||
fill$1(history, 'replaceState', historyReplacementFunction, wrappedBuiltIns);
|
||||
fill$1(_window$2.history, 'pushState', historyReplacementFunction, wrappedBuiltIns);
|
||||
fill$1(_window$2.history, 'replaceState', historyReplacementFunction, wrappedBuiltIns);
|
||||
}
|
||||
|
||||
if (autoBreadcrumbs.console && 'console' in _window$2 && console.log) {
|
||||
@@ -3290,7 +3422,7 @@ Raven.prototype = {
|
||||
}
|
||||
]
|
||||
},
|
||||
culprit: fileurl
|
||||
transaction: fileurl
|
||||
},
|
||||
options
|
||||
);
|
||||
@@ -3364,7 +3496,7 @@ Raven.prototype = {
|
||||
|
||||
if (this._hasNavigator && _navigator.userAgent) {
|
||||
httpData.headers = {
|
||||
'User-Agent': navigator.userAgent
|
||||
'User-Agent': _navigator.userAgent
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3405,7 +3537,7 @@ Raven.prototype = {
|
||||
if (
|
||||
!last ||
|
||||
current.message !== last.message || // defined for captureMessage
|
||||
current.culprit !== last.culprit // defined for captureException/onerror
|
||||
current.transaction !== last.transaction // defined for captureException/onerror
|
||||
)
|
||||
return false;
|
||||
|
||||
@@ -3750,7 +3882,11 @@ Raven.prototype = {
|
||||
},
|
||||
|
||||
_logDebug: function(level) {
|
||||
if (this._originalConsoleMethods[level] && this.debug) {
|
||||
// We allow `Raven.debug` and `Raven.config(DSN, { debug: true })` to not make backward incompatible API change
|
||||
if (
|
||||
this._originalConsoleMethods[level] &&
|
||||
(this.debug || this._globalOptions.debug)
|
||||
) {
|
||||
// In IE<10 console methods do not have their own 'apply' method
|
||||
Function.prototype.apply.call(
|
||||
this._originalConsoleMethods[level],
|
||||
@@ -3823,11 +3959,11 @@ var singleton = Raven$1;
|
||||
* const someAppReporter = new Raven.Client();
|
||||
* const someOtherAppReporter = new Raven.Client();
|
||||
*
|
||||
* someAppReporter('__DSN__', {
|
||||
* someAppReporter.config('__DSN__', {
|
||||
* ...config goes here
|
||||
* });
|
||||
*
|
||||
* someOtherAppReporter('__OTHER_DSN__', {
|
||||
* someOtherAppReporter.config('__OTHER_DSN__', {
|
||||
* ...config goes here
|
||||
* });
|
||||
*
|
||||
@@ -3887,7 +4023,7 @@ singleton.Client = Client;
|
||||
});
|
||||
|
||||
// Setup the player
|
||||
var player = new Plyr('video', {
|
||||
var player = new Plyr('#player', {
|
||||
debug: true,
|
||||
title: 'View From A Blue Moon',
|
||||
iconUrl: '../dist/plyr.svg',
|
||||
@@ -3914,6 +4050,39 @@ singleton.Client = Client;
|
||||
'airplay',
|
||||
'fullscreen',
|
||||
], */
|
||||
/* i18n: {
|
||||
restart: '重新開始',
|
||||
rewind: '快退{seektime}秒',
|
||||
play: '播放',
|
||||
pause: '暫停',
|
||||
fastForward: '快進{seektime}秒',
|
||||
seek: '尋求',
|
||||
played: '發揮',
|
||||
buffered: '緩衝的',
|
||||
currentTime: '當前時間戳',
|
||||
duration: '長短',
|
||||
volume: '音量',
|
||||
mute: '靜音',
|
||||
unmute: '取消靜音',
|
||||
enableCaptions: '開啟字幕',
|
||||
disableCaptions: '關閉字幕',
|
||||
enterFullscreen: '進入全螢幕',
|
||||
exitFullscreen: '退出全螢幕',
|
||||
frameTitle: '球員為{title}',
|
||||
captions: '字幕',
|
||||
settings: '設定',
|
||||
speed: '速度',
|
||||
normal: '正常',
|
||||
quality: '質量',
|
||||
loop: '循環',
|
||||
start: 'Start',
|
||||
end: 'End',
|
||||
all: 'All',
|
||||
reset: '重啟',
|
||||
disabled: '殘',
|
||||
enabled: '啟用',
|
||||
advertisement: '廣告',
|
||||
}, */
|
||||
captions: {
|
||||
active: true
|
||||
},
|
||||
@@ -4011,7 +4180,6 @@ singleton.Client = Client;
|
||||
case types.youtube:
|
||||
player.source = {
|
||||
type: 'video',
|
||||
title: 'View From A Blue Moon',
|
||||
sources: [{
|
||||
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
|
||||
provider: 'youtube'
|
||||
|
||||
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
File diff suppressed because one or more lines are too long
+7
-1
@@ -6,8 +6,14 @@
|
||||
<title>Doh. Looks like something went wrong.</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico">
|
||||
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png">
|
||||
|
||||
<!-- Docs styles -->
|
||||
<link rel="stylesheet" href="dist/error.css">
|
||||
<link rel="stylesheet" href="dist/error.css?v=2">
|
||||
|
||||
<!-- Preload -->
|
||||
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
|
||||
|
||||
+4
-4
@@ -27,7 +27,7 @@
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
|
||||
<!-- Docs styles -->
|
||||
<link rel="stylesheet" href="dist/demo.css">
|
||||
<link rel="stylesheet" href="dist/demo.css?v=2">
|
||||
|
||||
<!-- Preload -->
|
||||
<link rel="preload" as="font" crossorigin type="font/woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2">
|
||||
@@ -114,7 +114,7 @@
|
||||
<title>HTML5</title>
|
||||
<path d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"></path>
|
||||
</svg>
|
||||
<a href="http://viewfromabluemoon.com/" target="_blank">View From A Blue Moon</a> © Brainfarm
|
||||
<a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank">View From A Blue Moon</a> © Brainfarm
|
||||
</small>
|
||||
</li>
|
||||
<li class="plyr__cite plyr__cite--audio" hidden>
|
||||
@@ -141,7 +141,7 @@
|
||||
</li>
|
||||
<li class="plyr__cite plyr__cite--vimeo" hidden>
|
||||
<small>
|
||||
<a href="https://vimeo.com/ondemand/viewfromabluemoon4k" target="_blank">View From A Blue Moon</a> on
|
||||
<a href="https://vimeo.com/76979871" target="_blank">The New Vimeo Player</a> on
|
||||
<span class="color--vimeo">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>Vimeo</title>
|
||||
@@ -171,7 +171,7 @@
|
||||
</aside>
|
||||
|
||||
<!-- Polyfills -->
|
||||
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values"
|
||||
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Plyr core script -->
|
||||
|
||||
+34
-2
@@ -47,7 +47,7 @@ import Raven from 'raven-js';
|
||||
});
|
||||
|
||||
// Setup the player
|
||||
const player = new Plyr('video', {
|
||||
const player = new Plyr('#player', {
|
||||
debug: true,
|
||||
title: 'View From A Blue Moon',
|
||||
iconUrl: '../dist/plyr.svg',
|
||||
@@ -74,6 +74,39 @@ import Raven from 'raven-js';
|
||||
'airplay',
|
||||
'fullscreen',
|
||||
], */
|
||||
/* i18n: {
|
||||
restart: '重新開始',
|
||||
rewind: '快退{seektime}秒',
|
||||
play: '播放',
|
||||
pause: '暫停',
|
||||
fastForward: '快進{seektime}秒',
|
||||
seek: '尋求',
|
||||
played: '發揮',
|
||||
buffered: '緩衝的',
|
||||
currentTime: '當前時間戳',
|
||||
duration: '長短',
|
||||
volume: '音量',
|
||||
mute: '靜音',
|
||||
unmute: '取消靜音',
|
||||
enableCaptions: '開啟字幕',
|
||||
disableCaptions: '關閉字幕',
|
||||
enterFullscreen: '進入全螢幕',
|
||||
exitFullscreen: '退出全螢幕',
|
||||
frameTitle: '球員為{title}',
|
||||
captions: '字幕',
|
||||
settings: '設定',
|
||||
speed: '速度',
|
||||
normal: '正常',
|
||||
quality: '質量',
|
||||
loop: '循環',
|
||||
start: 'Start',
|
||||
end: 'End',
|
||||
all: 'All',
|
||||
reset: '重啟',
|
||||
disabled: '殘',
|
||||
enabled: '啟用',
|
||||
advertisement: '廣告',
|
||||
}, */
|
||||
captions: {
|
||||
active: true,
|
||||
},
|
||||
@@ -182,7 +215,6 @@ import Raven from 'raven-js';
|
||||
case types.youtube:
|
||||
player.source = {
|
||||
type: 'video',
|
||||
title: 'View From A Blue Moon',
|
||||
sources: [{
|
||||
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
|
||||
provider: 'youtube',
|
||||
|
||||
@@ -3,12 +3,6 @@
|
||||
// ==========================================================================
|
||||
@charset 'UTF-8';
|
||||
|
||||
// Libs
|
||||
@import '../lib/fontface';
|
||||
@import '../lib/mixins';
|
||||
@import '../lib/normalize';
|
||||
@import '../lib/reset';
|
||||
|
||||
// Settings
|
||||
@import '../settings/colors';
|
||||
@import '../settings/cosmetic';
|
||||
@@ -17,6 +11,12 @@
|
||||
@import '../settings/spacing';
|
||||
@import '../settings/type';
|
||||
|
||||
// Libs
|
||||
@import '../lib/fontface';
|
||||
@import '../lib/mixins';
|
||||
@import '../lib/normalize';
|
||||
@import '../lib/reset';
|
||||
|
||||
// Layout
|
||||
@import '../layout/error';
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ video {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
// Style full supported player
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
// Layout
|
||||
// ==========================================================================
|
||||
|
||||
$container-max-width: 1280px;
|
||||
$container-max-width: 1260px;
|
||||
|
||||
@@ -6,5 +6,6 @@ h1 {
|
||||
@include font-size($font-size-h1);
|
||||
font-weight: $font-weight-bold;
|
||||
letter-spacing: $letter-spacing-headings;
|
||||
margin: 0 0 ($spacing-base / 2);
|
||||
line-height: 1.2;
|
||||
margin: 0 0 $spacing-base;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2946
-2614
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
+3335
-2631
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
+12
-9
@@ -13,6 +13,7 @@ const filter = require('gulp-filter');
|
||||
const sass = require('gulp-sass');
|
||||
const cleancss = require('gulp-clean-css');
|
||||
const run = require('run-sequence');
|
||||
const header = require('gulp-header');
|
||||
const prefix = require('gulp-autoprefixer');
|
||||
const gitbranch = require('git-branch');
|
||||
const svgstore = require('gulp-svgstore');
|
||||
@@ -129,7 +130,7 @@ const build = {
|
||||
tasks.js.push(name);
|
||||
const { output } = paths[bundle];
|
||||
|
||||
gulp.task(name, () =>
|
||||
return gulp.task(name, () =>
|
||||
gulp
|
||||
.src(bundles[bundle].js[key])
|
||||
.pipe(sourcemaps.init())
|
||||
@@ -146,6 +147,7 @@ const build = {
|
||||
options,
|
||||
),
|
||||
)
|
||||
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
|
||||
.pipe(sourcemaps.write(''))
|
||||
.pipe(gulp.dest(output))
|
||||
.pipe(filter('**/*.js'))
|
||||
@@ -162,7 +164,7 @@ const build = {
|
||||
const name = `sass:${key}`;
|
||||
tasks.sass.push(name);
|
||||
|
||||
gulp.task(name, () =>
|
||||
return gulp.task(name, () =>
|
||||
gulp
|
||||
.src(bundles[bundle].sass[key])
|
||||
.pipe(sass())
|
||||
@@ -180,7 +182,7 @@ const build = {
|
||||
tasks.sprite.push(name);
|
||||
|
||||
// Process Icons
|
||||
gulp.task(name, () =>
|
||||
return gulp.task(name, () =>
|
||||
gulp
|
||||
.src(paths[bundle].src.sprite)
|
||||
.pipe(
|
||||
@@ -239,11 +241,11 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
||||
const branch = {
|
||||
current: gitbranch.sync(),
|
||||
master: 'master',
|
||||
beta: 'beta',
|
||||
develop: 'develop',
|
||||
};
|
||||
const allowed = [
|
||||
branch.master,
|
||||
branch.beta,
|
||||
branch.develop,
|
||||
];
|
||||
|
||||
const maxAge = 31536000; // 1 year
|
||||
@@ -255,7 +257,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
||||
},
|
||||
},
|
||||
demo: {
|
||||
uploadPath: branch.current === branch.beta ? 'beta/' : null,
|
||||
uploadPath: branch.current === branch.develop ? 'beta/' : null,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
|
||||
Vary: 'Accept-Encoding',
|
||||
@@ -287,7 +289,8 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
||||
'plyr.polyfilled.js',
|
||||
'defaults.js',
|
||||
];
|
||||
gulp
|
||||
|
||||
return gulp
|
||||
.src(files.map(file => path.join(root, `src/js/${file}`)))
|
||||
.pipe(replace(semver, `v${version}`))
|
||||
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
|
||||
@@ -406,7 +409,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
||||
});
|
||||
|
||||
// Do everything
|
||||
gulp.task('publish', () => {
|
||||
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo');
|
||||
gulp.task('publish', callback => {
|
||||
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo', callback);
|
||||
});
|
||||
}
|
||||
|
||||
+20
-16
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plyr",
|
||||
"version": "3.1.0",
|
||||
"version": "3.3.9",
|
||||
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||
"homepage": "https://plyr.io",
|
||||
"main": "./dist/plyr.js",
|
||||
@@ -8,45 +8,48 @@
|
||||
"sass": "./src/sass/plyr.scss",
|
||||
"style": "./dist/plyr.css",
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.2.2",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"del": "^3.0.0",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"eslint-plugin-import": "^2.10.0",
|
||||
"eslint-plugin-import": "^2.12.0",
|
||||
"git-branch": "^2.0.1",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-autoprefixer": "^5.0.0",
|
||||
"gulp-better-rollup": "^3.1.0",
|
||||
"gulp-clean-css": "^3.9.3",
|
||||
"gulp-clean-css": "^3.9.4",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-filter": "^5.1.0",
|
||||
"gulp-header": "^2.0.5",
|
||||
"gulp-open": "^3.0.1",
|
||||
"gulp-rename": "^1.2.2",
|
||||
"gulp-replace": "^0.6.1",
|
||||
"gulp-postcss": "^7.0.1",
|
||||
"gulp-rename": "^1.2.3",
|
||||
"gulp-replace": "^1.0.0",
|
||||
"gulp-s3": "^0.11.0",
|
||||
"gulp-sass": "^3.2.1",
|
||||
"gulp-sass": "^4.0.1",
|
||||
"gulp-size": "^3.0.0",
|
||||
"gulp-sourcemaps": "^2.6.4",
|
||||
"gulp-svgmin": "^1.2.4",
|
||||
"gulp-svgstore": "^6.1.1",
|
||||
"gulp-uglify-es": "^1.0.1",
|
||||
"gulp-uglify-es": "^1.0.4",
|
||||
"gulp-util": "^3.0.8",
|
||||
"postcss-custom-properties": "^7.0.0",
|
||||
"prettier-eslint": "^8.8.1",
|
||||
"prettier-stylelint": "^0.4.2",
|
||||
"rollup-plugin-babel": "^3.0.3",
|
||||
"rollup-plugin-commonjs": "^9.1.0",
|
||||
"rollup-plugin-babel": "^3.0.4",
|
||||
"rollup-plugin-commonjs": "^9.1.3",
|
||||
"rollup-plugin-node-resolve": "^3.3.0",
|
||||
"run-sequence": "^2.2.1",
|
||||
"stylelint": "^9.2.0",
|
||||
"stylelint-config-prettier": "^3.0.4",
|
||||
"stylelint": "^9.2.1",
|
||||
"stylelint-config-prettier": "^3.2.0",
|
||||
"stylelint-config-recommended": "^2.1.0",
|
||||
"stylelint-config-sass-guidelines": "^5.0.0",
|
||||
"stylelint-order": "^0.8.1",
|
||||
"stylelint-scss": "^3.0.0",
|
||||
"stylelint-scss": "^3.1.0",
|
||||
"stylelint-selector-bem-pattern": "^2.0.0"
|
||||
},
|
||||
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
|
||||
@@ -69,6 +72,7 @@
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"custom-event-polyfill": "^0.3.0",
|
||||
"loadjs": "^3.5.4",
|
||||
"raven-js": "^3.24.0"
|
||||
"raven-js": "^3.25.2",
|
||||
"url-polyfill": "^1.0.13"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,13 +39,13 @@ Check out the [changelog](changelog.md) to see what's new with Plyr.
|
||||
|
||||
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
|
||||
|
||||
| Type | Maintainer | Link |
|
||||
| --------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| WordPress | Ryan Anthony Drake ([@iamryandrake](https://github.com/iamryandrake)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
|
||||
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
|
||||
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
|
||||
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
|
||||
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
|
||||
| Type | Maintainer | Link |
|
||||
| --------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
|
||||
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
|
||||
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
|
||||
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
|
||||
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
|
||||
|
||||
## Quick setup
|
||||
|
||||
@@ -125,13 +125,17 @@ Include the `plyr.js` script before the closing `</body>` tag and then in your J
|
||||
|
||||
See [initialising](#initialising) for more information on advanced setups.
|
||||
|
||||
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following:
|
||||
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
|
||||
|
||||
```html
|
||||
<script src="https://cdn.plyr.io/3.1.0/plyr.js"></script>
|
||||
<script src="https://cdn.plyr.io/3.3.9/plyr.js"></script>
|
||||
```
|
||||
|
||||
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility
|
||||
...or...
|
||||
|
||||
```html
|
||||
<script src="https://cdn.plyr.io/3.3.9/plyr.polyfilled.js"></script>
|
||||
```
|
||||
|
||||
### CSS
|
||||
|
||||
@@ -144,13 +148,13 @@ Include the `plyr.css` stylsheet into your `<head>`
|
||||
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.1.0/plyr.css">
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.9/plyr.css">
|
||||
```
|
||||
|
||||
### SVG Sprite
|
||||
|
||||
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
|
||||
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.1.0/plyr.svg`.
|
||||
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.9/plyr.svg`.
|
||||
|
||||
## Ads
|
||||
|
||||
@@ -210,7 +214,7 @@ You can specify a range of arguments for the constructor to use:
|
||||
* A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
|
||||
* A [jQuery](https://jquery.com) object
|
||||
|
||||
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup.
|
||||
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below.
|
||||
|
||||
Here's some examples
|
||||
|
||||
@@ -226,20 +230,32 @@ Passing a [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElemen
|
||||
const player = new Plyr(document.getElementById('player'));
|
||||
```
|
||||
|
||||
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList):
|
||||
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) (see note below):
|
||||
|
||||
```javascript
|
||||
const player = new Plyr(document.querySelectorAll('.js-player'));
|
||||
```
|
||||
|
||||
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds
|
||||
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds.
|
||||
|
||||
##### Setting up multiple players
|
||||
|
||||
You have two choices here. You can either use a simple array loop to map the constructor:
|
||||
|
||||
```javascript
|
||||
const players = Array.from(document.querySelectorAll('.js-player')).map(player => new Plyr(player));
|
||||
const players = Array.from(document.querySelectorAll('.js-player')).map(p => new Plyr(p));
|
||||
```
|
||||
|
||||
...or use a static method where you can pass a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) or an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of elements:
|
||||
|
||||
```javascript
|
||||
const players = Plyr.setup('.js-player');
|
||||
```
|
||||
|
||||
Both options will also return an array of instances in the order of they were in the DOM for the string selector or the source NodeList or Array.
|
||||
|
||||
##### Passing options
|
||||
|
||||
The second argument for the constructor is the [options](#options) object:
|
||||
|
||||
```javascript
|
||||
@@ -248,7 +264,7 @@ const player = new Plyr('#player', {
|
||||
});
|
||||
```
|
||||
|
||||
The constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
|
||||
In all cases, the constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
|
||||
|
||||
#### Options
|
||||
|
||||
@@ -279,7 +295,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
||||
| `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. |
|
||||
| `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. |
|
||||
| `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. |
|
||||
| `showPosterOnEnd` | Boolean | false | This will restore and _reload_ HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution. |
|
||||
| `resetOnEnd` | Boolean | false | Reset the playback to the start once playback is complete. |
|
||||
| `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally |
|
||||
| `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. |
|
||||
| `duration` | Number | `null` | Specify a custom duration for media. |
|
||||
@@ -345,7 +361,7 @@ player.fullscreen.enter(); // Enter fullscreen
|
||||
| `fullscreen.exit()` | - | Exit fullscreen. |
|
||||
| `fullscreen.toggle()` | - | Toggle fullscreen. |
|
||||
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
|
||||
| `toggleControls(toggle)` | Boolean | Toggle the controls based on the specified boolean. |
|
||||
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
|
||||
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
|
||||
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
|
||||
| `supports(type)` | String | Check support for a mime type. |
|
||||
@@ -374,8 +390,9 @@ player.fullscreen.active; // false;
|
||||
| -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `isHTML5` | ✓ | - | Returns a boolean indicating if the current player is HTML5. |
|
||||
| `isEmbed` | ✓ | - | Returns a boolean indicating if the current player is an embedded player. |
|
||||
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
|
||||
| `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. |
|
||||
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
|
||||
| `stopped` | ✓ | - | Returns a boolean indicating if the current player is stopped. |
|
||||
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
|
||||
| `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
|
||||
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
|
||||
@@ -388,7 +405,7 @@ player.fullscreen.active; // false;
|
||||
| `quality`¹ | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
|
||||
| `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
|
||||
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
|
||||
| `poster`² | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
|
||||
| `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
|
||||
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
|
||||
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. |
|
||||
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
|
||||
@@ -599,6 +616,8 @@ Russitto ([@russitto](https://github.com/russitto)) for working on this. Here's
|
||||
* Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR)
|
||||
* Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN)
|
||||
|
||||
_Note_: These need updating to use the new v3 syntax but would still work.
|
||||
|
||||
## Fullscreen
|
||||
|
||||
Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
|
||||
@@ -607,19 +626,20 @@ Fullscreen in Plyr is supported by all browsers that [currently support it](http
|
||||
|
||||
Plyr supports the last 2 versions of most _modern_ browsers.
|
||||
|
||||
| Browser | Supported |
|
||||
| ------------- | --------- |
|
||||
| Safari | ✓ |
|
||||
| Mobile Safari | ✓¹ |
|
||||
| Firefox | ✓ |
|
||||
| Chrome | ✓ |
|
||||
| Opera | ✓ |
|
||||
| Edge | ✓ |
|
||||
| IE11 | ✓ |
|
||||
| IE10 | ✓² |
|
||||
| Browser | Supported |
|
||||
| ------------- | ------------- |
|
||||
| Safari | ✓ |
|
||||
| Mobile Safari | ✓¹ |
|
||||
| Firefox | ✓ |
|
||||
| Chrome | ✓ |
|
||||
| Opera | ✓ |
|
||||
| Edge | ✓ |
|
||||
| IE11 | ✓³ |
|
||||
| IE10 | ✓²³ |
|
||||
|
||||
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
|
||||
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options))
|
||||
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)).
|
||||
3. Polyfills required. See below.
|
||||
|
||||
### Polyfills
|
||||
|
||||
@@ -668,8 +688,10 @@ Plyr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me]
|
||||
|
||||
## Donate
|
||||
|
||||
Plyr costs money to run, not only my time - I donate that for free but domains, hosting and more. Any help is appreciated...
|
||||
[Donate to support Plyr](https://www.paypal.me/pottsy/20usd)
|
||||
Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated...
|
||||
|
||||
* [Donate via Patron](https://www.patreon.com/plyr)
|
||||
* [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
|
||||
|
||||
## Mentions
|
||||
|
||||
@@ -707,10 +729,14 @@ Credit to the PayPal HTML5 Video player from which Plyr's caption functionality
|
||||
|
||||
## Thanks
|
||||
|
||||
[](https://www.fastly.com/)
|
||||
[](https://www.fastly.com/)
|
||||
|
||||
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.
|
||||
|
||||
[](https://sentry.io/)
|
||||
|
||||
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
|
||||
|
||||
## Copyright and License
|
||||
|
||||
[The MIT license](license.md)
|
||||
|
||||
+47
-8
@@ -3,9 +3,10 @@
|
||||
// TODO: Create as class
|
||||
// ==========================================================================
|
||||
|
||||
import controls from './controls';
|
||||
import i18n from './i18n';
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
import controls from './controls';
|
||||
|
||||
const captions = {
|
||||
// Setup captions
|
||||
@@ -46,6 +47,7 @@ const captions = {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject the container
|
||||
if (!utils.is.element(this.elements.captions)) {
|
||||
this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions));
|
||||
@@ -148,7 +150,49 @@ const captions = {
|
||||
|
||||
// Get the current track for the current language
|
||||
getCurrentTrack() {
|
||||
return captions.getTracks.call(this).find(track => track.language.toLowerCase() === this.language);
|
||||
const tracks = captions.getTracks.call(this);
|
||||
|
||||
if (!tracks.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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
|
||||
getLabel(track) {
|
||||
let currentTrack = track;
|
||||
|
||||
if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) {
|
||||
currentTrack = captions.getCurrentTrack.call(this);
|
||||
}
|
||||
|
||||
if (utils.is.track(currentTrack)) {
|
||||
if (!utils.is.empty(currentTrack.label)) {
|
||||
return currentTrack.label;
|
||||
}
|
||||
|
||||
if (!utils.is.empty(currentTrack.language)) {
|
||||
return track.language.toUpperCase();
|
||||
}
|
||||
|
||||
return i18n.get('enabled', this.config);
|
||||
}
|
||||
|
||||
return i18n.get('disabled', this.config);
|
||||
},
|
||||
|
||||
// Display active caption if it contains text
|
||||
@@ -192,7 +236,7 @@ const captions = {
|
||||
|
||||
// Set the span content
|
||||
if (utils.is.string(caption)) {
|
||||
content.textContent = caption.trim();
|
||||
content.innerText = caption.trim();
|
||||
} else {
|
||||
content.appendChild(caption);
|
||||
}
|
||||
@@ -206,11 +250,6 @@ const captions = {
|
||||
|
||||
// Display captions container and button (for initialization)
|
||||
show() {
|
||||
// If there's no caption toggle, bail
|
||||
if (!utils.is.element(this.elements.buttons.captions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to load the value from storage
|
||||
let active = this.storage.get('captions');
|
||||
|
||||
|
||||
Vendored
+339
-112
@@ -2,49 +2,88 @@
|
||||
// Plyr controls
|
||||
// ==========================================================================
|
||||
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
import ui from './ui';
|
||||
import i18n from './i18n';
|
||||
import captions from './captions';
|
||||
import html5 from './html5';
|
||||
import i18n from './i18n';
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
|
||||
// Sniff out the browser
|
||||
const browser = utils.getBrowser();
|
||||
|
||||
const controls = {
|
||||
// Webkit polyfill for lower fill range
|
||||
updateRangeFill(target) {
|
||||
// WebKit only
|
||||
if (!browser.isWebkit) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 CSS custom property
|
||||
range.style.setProperty('--value', `${range.value / range.max * 100}%`);
|
||||
},
|
||||
|
||||
// Get icon URL
|
||||
getIconUrl() {
|
||||
const url = new URL(this.config.iconUrl, window.location);
|
||||
const cors = url.host !== window.location.host || (browser.isIE && !window.svg4everybody);
|
||||
|
||||
return {
|
||||
url: this.config.iconUrl,
|
||||
absolute: this.config.iconUrl.indexOf('http') === 0 || (browser.isIE && !window.svg4everybody),
|
||||
cors,
|
||||
};
|
||||
},
|
||||
|
||||
// Find the UI controls and store references in custom controls
|
||||
// TODO: Allow settings menus with custom controls
|
||||
findElements() {
|
||||
try {
|
||||
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
|
||||
|
||||
// Buttons
|
||||
this.elements.buttons = {
|
||||
play: utils.getElements.call(this, this.config.selectors.buttons.play),
|
||||
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
|
||||
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
|
||||
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
|
||||
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
|
||||
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
|
||||
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
|
||||
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
|
||||
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
|
||||
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
|
||||
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
|
||||
};
|
||||
|
||||
// Progress
|
||||
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
|
||||
|
||||
// Inputs
|
||||
this.elements.inputs = {
|
||||
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
|
||||
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
|
||||
};
|
||||
|
||||
// Display
|
||||
this.elements.display = {
|
||||
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
|
||||
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
|
||||
duration: utils.getElement.call(this, this.config.selectors.display.duration),
|
||||
};
|
||||
|
||||
// Seek tooltip
|
||||
if (utils.is.element(this.elements.progress)) {
|
||||
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Log it
|
||||
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
|
||||
|
||||
// Restore native video controls
|
||||
this.toggleNativeControls(true);
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Create <svg> icon
|
||||
createIcon(type, attributes) {
|
||||
const namespace = 'http://www.w3.org/2000/svg';
|
||||
const iconUrl = controls.getIconUrl.call(this);
|
||||
const iconPath = `${!iconUrl.absolute ? iconUrl.url : ''}#${this.config.iconPrefix}`;
|
||||
const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;
|
||||
|
||||
// Create <svg>
|
||||
const icon = document.createElementNS(namespace, 'svg');
|
||||
@@ -52,6 +91,7 @@ const controls = {
|
||||
icon,
|
||||
utils.extend(attributes, {
|
||||
role: 'presentation',
|
||||
focusable: 'false',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -206,7 +246,6 @@ const controls = {
|
||||
|
||||
// Add aria attributes
|
||||
attributes['aria-pressed'] = false;
|
||||
attributes['aria-label'] = i18n.get(label, this.config);
|
||||
} else {
|
||||
button.appendChild(controls.createIcon.call(this, icon));
|
||||
button.appendChild(controls.createLabel.call(this, label));
|
||||
@@ -238,6 +277,7 @@ const controls = {
|
||||
'label',
|
||||
{
|
||||
for: attributes.id,
|
||||
id: `${attributes.id}-label`,
|
||||
class: this.config.classNames.hidden,
|
||||
},
|
||||
i18n.get(type, this.config),
|
||||
@@ -255,6 +295,12 @@ const controls = {
|
||||
step: 0.01,
|
||||
value: 0,
|
||||
autocomplete: 'off',
|
||||
// A11y fixes for https://github.com/sampotts/plyr/issues/905
|
||||
role: 'slider',
|
||||
'aria-labelledby': `${attributes.id}-label`,
|
||||
'aria-valuemin': 0,
|
||||
'aria-valuemax': 100,
|
||||
'aria-valuenow': 0,
|
||||
},
|
||||
attributes,
|
||||
),
|
||||
@@ -281,6 +327,8 @@ const controls = {
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: 0,
|
||||
role: 'presentation',
|
||||
'aria-hidden': true,
|
||||
},
|
||||
attributes,
|
||||
),
|
||||
@@ -304,7 +352,7 @@ const controls = {
|
||||
break;
|
||||
}
|
||||
|
||||
progress.textContent = `% ${suffix.toLowerCase()}`;
|
||||
progress.innerText = `% ${suffix.toLowerCase()}`;
|
||||
}
|
||||
|
||||
this.elements.display[type] = progress;
|
||||
@@ -314,22 +362,14 @@ const controls = {
|
||||
|
||||
// Create time display
|
||||
createTime(type) {
|
||||
const container = utils.createElement('div', {
|
||||
class: 'plyr__time',
|
||||
});
|
||||
const attributes = utils.getAttributesFromSelector(this.config.selectors.display[type]);
|
||||
|
||||
container.appendChild(
|
||||
utils.createElement(
|
||||
'span',
|
||||
{
|
||||
class: this.config.classNames.hidden,
|
||||
},
|
||||
i18n.get(type, this.config),
|
||||
),
|
||||
);
|
||||
|
||||
container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.display[type]), '00:00'));
|
||||
const container = utils.createElement('div', utils.extend(attributes, {
|
||||
class: `plyr__time ${attributes.class}`,
|
||||
'aria-label': i18n.get(type, this.config),
|
||||
}), '00:00');
|
||||
|
||||
// Reference for updates
|
||||
this.elements.display[type] = container;
|
||||
|
||||
return container;
|
||||
@@ -354,7 +394,7 @@ const controls = {
|
||||
}),
|
||||
);
|
||||
|
||||
const faux = utils.createElement('span', { 'aria-hidden': true });
|
||||
const faux = utils.createElement('span', { hidden: '' });
|
||||
|
||||
label.appendChild(radio);
|
||||
label.appendChild(faux);
|
||||
@@ -368,6 +408,124 @@ const controls = {
|
||||
list.appendChild(item);
|
||||
},
|
||||
|
||||
// Update the displayed time
|
||||
updateTimeDisplay(target = null, time = 0, inverted = false) {
|
||||
// Bail if there's no element to display or the value isn't a number
|
||||
if (!utils.is.element(target) || !utils.is.number(time)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always display hours if duration is over an hour
|
||||
const forceHours = utils.getHours(this.duration) > 0;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
target.innerText = utils.formatTime(time, forceHours, inverted);
|
||||
},
|
||||
|
||||
// Update volume UI and storage
|
||||
updateVolume() {
|
||||
if (!this.supported.ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update range
|
||||
if (utils.is.element(this.elements.inputs.volume)) {
|
||||
controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
|
||||
}
|
||||
|
||||
// Update mute state
|
||||
if (utils.is.element(this.elements.buttons.mute)) {
|
||||
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
|
||||
}
|
||||
},
|
||||
|
||||
// Update seek value and lower fill
|
||||
setRange(target, value = 0) {
|
||||
if (!utils.is.element(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
target.value = value;
|
||||
|
||||
// Webkit range fill
|
||||
controls.updateRangeFill.call(this, target);
|
||||
},
|
||||
|
||||
// Update <progress> elements
|
||||
updateProgress(event) {
|
||||
if (!this.supported.ui || !utils.is.event(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = 0;
|
||||
|
||||
const setProgress = (target, input) => {
|
||||
const value = utils.is.number(input) ? input : 0;
|
||||
const progress = utils.is.element(target) ? target : this.elements.display.buffer;
|
||||
|
||||
// Update value and label
|
||||
if (utils.is.element(progress)) {
|
||||
progress.value = value;
|
||||
|
||||
// Update text label inside
|
||||
const label = progress.getElementsByTagName('span')[0];
|
||||
if (utils.is.element(label)) {
|
||||
label.childNodes[0].nodeValue = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (event) {
|
||||
switch (event.type) {
|
||||
// Video playing
|
||||
case 'timeupdate':
|
||||
case 'seeking':
|
||||
case 'seeked':
|
||||
value = utils.getPercentage(this.currentTime, this.duration);
|
||||
|
||||
// Set seek range value only if it's a 'natural' time event
|
||||
if (event.type === 'timeupdate') {
|
||||
controls.setRange.call(this, this.elements.inputs.seek, value);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Check buffer status
|
||||
case 'playing':
|
||||
case 'progress':
|
||||
setProgress(this.elements.display.buffer, this.buffered * 100);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Webkit polyfill for lower fill range
|
||||
updateRangeFill(target) {
|
||||
// Get range from event if event passed
|
||||
const range = utils.is.event(target) ? target.target : target;
|
||||
|
||||
// Needs to be a valid <input type='range'>
|
||||
if (!utils.is.element(range) || range.getAttribute('type') !== 'range') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set aria value for https://github.com/sampotts/plyr/issues/905
|
||||
range.setAttribute('aria-valuenow', range.value);
|
||||
|
||||
// WebKit only
|
||||
if (!browser.isWebkit) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set CSS custom property
|
||||
range.style.setProperty('--value', `${range.value / range.max * 100}%`);
|
||||
},
|
||||
|
||||
// Update hover tooltip for seeking
|
||||
updateSeekTooltip(event) {
|
||||
// Bail if setting not true
|
||||
@@ -382,7 +540,7 @@ const controls = {
|
||||
|
||||
// Calculate percentage
|
||||
let percent = 0;
|
||||
const clientRect = this.elements.inputs.seek.getBoundingClientRect();
|
||||
const clientRect = this.elements.progress.getBoundingClientRect();
|
||||
const visible = `${this.config.classNames.tooltip}--visible`;
|
||||
|
||||
const toggle = toggle => {
|
||||
@@ -412,7 +570,7 @@ const controls = {
|
||||
}
|
||||
|
||||
// Display the time a click would seek to
|
||||
ui.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent);
|
||||
controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent);
|
||||
|
||||
// Set position
|
||||
this.elements.display.seekTooltip.style.left = `${percent}%`;
|
||||
@@ -427,13 +585,50 @@ const controls = {
|
||||
}
|
||||
},
|
||||
|
||||
// Handle time change event
|
||||
timeUpdate(event) {
|
||||
// Only invert if only one time element is displayed and used for both duration and currentTime
|
||||
const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
|
||||
|
||||
// Duration
|
||||
controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
|
||||
|
||||
// Ignore updates while seeking
|
||||
if (event && event.type === 'timeupdate' && this.media.seeking) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Playing progress
|
||||
controls.updateProgress.call(this, event);
|
||||
},
|
||||
|
||||
// Show the duration on metadataloaded or durationchange events
|
||||
durationUpdate() {
|
||||
// Bail if no ui or durationchange event triggered after playing/seek when invertTime is false
|
||||
if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's a spot to display duration
|
||||
const hasDuration = utils.is.element(this.elements.display.duration);
|
||||
|
||||
// If there's only one time display, display duration there
|
||||
if (!hasDuration && this.config.displayDuration && this.paused) {
|
||||
controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
|
||||
}
|
||||
|
||||
// If there's a duration element, update content
|
||||
if (hasDuration) {
|
||||
controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
|
||||
}
|
||||
|
||||
// Update the tooltip (if visible)
|
||||
controls.updateSeekTooltip.call(this);
|
||||
},
|
||||
|
||||
// Hide/show a tab
|
||||
toggleTab(setting, toggle) {
|
||||
const tab = this.elements.settings.tabs[setting];
|
||||
const pane = this.elements.settings.panes[setting];
|
||||
|
||||
utils.toggleHidden(tab, !toggle);
|
||||
utils.toggleHidden(pane, !toggle);
|
||||
utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
|
||||
},
|
||||
|
||||
// Set the quality menu
|
||||
@@ -456,6 +651,9 @@ const controls = {
|
||||
const toggle = !utils.is.empty(this.options.quality) && this.options.quality.length > 1;
|
||||
controls.toggleTab.call(this, type, toggle);
|
||||
|
||||
// Check if we need to toggle the parent
|
||||
controls.checkMenu.call(this);
|
||||
|
||||
// If we're hiding, nothing more to do
|
||||
if (!toggle) {
|
||||
return;
|
||||
@@ -480,6 +678,7 @@ const controls = {
|
||||
break;
|
||||
|
||||
case 576:
|
||||
case 480:
|
||||
label = 'SD';
|
||||
break;
|
||||
|
||||
@@ -495,13 +694,15 @@ const controls = {
|
||||
};
|
||||
|
||||
// Sort options by the config and then render options
|
||||
this.options.quality.sort((a, b) => {
|
||||
const sorting = this.config.quality.options;
|
||||
return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
|
||||
}).forEach(quality => {
|
||||
const label = controls.getLabel.call(this, 'quality', quality);
|
||||
controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality));
|
||||
});
|
||||
this.options.quality
|
||||
.sort((a, b) => {
|
||||
const sorting = this.config.quality.options;
|
||||
return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
|
||||
})
|
||||
.forEach(quality => {
|
||||
const label = controls.getLabel.call(this, 'quality', quality);
|
||||
controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality));
|
||||
});
|
||||
|
||||
controls.updateSetting.call(this, type, list);
|
||||
},
|
||||
@@ -511,16 +712,17 @@ const controls = {
|
||||
getLabel(setting, value) {
|
||||
switch (setting) {
|
||||
case 'speed':
|
||||
return value === 1 ? 'Normal' : `${value}×`;
|
||||
return value === 1 ? i18n.get('normal', this.config) : `${value}×`;
|
||||
|
||||
case 'quality':
|
||||
if (utils.is.number(value)) {
|
||||
return `${value}p`;
|
||||
}
|
||||
|
||||
return utils.toTitleCase(value);
|
||||
|
||||
case 'captions':
|
||||
return controls.getLanguage.call(this);
|
||||
return captions.getLabel.call(this);
|
||||
|
||||
default:
|
||||
return null;
|
||||
@@ -535,7 +737,16 @@ const controls = {
|
||||
|
||||
switch (setting) {
|
||||
case 'captions':
|
||||
value = this.captions.active ? this.captions.language : i18n.get('disabled', this.config);
|
||||
if (this.captions.active) {
|
||||
if (this.options.captions.length > 2 || !this.options.captions.some(lang => lang === 'enabled')) {
|
||||
value = this.captions.language;
|
||||
} else {
|
||||
value = 'enabled';
|
||||
}
|
||||
} else {
|
||||
value = '';
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -566,17 +777,19 @@ const controls = {
|
||||
list = pane && pane.querySelector('ul');
|
||||
}
|
||||
|
||||
// Update the label
|
||||
if (!utils.is.empty(value)) {
|
||||
const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`);
|
||||
label.innerHTML = controls.getLabel.call(this, setting, value);
|
||||
// If there's no list it means it's not been rendered...
|
||||
if (!utils.is.element(list)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the radio option
|
||||
// Update the label
|
||||
const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`);
|
||||
label.innerHTML = controls.getLabel.call(this, setting, value);
|
||||
|
||||
// Find the radio option and check it
|
||||
const target = list && list.querySelector(`input[value="${value}"]`);
|
||||
|
||||
if (utils.is.element(target)) {
|
||||
// Check it
|
||||
target.checked = true;
|
||||
}
|
||||
},
|
||||
@@ -627,21 +840,6 @@ const controls = {
|
||||
|
||||
// Get current selected caption language
|
||||
// TODO: rework this to user the getter in the API?
|
||||
getLanguage() {
|
||||
if (!this.supported.ui) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (support.textTracks && captions.getTracks.call(this).length && this.captions.active) {
|
||||
const currentTrack = captions.getCurrentTrack.call(this);
|
||||
|
||||
if (utils.is.track(currentTrack)) {
|
||||
return currentTrack.label;
|
||||
}
|
||||
}
|
||||
|
||||
return i18n.get('disabled', this.config);
|
||||
},
|
||||
|
||||
// Set a list of available captions languages
|
||||
setCaptionsMenu() {
|
||||
@@ -656,6 +854,9 @@ const controls = {
|
||||
// Empty the menu
|
||||
utils.emptyElement(list);
|
||||
|
||||
// Check if we need to toggle the parent
|
||||
controls.checkMenu.call(this);
|
||||
|
||||
// If there's no captions, bail
|
||||
if (!toggle) {
|
||||
return;
|
||||
@@ -663,8 +864,8 @@ const controls = {
|
||||
|
||||
// Re-map the tracks into just the data we need
|
||||
const tracks = captions.getTracks.call(this).map(track => ({
|
||||
language: track.language,
|
||||
label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(),
|
||||
language: !utils.is.empty(track.language) ? track.language : 'enabled',
|
||||
label: captions.getLabel.call(this, track),
|
||||
}));
|
||||
|
||||
// Add the "Disabled" option to turn off captions
|
||||
@@ -680,12 +881,15 @@ const controls = {
|
||||
track.language,
|
||||
list,
|
||||
'language',
|
||||
track.label || track.language,
|
||||
controls.createBadge.call(this, track.language.toUpperCase()),
|
||||
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);
|
||||
},
|
||||
|
||||
@@ -736,10 +940,6 @@ const controls = {
|
||||
// Get the list to populate
|
||||
const list = this.elements.settings.panes.speed.querySelector('ul');
|
||||
|
||||
// Show the pane and tab
|
||||
utils.toggleHidden(this.elements.settings.tabs.speed, false);
|
||||
utils.toggleHidden(this.elements.settings.panes.speed, false);
|
||||
|
||||
// Empty the menu
|
||||
utils.emptyElement(list);
|
||||
|
||||
@@ -770,7 +970,7 @@ const controls = {
|
||||
return;
|
||||
}
|
||||
|
||||
const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.getAttribute('aria-hidden') === 'true';
|
||||
const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.hasAttribute('hidden');
|
||||
|
||||
if (utils.is.event(event)) {
|
||||
const isMenuItem = utils.is.element(form) && form.contains(event.target);
|
||||
@@ -795,7 +995,7 @@ const controls = {
|
||||
}
|
||||
|
||||
if (utils.is.element(form)) {
|
||||
form.setAttribute('aria-hidden', !show);
|
||||
utils.toggleHidden(form, !show);
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.menu.open, show);
|
||||
|
||||
if (show) {
|
||||
@@ -811,7 +1011,7 @@ const controls = {
|
||||
const clone = tab.cloneNode(true);
|
||||
clone.style.position = 'absolute';
|
||||
clone.style.opacity = 0;
|
||||
clone.setAttribute('aria-hidden', false);
|
||||
clone.removeAttribute('hidden');
|
||||
|
||||
// Prevent input's being unchecked due to the name being identical
|
||||
Array.from(clone.querySelectorAll('input[name]')).forEach(input => {
|
||||
@@ -836,11 +1036,9 @@ const controls = {
|
||||
},
|
||||
|
||||
// Toggle Menu
|
||||
showTab(event) {
|
||||
showTab(target = '') {
|
||||
const { menu } = this.elements.settings;
|
||||
const tab = event.target;
|
||||
const show = tab.getAttribute('aria-expanded') === 'false';
|
||||
const pane = document.getElementById(tab.getAttribute('aria-controls'));
|
||||
const pane = document.getElementById(target);
|
||||
|
||||
// Nothing to show, bail
|
||||
if (!utils.is.element(pane)) {
|
||||
@@ -855,7 +1053,7 @@ const controls = {
|
||||
|
||||
// Hide all other tabs
|
||||
// Get other tabs
|
||||
const current = menu.querySelector('[role="tabpanel"][aria-hidden="false"]');
|
||||
const current = menu.querySelector('[role="tabpanel"]:not([hidden])');
|
||||
const container = current.parentNode;
|
||||
|
||||
// Set other toggles to be expanded false
|
||||
@@ -899,12 +1097,16 @@ const controls = {
|
||||
}
|
||||
|
||||
// Set attributes on current tab
|
||||
current.setAttribute('aria-hidden', true);
|
||||
utils.toggleHidden(current, true);
|
||||
current.setAttribute('tabindex', -1);
|
||||
|
||||
// Set attributes on target
|
||||
pane.setAttribute('aria-hidden', !show);
|
||||
tab.setAttribute('aria-expanded', show);
|
||||
utils.toggleHidden(pane, false);
|
||||
|
||||
const tabs = utils.getElements.call(this, `[aria-controls="${target}"]`);
|
||||
Array.from(tabs).forEach(tab => {
|
||||
tab.setAttribute('aria-expanded', true);
|
||||
});
|
||||
pane.removeAttribute('tabindex');
|
||||
|
||||
// Focus the first item
|
||||
@@ -963,7 +1165,6 @@ const controls = {
|
||||
const tooltip = utils.createElement(
|
||||
'span',
|
||||
{
|
||||
role: 'tooltip',
|
||||
class: this.config.classNames.tooltip,
|
||||
},
|
||||
'00:00',
|
||||
@@ -1045,7 +1246,7 @@ const controls = {
|
||||
const form = utils.createElement('form', {
|
||||
class: 'plyr__menu__container',
|
||||
id: `plyr-settings-${data.id}`,
|
||||
'aria-hidden': true,
|
||||
hidden: '',
|
||||
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
|
||||
role: 'tablist',
|
||||
tabindex: -1,
|
||||
@@ -1055,7 +1256,6 @@ const controls = {
|
||||
|
||||
const home = utils.createElement('div', {
|
||||
id: `plyr-settings-${data.id}-home`,
|
||||
'aria-hidden': false,
|
||||
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
|
||||
role: 'tabpanel',
|
||||
});
|
||||
@@ -1106,11 +1306,10 @@ const controls = {
|
||||
this.config.settings.forEach(type => {
|
||||
const pane = utils.createElement('div', {
|
||||
id: `plyr-settings-${data.id}-${type}`,
|
||||
'aria-hidden': true,
|
||||
hidden: '',
|
||||
'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`,
|
||||
role: 'tabpanel',
|
||||
tabindex: -1,
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
const back = utils.createElement(
|
||||
@@ -1181,7 +1380,7 @@ const controls = {
|
||||
const icon = controls.getIconUrl.call(this);
|
||||
|
||||
// Only load external sprite using AJAX
|
||||
if (icon.absolute) {
|
||||
if (icon.cors) {
|
||||
utils.loadSprite(icon.url, 'sprite-plyr');
|
||||
}
|
||||
}
|
||||
@@ -1193,17 +1392,21 @@ const controls = {
|
||||
let container = null;
|
||||
this.elements.controls = null;
|
||||
|
||||
// HTML or Element passed as the option
|
||||
// Set template properties
|
||||
const props = {
|
||||
id: this.id,
|
||||
seektime: this.config.seekTime,
|
||||
title: this.config.title,
|
||||
};
|
||||
let update = true;
|
||||
|
||||
if (utils.is.string(this.config.controls) || utils.is.element(this.config.controls)) {
|
||||
// String or HTMLElement passed as the option
|
||||
container = this.config.controls;
|
||||
} else if (utils.is.function(this.config.controls)) {
|
||||
// A custom function to build controls
|
||||
// The function can return a HTMLElement or String
|
||||
container = this.config.controls({
|
||||
id: this.id,
|
||||
seektime: this.config.seekTime,
|
||||
title: this.config.title,
|
||||
});
|
||||
container = this.config.controls.call(this, props);
|
||||
} else {
|
||||
// Create controls
|
||||
container = controls.create.call(this, {
|
||||
@@ -1211,10 +1414,34 @@ const controls = {
|
||||
seektime: this.config.seekTime,
|
||||
speed: this.speed,
|
||||
quality: this.quality,
|
||||
captions: controls.getLanguage.call(this),
|
||||
captions: captions.getLabel.call(this),
|
||||
// TODO: Looping
|
||||
// loop: 'None',
|
||||
});
|
||||
update = false;
|
||||
}
|
||||
|
||||
// Replace props with their value
|
||||
const replace = input => {
|
||||
let result = input;
|
||||
|
||||
Object.entries(props).forEach(([
|
||||
key,
|
||||
value,
|
||||
]) => {
|
||||
result = utils.replaceAll(result, `{${key}}`, value);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Update markup
|
||||
if (update) {
|
||||
if (utils.is.string(this.config.controls)) {
|
||||
container = replace(container);
|
||||
} else if (utils.is.element(container)) {
|
||||
container.innerHTML = replace(container.innerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
// Controls container
|
||||
@@ -1233,13 +1460,13 @@ const controls = {
|
||||
// Inject controls HTML
|
||||
if (utils.is.element(container)) {
|
||||
target.appendChild(container);
|
||||
} else {
|
||||
} else if (container) {
|
||||
target.insertAdjacentHTML('beforeend', container);
|
||||
}
|
||||
|
||||
// Find the elements if need be
|
||||
if (!utils.is.element(this.elements.controls)) {
|
||||
utils.findElements.call(this);
|
||||
controls.findElements.call(this);
|
||||
}
|
||||
|
||||
// Edge sometimes doesn't finish the paint so force a redraw
|
||||
|
||||
+23
-15
@@ -47,8 +47,8 @@ const defaults = {
|
||||
// Auto hide the controls
|
||||
hideControls: true,
|
||||
|
||||
// Revert to poster on finish (HTML5 - will cause reload)
|
||||
showPosterOnEnd: false,
|
||||
// Reset to start when playback ended
|
||||
resetOnEnd: false,
|
||||
|
||||
// Disable the standard context menu
|
||||
disableContextMenu: true,
|
||||
@@ -56,7 +56,7 @@ const defaults = {
|
||||
// Sprite (for icons)
|
||||
loadSprite: true,
|
||||
iconPrefix: 'plyr',
|
||||
iconUrl: 'https://cdn.plyr.io/3.1.0/plyr.svg',
|
||||
iconUrl: 'https://cdn.plyr.io/3.3.9/plyr.svg',
|
||||
|
||||
// Blank video (used to prevent errors on source change)
|
||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||
@@ -115,7 +115,7 @@ const defaults = {
|
||||
// Captions settings
|
||||
captions: {
|
||||
active: false,
|
||||
language: window.navigator.language.split('-')[0],
|
||||
language: (navigator.language || navigator.userLanguage).split('-')[0],
|
||||
},
|
||||
|
||||
// Fullscreen settings
|
||||
@@ -157,10 +157,10 @@ const defaults = {
|
||||
// Localisation
|
||||
i18n: {
|
||||
restart: 'Restart',
|
||||
rewind: 'Rewind {seektime} secs',
|
||||
rewind: 'Rewind {seektime}s',
|
||||
play: 'Play',
|
||||
pause: 'Pause',
|
||||
fastForward: 'Forward {seektime} secs',
|
||||
fastForward: 'Forward {seektime}s',
|
||||
seek: 'Seek',
|
||||
played: 'Played',
|
||||
buffered: 'Buffered',
|
||||
@@ -177,6 +177,7 @@ const defaults = {
|
||||
captions: 'Captions',
|
||||
settings: 'Settings',
|
||||
speed: 'Speed',
|
||||
normal: 'Normal',
|
||||
quality: 'Quality',
|
||||
loop: 'Loop',
|
||||
start: 'Start',
|
||||
@@ -184,19 +185,23 @@ const defaults = {
|
||||
all: 'All',
|
||||
reset: 'Reset',
|
||||
disabled: 'Disabled',
|
||||
enabled: 'Enabled',
|
||||
advertisement: 'Ad',
|
||||
},
|
||||
|
||||
// URLs
|
||||
urls: {
|
||||
vimeo: {
|
||||
api: 'https://player.vimeo.com/api/player.js',
|
||||
sdk: 'https://player.vimeo.com/api/player.js',
|
||||
iframe: 'https://player.vimeo.com/video/{0}?{1}',
|
||||
api: 'https://vimeo.com/api/v2/video/{0}.json',
|
||||
},
|
||||
youtube: {
|
||||
api: 'https://www.youtube.com/iframe_api',
|
||||
sdk: 'https://www.youtube.com/iframe_api',
|
||||
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
|
||||
},
|
||||
googleIMA: {
|
||||
api: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
||||
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -320,16 +325,19 @@ const defaults = {
|
||||
|
||||
// Class hooks added to the player in different states
|
||||
classNames: {
|
||||
video: 'plyr__video-wrapper',
|
||||
embed: 'plyr__video-embed',
|
||||
ads: 'plyr__ads',
|
||||
control: 'plyr__control',
|
||||
type: 'plyr--{0}',
|
||||
provider: 'plyr--{0}',
|
||||
stopped: 'plyr--stopped',
|
||||
video: 'plyr__video-wrapper',
|
||||
embed: 'plyr__video-embed',
|
||||
embedContainer: 'plyr__video-embed__container',
|
||||
poster: 'plyr__poster',
|
||||
posterEnabled: 'plyr__poster-enabled',
|
||||
ads: 'plyr__ads',
|
||||
control: 'plyr__control',
|
||||
playing: 'plyr--playing',
|
||||
paused: 'plyr--paused',
|
||||
stopped: 'plyr--stopped',
|
||||
loading: 'plyr--loading',
|
||||
error: 'plyr--has-error',
|
||||
hover: 'plyr--hover',
|
||||
tooltip: 'plyr__tooltip',
|
||||
cues: 'plyr__cues',
|
||||
|
||||
@@ -19,7 +19,7 @@ function onChange() {
|
||||
}
|
||||
|
||||
// Trigger an event
|
||||
utils.dispatchEvent(this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
||||
utils.dispatchEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
||||
|
||||
// Trap focus in container
|
||||
if (!browser.isIos) {
|
||||
@@ -55,7 +55,7 @@ class Fullscreen {
|
||||
|
||||
// Get prefix
|
||||
this.prefix = Fullscreen.prefix;
|
||||
this.name = Fullscreen.name;
|
||||
this.property = Fullscreen.property;
|
||||
|
||||
// Scroll position
|
||||
this.scrollPosition = { x: 0, y: 0 };
|
||||
@@ -70,7 +70,7 @@ class Fullscreen {
|
||||
// Fullscreen toggle on double click
|
||||
utils.on(this.player.elements.container, 'dblclick', event => {
|
||||
// Ignore double click in controls
|
||||
if (this.player.elements.controls.contains(event.target)) {
|
||||
if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class Fullscreen {
|
||||
static get prefix() {
|
||||
// No prefix
|
||||
if (utils.is.function(document.exitFullscreen)) {
|
||||
return false;
|
||||
return '';
|
||||
}
|
||||
|
||||
// Check for fullscreen support by vendor prefix
|
||||
@@ -113,7 +113,7 @@ class Fullscreen {
|
||||
return value;
|
||||
}
|
||||
|
||||
static get name() {
|
||||
static get property() {
|
||||
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ class Fullscreen {
|
||||
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
|
||||
}
|
||||
|
||||
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.name}Element`];
|
||||
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
|
||||
|
||||
return element === this.target;
|
||||
}
|
||||
@@ -176,7 +176,7 @@ class Fullscreen {
|
||||
} else if (!this.prefix) {
|
||||
this.target.requestFullscreen();
|
||||
} else if (!utils.is.empty(this.prefix)) {
|
||||
this.target[`${this.prefix}Request${this.name}`]();
|
||||
this.target[`${this.prefix}Request${this.property}`]();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,10 +193,10 @@ class Fullscreen {
|
||||
} else if (!Fullscreen.native) {
|
||||
toggleFallback.call(this, false);
|
||||
} else if (!this.prefix) {
|
||||
document.cancelFullScreen();
|
||||
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
||||
} else if (!utils.is.empty(this.prefix)) {
|
||||
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
|
||||
document[`${this.prefix}${action}${this.name}`]();
|
||||
document[`${this.prefix}${action}${this.property}`]();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+135
-43
@@ -2,10 +2,9 @@
|
||||
// Plyr Event Listeners
|
||||
// ==========================================================================
|
||||
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
import controls from './controls';
|
||||
import ui from './ui';
|
||||
import utils from './utils';
|
||||
|
||||
// Sniff out the browser
|
||||
const browser = utils.getBrowser();
|
||||
@@ -239,22 +238,45 @@ class Listeners {
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Toggle controls visibility based on mouse movement
|
||||
if (this.player.config.hideControls) {
|
||||
// Toggle controls on mouse events and entering fullscreen
|
||||
utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => {
|
||||
this.player.toggleControls(event);
|
||||
});
|
||||
}
|
||||
// Toggle controls on mouse events and entering fullscreen
|
||||
utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {
|
||||
const { controls } = this.player.elements;
|
||||
|
||||
// Remove button states for fullscreen
|
||||
if (event.type === 'enterfullscreen') {
|
||||
controls.pressed = false;
|
||||
controls.hover = false;
|
||||
}
|
||||
|
||||
// Show, then hide after a timeout unless another control event occurs
|
||||
const show = [
|
||||
'touchstart',
|
||||
'touchmove',
|
||||
'mousemove',
|
||||
].includes(event.type);
|
||||
|
||||
let delay = 0;
|
||||
|
||||
if (show) {
|
||||
ui.toggleControls.call(this.player, true);
|
||||
// Use longer timeout for touch devices
|
||||
delay = this.player.touch ? 3000 : 2000;
|
||||
}
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(this.player.timers.controls);
|
||||
// Timer to prevent flicker when seeking
|
||||
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for media events
|
||||
media() {
|
||||
// Time change on media
|
||||
utils.on(this.player.media, 'timeupdate seeking', event => ui.timeUpdate.call(this.player, event));
|
||||
utils.on(this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event));
|
||||
|
||||
// Display duration
|
||||
utils.on(this.player.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this.player, event));
|
||||
utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event));
|
||||
|
||||
// Check for audio tracks on load
|
||||
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
|
||||
@@ -266,33 +288,31 @@ class Listeners {
|
||||
// Handle the media finishing
|
||||
utils.on(this.player.media, 'ended', () => {
|
||||
// Show poster on end
|
||||
if (this.player.isHTML5 && this.player.isVideo && this.player.config.showPosterOnEnd) {
|
||||
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
|
||||
// Restart
|
||||
this.player.restart();
|
||||
|
||||
// Re-load media
|
||||
this.player.media.load();
|
||||
}
|
||||
});
|
||||
|
||||
// Check for buffer progress
|
||||
utils.on(this.player.media, 'progress playing', event => ui.updateProgress.call(this.player, event));
|
||||
utils.on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event));
|
||||
|
||||
// Handle volume changes
|
||||
utils.on(this.player.media, 'volumechange', event => ui.updateVolume.call(this.player, event));
|
||||
utils.on(this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event));
|
||||
|
||||
// Handle play/pause
|
||||
utils.on(this.player.media, 'playing play pause ended emptied', event => ui.checkPlaying.call(this.player, event));
|
||||
utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event));
|
||||
|
||||
// Loading state
|
||||
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
|
||||
|
||||
// Check if media failed to load
|
||||
// utils.on(this.player.media, 'play', event => ui.checkFailed.call(this.player, event));
|
||||
|
||||
// If autoplay, then load advertisement if required
|
||||
// TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
|
||||
utils.on(this.player.media, 'playing', () => {
|
||||
if (!this.player.ads) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If ads are enabled, wait for them first
|
||||
if (this.player.ads.enabled && !this.player.ads.initialized) {
|
||||
// Wait for manager response
|
||||
@@ -331,7 +351,7 @@ class Listeners {
|
||||
// Disable right click
|
||||
if (this.player.supported.ui && this.player.config.disableContextMenu) {
|
||||
utils.on(
|
||||
this.player.media,
|
||||
this.player.elements.wrapper,
|
||||
'contextmenu',
|
||||
event => {
|
||||
event.preventDefault();
|
||||
@@ -489,12 +509,19 @@ class Listeners {
|
||||
on(this.player.elements.settings.form, 'click', event => {
|
||||
event.stopPropagation();
|
||||
|
||||
// Go back to home tab on click
|
||||
const showHomeTab = () => {
|
||||
const id = `plyr-settings-${this.player.id}-home`;
|
||||
controls.showTab.call(this.player, id);
|
||||
};
|
||||
|
||||
// Settings menu items - use event delegation as items are added/removed
|
||||
if (utils.matches(event.target, this.player.config.selectors.inputs.language)) {
|
||||
proxy(
|
||||
event,
|
||||
() => {
|
||||
this.player.language = event.target.value;
|
||||
showHomeTab();
|
||||
},
|
||||
'language',
|
||||
);
|
||||
@@ -503,6 +530,7 @@ class Listeners {
|
||||
event,
|
||||
() => {
|
||||
this.player.quality = event.target.value;
|
||||
showHomeTab();
|
||||
},
|
||||
'quality',
|
||||
);
|
||||
@@ -511,11 +539,44 @@ class Listeners {
|
||||
event,
|
||||
() => {
|
||||
this.player.speed = parseFloat(event.target.value);
|
||||
showHomeTab();
|
||||
},
|
||||
'speed',
|
||||
);
|
||||
} else {
|
||||
controls.showTab.call(this.player, event);
|
||||
const tab = event.target;
|
||||
controls.showTab.call(this.player, tab.getAttribute('aria-controls'));
|
||||
}
|
||||
});
|
||||
|
||||
// Set range input alternative "value", which matches the tooltip time (#954)
|
||||
on(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
|
||||
const clientRect = this.player.elements.progress.getBoundingClientRect();
|
||||
const percent = 100 / clientRect.width * (event.pageX - clientRect.left);
|
||||
event.currentTarget.setAttribute('seek-value', percent);
|
||||
});
|
||||
|
||||
// Pause while seeking
|
||||
on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
|
||||
const seek = event.currentTarget;
|
||||
|
||||
// Was playing before?
|
||||
const play = seek.hasAttribute('play-on-seeked');
|
||||
|
||||
// Done seeking
|
||||
const done = [
|
||||
'mouseup',
|
||||
'touchend',
|
||||
'keyup',
|
||||
].includes(event.type);
|
||||
|
||||
// If we're done seeking and it was playing, resume playback
|
||||
if (play && done) {
|
||||
seek.removeAttribute('play-on-seeked');
|
||||
this.player.play();
|
||||
} else if (!done && this.player.playing) {
|
||||
seek.setAttribute('play-on-seeked', '');
|
||||
this.player.pause();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -524,7 +585,18 @@ class Listeners {
|
||||
this.player.elements.inputs.seek,
|
||||
inputEvent,
|
||||
event => {
|
||||
this.player.currentTime = event.target.value / event.target.max * this.player.duration;
|
||||
const seek = event.currentTarget;
|
||||
|
||||
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
|
||||
let seekTo = seek.getAttribute('seek-value');
|
||||
|
||||
if (utils.is.empty(seekTo)) {
|
||||
seekTo = seek.value;
|
||||
}
|
||||
|
||||
seek.removeAttribute('seek-value');
|
||||
|
||||
this.player.currentTime = seekTo / seek.max * this.player.duration;
|
||||
},
|
||||
'seek',
|
||||
);
|
||||
@@ -539,7 +611,8 @@ class Listeners {
|
||||
}
|
||||
|
||||
this.player.config.invertTime = !this.player.config.invertTime;
|
||||
ui.timeUpdate.call(this.player);
|
||||
|
||||
controls.timeUpdate.call(this.player);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -563,26 +636,45 @@ class Listeners {
|
||||
// Seek tooltip
|
||||
on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event));
|
||||
|
||||
// Toggle controls visibility based on mouse movement
|
||||
if (this.player.config.hideControls) {
|
||||
// Watch for cursor over controls so they don't hide when trying to interact
|
||||
on(this.player.elements.controls, 'mouseenter mouseleave', event => {
|
||||
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
|
||||
});
|
||||
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
|
||||
on(this.player.elements.controls, 'mouseenter mouseleave', event => {
|
||||
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
|
||||
});
|
||||
|
||||
// Watch for cursor over controls so they don't hide when trying to interact
|
||||
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
||||
this.player.elements.controls.pressed = [
|
||||
'mousedown',
|
||||
'touchstart',
|
||||
].includes(event.type);
|
||||
});
|
||||
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
|
||||
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
||||
this.player.elements.controls.pressed = [
|
||||
'mousedown',
|
||||
'touchstart',
|
||||
].includes(event.type);
|
||||
});
|
||||
|
||||
// Focus in/out on controls
|
||||
on(this.player.elements.controls, 'focusin focusout', event => {
|
||||
this.player.toggleControls(event);
|
||||
});
|
||||
}
|
||||
// Focus in/out on controls
|
||||
on(this.player.elements.controls, 'focusin focusout', event => {
|
||||
const { config, elements, timers } = this.player;
|
||||
|
||||
// Skip transition to prevent focus from scrolling the parent element
|
||||
utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
|
||||
|
||||
// Toggle
|
||||
ui.toggleControls.call(this.player, event.type === 'focusin');
|
||||
|
||||
// If focusin, hide again after delay
|
||||
if (event.type === 'focusin') {
|
||||
// Restore transition
|
||||
setTimeout(() => {
|
||||
utils.toggleClass(elements.controls, config.classNames.noTransition, false);
|
||||
}, 0);
|
||||
|
||||
// Delay a little more for keyboard users
|
||||
const delay = this.touch ? 3000 : 4000;
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(timers.controls);
|
||||
// Hide
|
||||
timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse wheel for volume
|
||||
on(
|
||||
|
||||
+10
-27
@@ -2,15 +2,10 @@
|
||||
// Plyr Media
|
||||
// ==========================================================================
|
||||
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
import youtube from './plugins/youtube';
|
||||
import vimeo from './plugins/vimeo';
|
||||
import html5 from './html5';
|
||||
import ui from './ui';
|
||||
|
||||
// Sniff out the browser
|
||||
const browser = utils.getBrowser();
|
||||
import vimeo from './plugins/vimeo';
|
||||
import youtube from './plugins/youtube';
|
||||
import utils from './utils';
|
||||
|
||||
const media = {
|
||||
// Setup media
|
||||
@@ -33,23 +28,6 @@ const media = {
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
|
||||
}
|
||||
|
||||
if (this.supported.ui) {
|
||||
// Check for picture-in-picture support
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
|
||||
|
||||
// Check for airplay support
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
||||
|
||||
// If there's no autoplay attribute, assume the video is stopped and add state class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.config.autoplay);
|
||||
|
||||
// Add iOS class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
||||
|
||||
// Add touch class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
||||
}
|
||||
|
||||
// Inject the player wrapper
|
||||
if (this.isVideo) {
|
||||
// Create the wrapper div
|
||||
@@ -59,6 +37,13 @@ const media = {
|
||||
|
||||
// Wrap the video in a container
|
||||
utils.wrap(this.media, this.elements.wrapper);
|
||||
|
||||
// Faux poster container
|
||||
this.elements.poster = utils.createElement('div', {
|
||||
class: this.config.classNames.poster,
|
||||
});
|
||||
|
||||
this.elements.wrapper.appendChild(this.elements.poster);
|
||||
}
|
||||
|
||||
if (this.isEmbed) {
|
||||
@@ -75,8 +60,6 @@ const media = {
|
||||
break;
|
||||
}
|
||||
} else if (this.isHTML5) {
|
||||
ui.setTitle.call(this);
|
||||
|
||||
html5.extend.call(this);
|
||||
}
|
||||
},
|
||||
|
||||
+10
-4
@@ -6,8 +6,8 @@
|
||||
|
||||
/* global google */
|
||||
|
||||
import utils from '../utils';
|
||||
import i18n from '../i18n';
|
||||
import utils from '../utils';
|
||||
|
||||
class Ads {
|
||||
/**
|
||||
@@ -18,7 +18,6 @@ class Ads {
|
||||
constructor(player) {
|
||||
this.player = player;
|
||||
this.publisherId = player.config.ads.publisherId;
|
||||
this.enabled = player.isHTML5 && player.isVideo && player.config.ads.enabled && utils.is.string(this.publisherId) && this.publisherId.length;
|
||||
this.playing = false;
|
||||
this.initialized = false;
|
||||
this.elements = {
|
||||
@@ -44,6 +43,10 @@ class Ads {
|
||||
this.load();
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the IMA SDK
|
||||
*/
|
||||
@@ -52,7 +55,7 @@ class Ads {
|
||||
// Check if the Google IMA3 SDK is loaded or load it ourselves
|
||||
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
|
||||
utils
|
||||
.loadScript(this.player.config.urls.googleIMA.api)
|
||||
.loadScript(this.player.config.urls.googleIMA.sdk)
|
||||
.then(() => {
|
||||
this.ready();
|
||||
})
|
||||
@@ -160,6 +163,9 @@ class Ads {
|
||||
// We only overlay ads as we only support video.
|
||||
request.forceNonLinearFullSlot = false;
|
||||
|
||||
// Mute based on current state
|
||||
request.setAdWillPlayMuted(!this.player.muted);
|
||||
|
||||
this.loader.requestAds(request);
|
||||
} catch (e) {
|
||||
this.onAdError(e);
|
||||
@@ -226,7 +232,7 @@ class Ads {
|
||||
|
||||
// Get skippable state
|
||||
// TODO: Skip button
|
||||
// this.manager.getAdSkippableState();
|
||||
// this.player.debug.warn(this.manager.getAdSkippableState());
|
||||
|
||||
// Set volume to match player
|
||||
this.manager.setVolume(this.player.volume);
|
||||
|
||||
+79
-37
@@ -2,10 +2,18 @@
|
||||
// Vimeo plugin
|
||||
// ==========================================================================
|
||||
|
||||
import utils from './../utils';
|
||||
import captions from './../captions';
|
||||
import controls from './../controls';
|
||||
import ui from './../ui';
|
||||
import utils from './../utils';
|
||||
|
||||
// Set playback state and trigger change (only on actual change)
|
||||
function assurePlaybackState(play) {
|
||||
if (this.media.paused === play) {
|
||||
this.media.paused = !play;
|
||||
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||
}
|
||||
}
|
||||
|
||||
const vimeo = {
|
||||
setup() {
|
||||
@@ -18,7 +26,7 @@ const vimeo = {
|
||||
// Load the API if not already
|
||||
if (!utils.is.object(window.Vimeo)) {
|
||||
utils
|
||||
.loadScript(this.config.urls.vimeo.api)
|
||||
.loadScript(this.config.urls.vimeo.sdk)
|
||||
.then(() => {
|
||||
vimeo.ready.call(this);
|
||||
})
|
||||
@@ -35,10 +43,14 @@ const vimeo = {
|
||||
setAspectRatio(input) {
|
||||
const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':');
|
||||
const padding = 100 / ratio[0] * ratio[1];
|
||||
const height = 240;
|
||||
const offset = (height - padding) / (height / 50);
|
||||
this.elements.wrapper.style.paddingBottom = `${padding}%`;
|
||||
this.media.style.transform = `translateY(-${offset}%)`;
|
||||
|
||||
if (this.supported.ui) {
|
||||
const height = 240;
|
||||
const offset = (height - padding) / (height / 50);
|
||||
|
||||
this.media.style.transform = `translateY(-${offset}%)`;
|
||||
}
|
||||
},
|
||||
|
||||
// API Ready
|
||||
@@ -49,12 +61,14 @@ const vimeo = {
|
||||
const options = {
|
||||
loop: player.config.loop.active,
|
||||
autoplay: player.autoplay,
|
||||
// muted: player.muted,
|
||||
byline: false,
|
||||
portrait: false,
|
||||
title: false,
|
||||
speed: true,
|
||||
transparent: 0,
|
||||
gesture: 'media',
|
||||
playsinline: !this.config.fullscreen.iosNative,
|
||||
};
|
||||
const params = utils.buildUrlParams(options);
|
||||
|
||||
@@ -63,42 +77,64 @@ const vimeo = {
|
||||
|
||||
// Get from <div> if needed
|
||||
if (utils.is.empty(source)) {
|
||||
source = player.media.getAttribute(this.config.attributes.embed.id);
|
||||
source = player.media.getAttribute(player.config.attributes.embed.id);
|
||||
}
|
||||
|
||||
const id = utils.parseVimeoId(source);
|
||||
|
||||
// Build an iframe
|
||||
const iframe = utils.createElement('iframe');
|
||||
const src = `https://player.vimeo.com/video/${id}?${params}`;
|
||||
const src = utils.format(player.config.urls.vimeo.iframe, id, params);
|
||||
iframe.setAttribute('src', src);
|
||||
iframe.setAttribute('allowfullscreen', '');
|
||||
iframe.setAttribute('allowtransparency', '');
|
||||
iframe.setAttribute('allow', 'autoplay');
|
||||
|
||||
// Inject the package
|
||||
const wrapper = utils.createElement('div');
|
||||
const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer });
|
||||
wrapper.appendChild(iframe);
|
||||
player.media = utils.replaceElement(wrapper, player.media);
|
||||
|
||||
// Get poster image
|
||||
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => {
|
||||
if (utils.is.empty(response)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the URL for thumbnail
|
||||
const url = new URL(response[0].thumbnail_large);
|
||||
|
||||
// Get original image
|
||||
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
|
||||
|
||||
// Set and show poster
|
||||
ui.setPoster.call(player, url.href);
|
||||
});
|
||||
|
||||
// Setup instance
|
||||
// https://github.com/vimeo/player.js
|
||||
player.embed = new window.Vimeo.Player(iframe);
|
||||
player.embed = new window.Vimeo.Player(iframe, {
|
||||
autopause: player.config.autopause,
|
||||
muted: player.muted,
|
||||
});
|
||||
|
||||
player.media.paused = true;
|
||||
player.media.currentTime = 0;
|
||||
|
||||
// Disable native text track rendering
|
||||
if (player.supported.ui) {
|
||||
player.embed.disableTextTrack();
|
||||
}
|
||||
|
||||
// Create a faux HTML5 API using the Vimeo API
|
||||
player.media.play = () => {
|
||||
player.embed.play().then(() => {
|
||||
player.media.paused = false;
|
||||
});
|
||||
assurePlaybackState.call(player, true);
|
||||
return player.embed.play();
|
||||
};
|
||||
|
||||
player.media.pause = () => {
|
||||
player.embed.pause().then(() => {
|
||||
player.media.paused = true;
|
||||
});
|
||||
assurePlaybackState.call(player, false);
|
||||
return player.embed.pause();
|
||||
};
|
||||
|
||||
player.media.stop = () => {
|
||||
@@ -113,23 +149,26 @@ const vimeo = {
|
||||
return currentTime;
|
||||
},
|
||||
set(time) {
|
||||
// Get current paused state
|
||||
// Vimeo will automatically play on seek
|
||||
const { paused } = player.media;
|
||||
// Vimeo will automatically play on seek if the video hasn't been played before
|
||||
|
||||
// Set seeking flag
|
||||
player.media.seeking = true;
|
||||
// Get current paused state and volume etc
|
||||
const { embed, media, paused, volume } = player;
|
||||
|
||||
// Trigger seeking
|
||||
utils.dispatchEvent.call(player, player.media, 'seeking');
|
||||
// Set seeking state and trigger event
|
||||
media.seeking = true;
|
||||
utils.dispatchEvent.call(player, media, 'seeking');
|
||||
|
||||
// Seek after events
|
||||
player.embed.setCurrentTime(time);
|
||||
|
||||
// Restore pause state
|
||||
if (paused) {
|
||||
player.pause();
|
||||
}
|
||||
// If paused, mute until seek is complete
|
||||
Promise.resolve(paused && embed.setVolume(0))
|
||||
// Seek
|
||||
.then(() => embed.setCurrentTime(time))
|
||||
// Restore paused
|
||||
.then(() => paused && embed.pause())
|
||||
// Restore volume
|
||||
.then(() => paused && embed.setVolume(volume))
|
||||
.catch(() => {
|
||||
// Do nothing
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -283,17 +322,12 @@ const vimeo = {
|
||||
});
|
||||
|
||||
player.embed.on('play', () => {
|
||||
// Only fire play if paused before
|
||||
if (player.media.paused) {
|
||||
utils.dispatchEvent.call(player, player.media, 'play');
|
||||
}
|
||||
player.media.paused = false;
|
||||
assurePlaybackState.call(player, true);
|
||||
utils.dispatchEvent.call(player, player.media, 'playing');
|
||||
});
|
||||
|
||||
player.embed.on('pause', () => {
|
||||
player.media.paused = true;
|
||||
utils.dispatchEvent.call(player, player.media, 'pause');
|
||||
assurePlaybackState.call(player, false);
|
||||
});
|
||||
|
||||
player.embed.on('timeupdate', data => {
|
||||
@@ -310,12 +344,20 @@ const vimeo = {
|
||||
if (parseInt(data.percent, 10) === 1) {
|
||||
utils.dispatchEvent.call(player, player.media, 'canplaythrough');
|
||||
}
|
||||
|
||||
// Get duration as if we do it before load, it gives an incorrect value
|
||||
// https://github.com/sampotts/plyr/issues/891
|
||||
player.embed.getDuration().then(value => {
|
||||
if (value !== player.media.duration) {
|
||||
player.media.duration = value;
|
||||
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
player.embed.on('seeked', () => {
|
||||
player.media.seeking = false;
|
||||
utils.dispatchEvent.call(player, player.media, 'seeked');
|
||||
utils.dispatchEvent.call(player, player.media, 'play');
|
||||
});
|
||||
|
||||
player.embed.on('ended', () => {
|
||||
|
||||
+73
-37
@@ -2,9 +2,9 @@
|
||||
// YouTube plugin
|
||||
// ==========================================================================
|
||||
|
||||
import utils from './../utils';
|
||||
import controls from './../controls';
|
||||
import ui from './../ui';
|
||||
import utils from './../utils';
|
||||
|
||||
// Standardise YouTube quality unit
|
||||
function mapQualityUnit(input) {
|
||||
@@ -64,6 +64,14 @@ function mapQualityUnits(levels) {
|
||||
return utils.dedupe(levels.map(level => mapQualityUnit(level)));
|
||||
}
|
||||
|
||||
// Set playback state and trigger change (only on actual change)
|
||||
function assurePlaybackState(play) {
|
||||
if (this.media.paused === play) {
|
||||
this.media.paused = !play;
|
||||
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||
}
|
||||
}
|
||||
|
||||
const youtube = {
|
||||
setup() {
|
||||
// Add embed class for responsive
|
||||
@@ -77,7 +85,7 @@ const youtube = {
|
||||
youtube.ready.call(this);
|
||||
} else {
|
||||
// Load the API
|
||||
utils.loadScript(this.config.urls.youtube.api).catch(error => {
|
||||
utils.loadScript(this.config.urls.youtube.sdk).catch(error => {
|
||||
this.debug.warn('YouTube API failed to load', error);
|
||||
});
|
||||
|
||||
@@ -117,7 +125,7 @@ const youtube = {
|
||||
// Or via Google API
|
||||
const key = this.config.keys.google;
|
||||
if (utils.is.string(key) && !utils.is.empty(key)) {
|
||||
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`;
|
||||
const url = utils.format(this.config.urls.youtube.api, videoId, key);
|
||||
|
||||
utils
|
||||
.fetch(url)
|
||||
@@ -161,6 +169,21 @@ const youtube = {
|
||||
const container = utils.createElement('div', { id });
|
||||
player.media = utils.replaceElement(container, player.media);
|
||||
|
||||
// Set poster image
|
||||
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
|
||||
|
||||
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
|
||||
utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
|
||||
.catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
|
||||
.catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
|
||||
.then(image => ui.setPoster.call(player, image.src))
|
||||
.then(posterSrc => {
|
||||
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
|
||||
if (!posterSrc.includes('maxres')) {
|
||||
player.elements.poster.style.backgroundSize = 'cover';
|
||||
}
|
||||
});
|
||||
|
||||
// Setup instance
|
||||
// https://developers.google.com/youtube/iframe_api_reference
|
||||
player.embed = new window.YT.Player(id, {
|
||||
@@ -249,10 +272,12 @@ const youtube = {
|
||||
|
||||
// Create a faux HTML5 API using the YouTube API
|
||||
player.media.play = () => {
|
||||
assurePlaybackState.call(player, true);
|
||||
instance.playVideo();
|
||||
};
|
||||
|
||||
player.media.pause = () => {
|
||||
assurePlaybackState.call(player, false);
|
||||
instance.pauseVideo();
|
||||
};
|
||||
|
||||
@@ -270,10 +295,13 @@ const youtube = {
|
||||
return Number(instance.getCurrentTime());
|
||||
},
|
||||
set(time) {
|
||||
// Set seeking flag
|
||||
player.media.seeking = true;
|
||||
// If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
|
||||
if (player.paused) {
|
||||
player.embed.mute();
|
||||
}
|
||||
|
||||
// Trigger seeking
|
||||
// Set seeking state and trigger event
|
||||
player.media.seeking = true;
|
||||
utils.dispatchEvent.call(player, player.media, 'seeking');
|
||||
|
||||
// Seek after events sent
|
||||
@@ -396,6 +424,17 @@ const youtube = {
|
||||
// Reset timer
|
||||
clearInterval(player.timers.playing);
|
||||
|
||||
const seeked = player.media.seeking && [
|
||||
1,
|
||||
2,
|
||||
].includes(event.data);
|
||||
|
||||
if (seeked) {
|
||||
// Unset seeking and fire seeked event
|
||||
player.media.seeking = false;
|
||||
utils.dispatchEvent.call(player, player.media, 'seeked');
|
||||
}
|
||||
|
||||
// Handle events
|
||||
// -1 Unstarted
|
||||
// 0 Ended
|
||||
@@ -415,7 +454,7 @@ const youtube = {
|
||||
break;
|
||||
|
||||
case 0:
|
||||
player.media.paused = true;
|
||||
assurePlaybackState.call(player, false);
|
||||
|
||||
// YouTube doesn't support loop for a single video, so mimick it.
|
||||
if (player.media.loop) {
|
||||
@@ -429,42 +468,39 @@ const youtube = {
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// If we were seeking, fire seeked event
|
||||
if (player.media.seeking) {
|
||||
utils.dispatchEvent.call(player, player.media, 'seeked');
|
||||
}
|
||||
player.media.seeking = false;
|
||||
|
||||
// Only fire play if paused before
|
||||
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
|
||||
if (player.media.paused) {
|
||||
utils.dispatchEvent.call(player, player.media, 'play');
|
||||
player.media.pause();
|
||||
} else {
|
||||
assurePlaybackState.call(player, true);
|
||||
|
||||
utils.dispatchEvent.call(player, player.media, 'playing');
|
||||
|
||||
// Poll to get playback progress
|
||||
player.timers.playing = setInterval(() => {
|
||||
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
||||
}, 50);
|
||||
|
||||
// Check duration again due to YouTube bug
|
||||
// https://github.com/sampotts/plyr/issues/374
|
||||
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
|
||||
if (player.media.duration !== instance.getDuration()) {
|
||||
player.media.duration = instance.getDuration();
|
||||
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
||||
}
|
||||
|
||||
// Get quality
|
||||
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
|
||||
}
|
||||
player.media.paused = false;
|
||||
|
||||
utils.dispatchEvent.call(player, player.media, 'playing');
|
||||
|
||||
// Poll to get playback progress
|
||||
player.timers.playing = setInterval(() => {
|
||||
utils.dispatchEvent.call(player, player.media, 'timeupdate');
|
||||
}, 50);
|
||||
|
||||
// Check duration again due to YouTube bug
|
||||
// https://github.com/sampotts/plyr/issues/374
|
||||
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
|
||||
if (player.media.duration !== instance.getDuration()) {
|
||||
player.media.duration = instance.getDuration();
|
||||
utils.dispatchEvent.call(player, player.media, 'durationchange');
|
||||
}
|
||||
|
||||
// Get quality
|
||||
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
|
||||
|
||||
break;
|
||||
|
||||
case 2:
|
||||
player.media.paused = true;
|
||||
|
||||
utils.dispatchEvent.call(player, player.media, 'pause');
|
||||
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
|
||||
if (!player.muted) {
|
||||
player.embed.unMute();
|
||||
}
|
||||
assurePlaybackState.call(player, false);
|
||||
|
||||
break;
|
||||
|
||||
|
||||
+114
-171
@@ -1,26 +1,24 @@
|
||||
// ==========================================================================
|
||||
// Plyr
|
||||
// plyr.js v3.1.0
|
||||
// plyr.js v3.3.9
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
|
||||
import { providers, types } from './types';
|
||||
import defaults from './defaults';
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
|
||||
import captions from './captions';
|
||||
import Console from './console';
|
||||
import controls from './controls';
|
||||
import defaults from './defaults';
|
||||
import Fullscreen from './fullscreen';
|
||||
import Listeners from './listeners';
|
||||
import Storage from './storage';
|
||||
import Ads from './plugins/ads';
|
||||
|
||||
import captions from './captions';
|
||||
import controls from './controls';
|
||||
import media from './media';
|
||||
import Ads from './plugins/ads';
|
||||
import source from './source';
|
||||
import Storage from './storage';
|
||||
import support from './support';
|
||||
import { providers, types } from './types';
|
||||
import ui from './ui';
|
||||
import utils from './utils';
|
||||
|
||||
// Private properties
|
||||
// TODO: Use a WeakMap for private globals
|
||||
@@ -57,7 +55,8 @@ class Plyr {
|
||||
this.config = utils.extend(
|
||||
{},
|
||||
defaults,
|
||||
options,
|
||||
Plyr.defaults,
|
||||
options || {},
|
||||
(() => {
|
||||
try {
|
||||
return JSON.parse(this.media.getAttribute('data-plyr-config'));
|
||||
@@ -97,6 +96,7 @@ class Plyr {
|
||||
this.options = {
|
||||
speed: [],
|
||||
quality: [],
|
||||
captions: [],
|
||||
};
|
||||
|
||||
// Debugging
|
||||
@@ -133,17 +133,9 @@ class Plyr {
|
||||
}
|
||||
|
||||
// Cache original element state for .destroy()
|
||||
// TODO: Investigate a better solution as I suspect this causes reported double load issues?
|
||||
setTimeout(() => {
|
||||
const clone = this.media.cloneNode(true);
|
||||
|
||||
// Prevent the clone autoplaying
|
||||
if (clone.getAttribute('autoplay')) {
|
||||
clone.pause();
|
||||
}
|
||||
|
||||
this.elements.original = clone;
|
||||
}, 0);
|
||||
const clone = this.media.cloneNode(true);
|
||||
clone.autoplay = false;
|
||||
this.elements.original = clone;
|
||||
|
||||
// Set media type based on tag or data attribute
|
||||
// Supported: video, audio, vimeo, youtube
|
||||
@@ -184,12 +176,17 @@ class Plyr {
|
||||
if (truthy.includes(params.autoplay)) {
|
||||
this.config.autoplay = true;
|
||||
}
|
||||
if (truthy.includes(params.playsinline)) {
|
||||
this.config.inline = true;
|
||||
}
|
||||
if (truthy.includes(params.loop)) {
|
||||
this.config.loop.active = true;
|
||||
}
|
||||
|
||||
// TODO: replace fullscreen.iosNative with this playsinline config option
|
||||
// YouTube requires the playsinline in the URL
|
||||
if (this.isYouTube) {
|
||||
this.config.playsinline = truthy.includes(params.playsinline);
|
||||
} else {
|
||||
this.config.playsinline = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// <div> with attributes
|
||||
@@ -223,7 +220,7 @@ class Plyr {
|
||||
this.config.autoplay = true;
|
||||
}
|
||||
if (this.media.hasAttribute('playsinline')) {
|
||||
this.config.inline = true;
|
||||
this.config.playsinline = true;
|
||||
}
|
||||
if (this.media.hasAttribute('muted')) {
|
||||
this.config.muted = true;
|
||||
@@ -240,7 +237,7 @@ class Plyr {
|
||||
}
|
||||
|
||||
// Check for support again but with type
|
||||
this.supported = support.check(this.type, this.provider, this.config.inline);
|
||||
this.supported = support.check(this.type, this.provider, this.config.playsinline);
|
||||
|
||||
// If no support for even API, bail
|
||||
if (!this.supported.api) {
|
||||
@@ -337,11 +334,6 @@ class Plyr {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If ads are enabled, wait for them first
|
||||
/* if (this.ads.enabled && !this.ads.initialized) {
|
||||
return this.ads.managerPromise.then(() => this.ads.play()).catch(() => this.media.play());
|
||||
} */
|
||||
|
||||
// Return the promise (for HTML5)
|
||||
return this.media.play();
|
||||
}
|
||||
@@ -357,6 +349,13 @@ class Plyr {
|
||||
this.media.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playing state
|
||||
*/
|
||||
get playing() {
|
||||
return Boolean(this.ready && !this.paused && !this.ended);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paused state
|
||||
*/
|
||||
@@ -365,10 +364,10 @@ class Plyr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playing state
|
||||
* Get stopped state
|
||||
*/
|
||||
get playing() {
|
||||
return Boolean(!this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true));
|
||||
get stopped() {
|
||||
return Boolean(this.paused && this.currentTime === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,7 +397,8 @@ class Plyr {
|
||||
*/
|
||||
stop() {
|
||||
if (this.isHTML5) {
|
||||
this.media.load();
|
||||
this.pause();
|
||||
this.restart();
|
||||
} else if (utils.is.function(this.media.stop)) {
|
||||
this.media.stop();
|
||||
}
|
||||
@@ -432,21 +432,16 @@ class Plyr {
|
||||
* @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
|
||||
*/
|
||||
set currentTime(input) {
|
||||
let targetTime = 0;
|
||||
|
||||
if (utils.is.number(input)) {
|
||||
targetTime = input;
|
||||
// Bail if media duration isn't available yet
|
||||
if (!this.duration) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalise targetTime
|
||||
if (targetTime < 0) {
|
||||
targetTime = 0;
|
||||
} else if (targetTime > this.duration) {
|
||||
targetTime = this.duration;
|
||||
}
|
||||
// Validate input
|
||||
const inputIsValid = utils.is.number(input) && input > 0;
|
||||
|
||||
// Set
|
||||
this.media.currentTime = parseFloat(targetTime.toFixed(4));
|
||||
this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
|
||||
|
||||
// Logging
|
||||
this.debug.log(`Seeking to ${this.currentTime} seconds`);
|
||||
@@ -492,13 +487,13 @@ class Plyr {
|
||||
*/
|
||||
get duration() {
|
||||
// Faux duration set via config
|
||||
const fauxDuration = parseInt(this.config.duration, 10);
|
||||
const fauxDuration = parseFloat(this.config.duration);
|
||||
|
||||
// True duration
|
||||
const realDuration = this.media ? Number(this.media.duration) : 0;
|
||||
// Media duration can be NaN before the media has loaded
|
||||
const duration = (this.media || {}).duration || 0;
|
||||
|
||||
// If custom duration is funky, use regular duration
|
||||
return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration;
|
||||
// If config duration is funky, use regular duration
|
||||
return fauxDuration || duration;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -793,25 +788,23 @@ class Plyr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the poster image for a HTML5 video
|
||||
* Set the poster image for a video
|
||||
* @param {input} - the URL for the new poster image
|
||||
*/
|
||||
set poster(input) {
|
||||
if (!this.isHTML5 || !this.isVideo) {
|
||||
this.debug.warn('Poster can only be set on HTML5 video');
|
||||
if (!this.isVideo) {
|
||||
this.debug.warn('Poster can only be set for video');
|
||||
return;
|
||||
}
|
||||
|
||||
if (utils.is.string(input)) {
|
||||
this.media.setAttribute('poster', input);
|
||||
}
|
||||
ui.setPoster.call(this, input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current poster image
|
||||
*/
|
||||
get poster() {
|
||||
if (!this.isHTML5 || !this.isVideo) {
|
||||
if (!this.isVideo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -839,13 +832,13 @@ class Plyr {
|
||||
* @param {boolean} input - Whether to enable captions
|
||||
*/
|
||||
toggleCaptions(input) {
|
||||
// If there's no full support, or there's no caption toggle
|
||||
if (!this.supported.ui || !utils.is.element(this.elements.buttons.captions)) {
|
||||
// If there's no full support
|
||||
if (!this.supported.ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the method is called without parameter, toggle based on current value
|
||||
const show = utils.is.boolean(input) ? input : this.elements.container.className.indexOf(this.config.classNames.captions.active) === -1;
|
||||
const show = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
|
||||
|
||||
// Nothing to change...
|
||||
if (this.captions.active === show) {
|
||||
@@ -875,17 +868,29 @@ class Plyr {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle captions based on input
|
||||
this.toggleCaptions(!utils.is.empty(input));
|
||||
|
||||
// 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;
|
||||
@@ -958,119 +963,32 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Toggle the player controls
|
||||
* @param {boolean} toggle - Whether to show the controls
|
||||
* @param {boolean} [toggle] - Whether to show the controls
|
||||
*/
|
||||
toggleControls(toggle) {
|
||||
// We need controls of course...
|
||||
if (!utils.is.element(this.elements.controls)) {
|
||||
return;
|
||||
}
|
||||
// Don't toggle if missing UI support or if it's audio
|
||||
if (this.supported.ui && !this.isAudio) {
|
||||
// Get state before change
|
||||
const isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
|
||||
|
||||
// Don't hide if no UI support or it's audio
|
||||
if (!this.supported.ui || this.isAudio) {
|
||||
return;
|
||||
}
|
||||
// Negate the argument if not undefined since adding the class to hides the controls
|
||||
const force = typeof toggle === 'undefined' ? undefined : !toggle;
|
||||
|
||||
let delay = 0;
|
||||
let show = toggle;
|
||||
let isEnterFullscreen = false;
|
||||
// Apply and get updated state
|
||||
const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
|
||||
|
||||
// Get toggle state if not set
|
||||
if (!utils.is.boolean(toggle)) {
|
||||
if (utils.is.event(toggle)) {
|
||||
// Is the enter fullscreen event
|
||||
isEnterFullscreen = toggle.type === 'enterfullscreen';
|
||||
|
||||
// Events that show the controls
|
||||
const showEvents = [
|
||||
'touchstart',
|
||||
'touchmove',
|
||||
'mouseenter',
|
||||
'mousemove',
|
||||
'focusin',
|
||||
];
|
||||
|
||||
// Events that delay hiding
|
||||
const delayEvents = [
|
||||
'touchmove',
|
||||
'touchend',
|
||||
'mousemove',
|
||||
];
|
||||
|
||||
// Whether to show controls
|
||||
show = showEvents.includes(toggle.type);
|
||||
|
||||
// Delay hiding on move events
|
||||
if (delayEvents.includes(toggle.type)) {
|
||||
delay = 2000;
|
||||
}
|
||||
|
||||
// Delay a little more for keyboard users
|
||||
if (!this.touch && toggle.type === 'focusin') {
|
||||
delay = 3000;
|
||||
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
|
||||
}
|
||||
} else {
|
||||
show = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
|
||||
// Close menu
|
||||
if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
||||
controls.toggleMenu.call(this, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear timer on every call
|
||||
clearTimeout(this.timers.controls);
|
||||
|
||||
// If the mouse is not over the controls, set a timeout to hide them
|
||||
if (show || this.paused || this.loading) {
|
||||
// Check if controls toggled
|
||||
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false);
|
||||
|
||||
// Trigger event
|
||||
if (toggled) {
|
||||
utils.dispatchEvent.call(this, this.media, 'controlsshown');
|
||||
}
|
||||
|
||||
// Always show controls when paused or if touch
|
||||
if (this.paused || this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delay for hiding on touch
|
||||
if (this.touch) {
|
||||
delay = 3000;
|
||||
// Trigger event on change
|
||||
if (hiding !== isHidden) {
|
||||
const eventName = hiding ? 'controlshidden' : 'controlsshown';
|
||||
utils.dispatchEvent.call(this, this.media, eventName);
|
||||
}
|
||||
return !hiding;
|
||||
}
|
||||
|
||||
// If toggle is false or if we're playing (regardless of toggle),
|
||||
// then set the timer to hide the controls
|
||||
if (!show || this.playing) {
|
||||
this.timers.controls = setTimeout(() => {
|
||||
// We need controls of course...
|
||||
if (!utils.is.element(this.elements.controls)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the mouse is over the controls (and not entering fullscreen), bail
|
||||
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore transition behaviour
|
||||
if (!utils.hasClass(this.elements.container, this.config.classNames.hideControls)) {
|
||||
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false);
|
||||
}
|
||||
|
||||
// Check if controls toggled
|
||||
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, true);
|
||||
|
||||
// Trigger event and close menu
|
||||
if (toggled) {
|
||||
utils.dispatchEvent.call(this, this.media, 'controlshidden');
|
||||
|
||||
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
||||
controls.toggleMenu.call(this, false);
|
||||
}
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1232,6 +1150,31 @@ class Plyr {
|
||||
static loadSprite(url, id) {
|
||||
return utils.loadSprite(url, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup multiple instances
|
||||
* @param {*} selector
|
||||
* @param {object} options
|
||||
*/
|
||||
static setup(selector, options = {}) {
|
||||
let targets = null;
|
||||
|
||||
if (utils.is.string(selector)) {
|
||||
targets = Array.from(document.querySelectorAll(selector));
|
||||
} else if (utils.is.nodeList(selector)) {
|
||||
targets = Array.from(selector);
|
||||
} else if (utils.is.array(selector)) {
|
||||
targets = selector.filter(i => utils.is.element(i));
|
||||
}
|
||||
|
||||
if (utils.is.empty(targets)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return targets.map(t => new Plyr(t, options));
|
||||
}
|
||||
}
|
||||
|
||||
Plyr.defaults = utils.cloneDeep(defaults);
|
||||
|
||||
export default Plyr;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
// ==========================================================================
|
||||
// Plyr Polyfilled Build
|
||||
// plyr.js v3.1.0
|
||||
// plyr.js v3.3.9
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
|
||||
import 'babel-polyfill';
|
||||
|
||||
import 'custom-event-polyfill';
|
||||
|
||||
import 'url-polyfill';
|
||||
import Plyr from './plyr';
|
||||
|
||||
export default Plyr;
|
||||
|
||||
+7
-7
@@ -2,12 +2,12 @@
|
||||
// Plyr source update
|
||||
// ==========================================================================
|
||||
|
||||
import { providers } from './types';
|
||||
import utils from './utils';
|
||||
import html5 from './html5';
|
||||
import media from './media';
|
||||
import ui from './ui';
|
||||
import support from './support';
|
||||
import { providers } from './types';
|
||||
import ui from './ui';
|
||||
import utils from './utils';
|
||||
|
||||
const source = {
|
||||
// Add elements to HTML5 media (source, tracks, etc)
|
||||
@@ -55,7 +55,7 @@ const source = {
|
||||
this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5;
|
||||
|
||||
// Check for support
|
||||
this.supported = support.check(this.type, this.provider, this.config.inline);
|
||||
this.supported = support.check(this.type, this.provider, this.config.playsinline);
|
||||
|
||||
// Create new markup
|
||||
switch (`${this.provider}:${this.type}`) {
|
||||
@@ -94,8 +94,8 @@ const source = {
|
||||
if (this.config.autoplay) {
|
||||
this.media.setAttribute('autoplay', '');
|
||||
}
|
||||
if ('poster' in input) {
|
||||
this.media.setAttribute('poster', input.poster);
|
||||
if (!utils.is.empty(input.poster)) {
|
||||
this.poster = input.poster;
|
||||
}
|
||||
if (this.config.loop.active) {
|
||||
this.media.setAttribute('loop', '');
|
||||
@@ -103,7 +103,7 @@ const source = {
|
||||
if (this.config.muted) {
|
||||
this.media.setAttribute('muted', '');
|
||||
}
|
||||
if (this.config.inline) {
|
||||
if (this.config.playsinline) {
|
||||
this.media.setAttribute('playsinline', '');
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ class Storage {
|
||||
}
|
||||
|
||||
get(key) {
|
||||
if (!Storage.supported) {
|
||||
if (!Storage.supported || !this.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -12,16 +12,16 @@ const support = {
|
||||
|
||||
// Check for support
|
||||
// Basic functionality vs full UI
|
||||
check(type, provider, inline) {
|
||||
check(type, provider, playsinline) {
|
||||
let api = false;
|
||||
let ui = false;
|
||||
const browser = utils.getBrowser();
|
||||
const playsInline = browser.isIPhone && inline && support.inline;
|
||||
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
|
||||
|
||||
switch (`${provider}:${type}`) {
|
||||
case 'html5:video':
|
||||
api = support.video;
|
||||
ui = api && support.rangeInput && (!browser.isIPhone || playsInline);
|
||||
ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline);
|
||||
break;
|
||||
|
||||
case 'html5:audio':
|
||||
@@ -32,7 +32,7 @@ const support = {
|
||||
case 'youtube:video':
|
||||
case 'vimeo:video':
|
||||
api = true;
|
||||
ui = support.rangeInput && (!browser.isIPhone || playsInline);
|
||||
ui = support.rangeInput && (!browser.isIPhone || canPlayInline);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -59,7 +59,7 @@ const support = {
|
||||
|
||||
// Inline playback support
|
||||
// https://webkit.org/blog/6784/new-video-policies-for-ios/
|
||||
inline: 'playsInline' in document.createElement('video'),
|
||||
playsinline: 'playsInline' in document.createElement('video'),
|
||||
|
||||
// Check for mime type support against a player instance
|
||||
// Credits: http://diveintohtml5.info/everything.html
|
||||
@@ -133,6 +133,7 @@ const support = {
|
||||
},
|
||||
});
|
||||
window.addEventListener('test', null, options);
|
||||
window.removeEventListener('test', null, options);
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
+79
-170
@@ -2,10 +2,14 @@
|
||||
// Plyr UI
|
||||
// ==========================================================================
|
||||
|
||||
import utils from './utils';
|
||||
import captions from './captions';
|
||||
import controls from './controls';
|
||||
import i18n from './i18n';
|
||||
import support from './support';
|
||||
import utils from './utils';
|
||||
|
||||
// Sniff out the browser
|
||||
const browser = utils.getBrowser();
|
||||
|
||||
const ui = {
|
||||
addStyleHook() {
|
||||
@@ -48,11 +52,6 @@ const ui = {
|
||||
this.listeners.controls();
|
||||
}
|
||||
|
||||
// If there's no controls, bail
|
||||
if (!utils.is.element(this.elements.controls)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove native controls
|
||||
ui.toggleNativeControls.call(this);
|
||||
|
||||
@@ -75,14 +74,26 @@ const ui = {
|
||||
this.quality = null;
|
||||
|
||||
// Reset volume display
|
||||
ui.updateVolume.call(this);
|
||||
controls.updateVolume.call(this);
|
||||
|
||||
// Reset time display
|
||||
ui.timeUpdate.call(this);
|
||||
controls.timeUpdate.call(this);
|
||||
|
||||
// Update the UI
|
||||
ui.checkPlaying.call(this);
|
||||
|
||||
// Check for picture-in-picture support
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
|
||||
|
||||
// Check for airplay support
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
||||
|
||||
// Add iOS class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
||||
|
||||
// Add touch class
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
||||
|
||||
// Ready for API calls
|
||||
this.ready = true;
|
||||
|
||||
@@ -93,6 +104,11 @@ const ui = {
|
||||
|
||||
// Set the title
|
||||
ui.setTitle.call(this);
|
||||
|
||||
// Assure the poster image is set, if the property was added before the element was created
|
||||
if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) {
|
||||
ui.setPoster.call(this, this.poster);
|
||||
}
|
||||
},
|
||||
|
||||
// Setup aria attribute for play and iframe title
|
||||
@@ -126,22 +142,64 @@ const ui = {
|
||||
|
||||
// Default to media type
|
||||
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
|
||||
const format = i18n.get('frameTitle', this.config);
|
||||
|
||||
iframe.setAttribute('title', i18n.get('frameTitle', this.config));
|
||||
iframe.setAttribute('title', format.replace('{title}', title));
|
||||
}
|
||||
},
|
||||
|
||||
// Toggle poster
|
||||
togglePoster(enable) {
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
|
||||
},
|
||||
|
||||
// Set the poster image (async)
|
||||
setPoster(poster) {
|
||||
// Set property regardless of validity
|
||||
this.media.setAttribute('poster', poster);
|
||||
|
||||
// Bail if element is missing
|
||||
if (!utils.is.element(this.elements.poster)) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
// Load the image, and set poster if successful
|
||||
const loadPromise = utils.loadImage(poster)
|
||||
.then(() => {
|
||||
this.elements.poster.style.backgroundImage = `url('${poster}')`;
|
||||
Object.assign(this.elements.poster.style, {
|
||||
backgroundImage: `url('${poster}')`,
|
||||
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
|
||||
backgroundSize: '',
|
||||
});
|
||||
ui.togglePoster.call(this, true);
|
||||
return poster;
|
||||
});
|
||||
|
||||
// Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video)
|
||||
loadPromise.catch(() => ui.togglePoster.call(this, false));
|
||||
|
||||
// Return the promise so the caller can use it as well
|
||||
return loadPromise;
|
||||
},
|
||||
|
||||
// Check playing state
|
||||
checkPlaying() {
|
||||
checkPlaying(event) {
|
||||
// Class hooks
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused);
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
|
||||
|
||||
// Set ARIA state
|
||||
utils.toggleState(this.elements.buttons.play, this.playing);
|
||||
|
||||
// Only update controls on non timeupdate events
|
||||
if (utils.is.event(event) && event.type === 'timeupdate') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle controls
|
||||
this.toggleControls(!this.playing);
|
||||
ui.toggleControls.call(this);
|
||||
},
|
||||
|
||||
// Check if media is loading
|
||||
@@ -156,171 +214,22 @@ const ui = {
|
||||
|
||||
// Timer to prevent flicker when seeking
|
||||
this.timers.loading = setTimeout(() => {
|
||||
// Toggle container class hook
|
||||
// Update progress bar loading class state
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||
|
||||
// Show controls if loading, hide if done
|
||||
this.toggleControls(this.loading);
|
||||
// Update controls visibility
|
||||
ui.toggleControls.call(this);
|
||||
}, this.loading ? 250 : 0);
|
||||
},
|
||||
|
||||
// Check if media failed to load
|
||||
checkFailed() {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState
|
||||
this.failed = this.media.networkState === 3;
|
||||
// Toggle controls based on state and `force` argument
|
||||
toggleControls(force) {
|
||||
const { controls } = this.elements;
|
||||
|
||||
if (this.failed) {
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, false);
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.error, true);
|
||||
if (controls && this.config.hideControls) {
|
||||
// Show controls if force, loading, paused, or button interaction, otherwise hide
|
||||
this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover));
|
||||
}
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(this.timers.failed);
|
||||
|
||||
// Timer to prevent flicker when seeking
|
||||
this.timers.loading = setTimeout(() => {
|
||||
// Toggle container class hook
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||
|
||||
// Show controls if loading, hide if done
|
||||
this.toggleControls(this.loading);
|
||||
}, this.loading ? 250 : 0);
|
||||
},
|
||||
|
||||
// Update volume UI and storage
|
||||
updateVolume() {
|
||||
if (!this.supported.ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update range
|
||||
if (utils.is.element(this.elements.inputs.volume)) {
|
||||
ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
|
||||
}
|
||||
|
||||
// Update mute state
|
||||
if (utils.is.element(this.elements.buttons.mute)) {
|
||||
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
|
||||
}
|
||||
},
|
||||
|
||||
// Update seek value and lower fill
|
||||
setRange(target, value = 0) {
|
||||
if (!utils.is.element(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
target.value = value;
|
||||
|
||||
// Webkit range fill
|
||||
controls.updateRangeFill.call(this, target);
|
||||
},
|
||||
|
||||
// Set <progress> value
|
||||
setProgress(target, input) {
|
||||
const value = utils.is.number(input) ? input : 0;
|
||||
const progress = utils.is.element(target) ? target : this.elements.display.buffer;
|
||||
|
||||
// Update value and label
|
||||
if (utils.is.element(progress)) {
|
||||
progress.value = value;
|
||||
|
||||
// Update text label inside
|
||||
const label = progress.getElementsByTagName('span')[0];
|
||||
if (utils.is.element(label)) {
|
||||
label.childNodes[0].nodeValue = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Update <progress> elements
|
||||
updateProgress(event) {
|
||||
if (!this.supported.ui || !utils.is.event(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = 0;
|
||||
|
||||
if (event) {
|
||||
switch (event.type) {
|
||||
// Video playing
|
||||
case 'timeupdate':
|
||||
case 'seeking':
|
||||
value = utils.getPercentage(this.currentTime, this.duration);
|
||||
|
||||
// Set seek range value only if it's a 'natural' time event
|
||||
if (event.type === 'timeupdate') {
|
||||
ui.setRange.call(this, this.elements.inputs.seek, value);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Check buffer status
|
||||
case 'playing':
|
||||
case 'progress':
|
||||
ui.setProgress.call(this, this.elements.display.buffer, this.buffered * 100);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Update the displayed time
|
||||
updateTimeDisplay(target = null, time = 0, inverted = false) {
|
||||
// Bail if there's no element to display or the value isn't a number
|
||||
if (!utils.is.element(target) || !utils.is.number(time)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always display hours if duration is over an hour
|
||||
const displayHours = utils.getHours(this.duration) > 0;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
target.textContent = utils.formatTime(time, displayHours, inverted);
|
||||
},
|
||||
|
||||
// Handle time change event
|
||||
timeUpdate(event) {
|
||||
// Only invert if only one time element is displayed and used for both duration and currentTime
|
||||
const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
|
||||
|
||||
// Duration
|
||||
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
|
||||
|
||||
// Ignore updates while seeking
|
||||
if (event && event.type === 'timeupdate' && this.media.seeking) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Playing progress
|
||||
ui.updateProgress.call(this, event);
|
||||
},
|
||||
|
||||
// Show the duration on metadataloaded
|
||||
durationUpdate() {
|
||||
if (!this.supported.ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's a spot to display duration
|
||||
const hasDuration = utils.is.element(this.elements.display.duration);
|
||||
|
||||
// If there's only one time display, display duration there
|
||||
if (!hasDuration && this.config.displayDuration && this.paused) {
|
||||
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
|
||||
}
|
||||
|
||||
// If there's a duration element, update content
|
||||
if (hasDuration) {
|
||||
ui.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
|
||||
}
|
||||
|
||||
// Update the tooltip (if visible)
|
||||
controls.updateSeekTooltip.call(this);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+73
-91
@@ -3,16 +3,13 @@
|
||||
// ==========================================================================
|
||||
|
||||
import loadjs from 'loadjs';
|
||||
|
||||
import Storage from './storage';
|
||||
import support from './support';
|
||||
import { providers } from './types';
|
||||
|
||||
const utils = {
|
||||
// Check variable types
|
||||
is: {
|
||||
plyr(input) {
|
||||
return this.instanceof(input, window.Plyr);
|
||||
},
|
||||
object(input) {
|
||||
return this.getConstructor(input) === Object;
|
||||
},
|
||||
@@ -32,19 +29,19 @@ const utils = {
|
||||
return !this.nullOrUndefined(input) && Array.isArray(input);
|
||||
},
|
||||
weakMap(input) {
|
||||
return this.instanceof(input, window.WeakMap);
|
||||
return this.instanceof(input, WeakMap);
|
||||
},
|
||||
nodeList(input) {
|
||||
return this.instanceof(input, window.NodeList);
|
||||
return this.instanceof(input, NodeList);
|
||||
},
|
||||
element(input) {
|
||||
return this.instanceof(input, window.Element);
|
||||
return this.instanceof(input, Element);
|
||||
},
|
||||
textNode(input) {
|
||||
return this.getConstructor(input) === Text;
|
||||
},
|
||||
event(input) {
|
||||
return this.instanceof(input, window.Event);
|
||||
return this.instanceof(input, Event);
|
||||
},
|
||||
cue(input) {
|
||||
return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue);
|
||||
@@ -123,6 +120,21 @@ const utils = {
|
||||
});
|
||||
},
|
||||
|
||||
// Load image avoiding xhr/fetch CORS issues
|
||||
// Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded.
|
||||
// By default it checks if it is at least 1px, but you can add a second argument to change this.
|
||||
loadImage(src, minWidth = 1) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
const handler = () => {
|
||||
delete image.onload;
|
||||
delete image.onerror;
|
||||
(image.naturalWidth >= minWidth ? resolve : reject)(image);
|
||||
};
|
||||
Object.assign(image, {onload: handler, onerror: handler, src});
|
||||
});
|
||||
},
|
||||
|
||||
// Load an external script
|
||||
loadScript(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -160,6 +172,8 @@ const utils = {
|
||||
|
||||
// Only load once if ID set
|
||||
if (!hasId || !exists()) {
|
||||
const useStorage = Storage.supported;
|
||||
|
||||
// Create container
|
||||
const container = document.createElement('div');
|
||||
utils.toggleHidden(container, true);
|
||||
@@ -169,7 +183,7 @@ const utils = {
|
||||
}
|
||||
|
||||
// Check in cache
|
||||
if (support.storage) {
|
||||
if (useStorage) {
|
||||
const cached = window.localStorage.getItem(prefix + id);
|
||||
isCached = cached !== null;
|
||||
|
||||
@@ -188,7 +202,7 @@ const utils = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (support.storage) {
|
||||
if (useStorage) {
|
||||
window.localStorage.setItem(
|
||||
prefix + id,
|
||||
JSON.stringify({
|
||||
@@ -251,7 +265,7 @@ const utils = {
|
||||
|
||||
// Add text node
|
||||
if (utils.is.string(text)) {
|
||||
element.textContent = text;
|
||||
element.innerText = text;
|
||||
}
|
||||
|
||||
// Return built element
|
||||
@@ -269,14 +283,14 @@ const utils = {
|
||||
parent.appendChild(utils.createElement(type, attributes, text));
|
||||
},
|
||||
|
||||
// Remove an element
|
||||
// Remove element(s)
|
||||
removeElement(element) {
|
||||
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
|
||||
if (utils.is.nodeList(element) || utils.is.array(element)) {
|
||||
Array.from(element).forEach(utils.removeElement);
|
||||
return;
|
||||
}
|
||||
|
||||
if (utils.is.nodeList(element) || utils.is.array(element)) {
|
||||
Array.from(element).forEach(utils.removeElement);
|
||||
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -375,14 +389,35 @@ const utils = {
|
||||
return attributes;
|
||||
},
|
||||
|
||||
// Toggle class on an element
|
||||
toggleClass(element, className, toggle) {
|
||||
// Toggle hidden
|
||||
toggleHidden(element, hidden) {
|
||||
if (!utils.is.element(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hide = hidden;
|
||||
|
||||
if (!utils.is.boolean(hide)) {
|
||||
hide = !element.hasAttribute('hidden');
|
||||
}
|
||||
|
||||
if (hide) {
|
||||
element.setAttribute('hidden', '');
|
||||
} else {
|
||||
element.removeAttribute('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
|
||||
toggleClass(element, className, force) {
|
||||
if (utils.is.element(element)) {
|
||||
const contains = element.classList.contains(className);
|
||||
let method = 'toggle';
|
||||
if (typeof force !== 'undefined') {
|
||||
method = force ? 'add' : 'remove';
|
||||
}
|
||||
|
||||
element.classList[toggle ? 'add' : 'remove'](className);
|
||||
|
||||
return (toggle && !contains) || (!toggle && contains);
|
||||
element.classList[method](className);
|
||||
return element.classList.contains(className);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -393,19 +428,6 @@ const utils = {
|
||||
return utils.is.element(element) && element.classList.contains(className);
|
||||
},
|
||||
|
||||
// Toggle hidden attribute on an element
|
||||
toggleHidden(element, toggle) {
|
||||
if (!utils.is.element(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (toggle) {
|
||||
element.setAttribute('hidden', '');
|
||||
} else {
|
||||
element.removeAttribute('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
// Element matches selector
|
||||
matches(element, selector) {
|
||||
const prototype = { Element };
|
||||
@@ -429,60 +451,6 @@ const utils = {
|
||||
return this.elements.container.querySelector(selector);
|
||||
},
|
||||
|
||||
// Find the UI controls and store references in custom controls
|
||||
// TODO: Allow settings menus with custom controls
|
||||
findElements() {
|
||||
try {
|
||||
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
|
||||
|
||||
// Buttons
|
||||
this.elements.buttons = {
|
||||
play: utils.getElements.call(this, this.config.selectors.buttons.play),
|
||||
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
|
||||
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
|
||||
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
|
||||
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
|
||||
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
|
||||
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
|
||||
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
|
||||
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
|
||||
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
|
||||
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
|
||||
};
|
||||
|
||||
// Progress
|
||||
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
|
||||
|
||||
// Inputs
|
||||
this.elements.inputs = {
|
||||
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
|
||||
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
|
||||
};
|
||||
|
||||
// Display
|
||||
this.elements.display = {
|
||||
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
|
||||
duration: utils.getElement.call(this, this.config.selectors.display.duration),
|
||||
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
|
||||
};
|
||||
|
||||
// Seek tooltip
|
||||
if (utils.is.element(this.elements.progress)) {
|
||||
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Log it
|
||||
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
|
||||
|
||||
// Restore native video controls
|
||||
this.toggleNativeControls(true);
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Get the focused element
|
||||
getFocusElement() {
|
||||
let focused = document.activeElement;
|
||||
@@ -596,7 +564,7 @@ const utils = {
|
||||
const event = new CustomEvent(type, {
|
||||
bubbles,
|
||||
detail: Object.assign({}, detail, {
|
||||
plyr: utils.is.plyr(this) ? this : null,
|
||||
plyr: this,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -626,6 +594,15 @@ const utils = {
|
||||
element.setAttribute('aria-pressed', state);
|
||||
},
|
||||
|
||||
// Format string
|
||||
format(input, ...args) {
|
||||
if (utils.is.empty(input)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return input.toString().replace(/{(\d+)}/g, (match, i) => (utils.is.string(args[i]) ? args[i] : ''));
|
||||
},
|
||||
|
||||
// Get percentage
|
||||
getPercentage(current, max) {
|
||||
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
|
||||
@@ -746,13 +723,18 @@ const utils = {
|
||||
return array.filter((item, index) => array.indexOf(item) === index);
|
||||
},
|
||||
|
||||
// Clone nested objects
|
||||
cloneDeep(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
},
|
||||
|
||||
// Get the closest value in an array
|
||||
closest(array, value) {
|
||||
if (!utils.is.array(array) || !array.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
|
||||
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
|
||||
},
|
||||
|
||||
// Get the provider for a given URL
|
||||
@@ -763,7 +745,7 @@ const utils = {
|
||||
}
|
||||
|
||||
// Vimeo
|
||||
if (/^https?:\/\/player.vimeo.com\/video\/\d{8,}(?=\b|\/)/.test(url)) {
|
||||
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
|
||||
return providers.vimeo;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
button {
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// Ignore focus
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
// YouTube, Vimeo, etc
|
||||
// --------------------------------------------------------------
|
||||
|
||||
.plyr__video-embed {
|
||||
// Default to 16:9 ratio but this is set by JavaScript based on config
|
||||
$padding: ((100 / 16) * 9);
|
||||
$height: 240;
|
||||
$offset: to-percentage(($height - $padding) / ($height / 50));
|
||||
// Default to 16:9 ratio but this is set by JavaScript based on config
|
||||
$embed-padding: ((100 / 16) * 9);
|
||||
|
||||
.plyr__video-embed {
|
||||
height: 0;
|
||||
padding-bottom: to-percentage($padding);
|
||||
padding-bottom: to-percentage($embed-padding);
|
||||
position: relative;
|
||||
|
||||
iframe {
|
||||
@@ -22,15 +20,22 @@
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Vimeo hack
|
||||
> div {
|
||||
// If the full custom UI is supported
|
||||
.plyr--full-ui .plyr__video-embed {
|
||||
$height: 240;
|
||||
$offset: to-percentage(($height - $embed-padding) / ($height / 50));
|
||||
|
||||
// To allow mouse events to be captured if full support
|
||||
iframe {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Only used for Vimeo
|
||||
> .plyr__video-embed__container {
|
||||
padding-bottom: to-percentage($height);
|
||||
position: relative;
|
||||
transform: translateY(-$offset);
|
||||
}
|
||||
}
|
||||
// To allow mouse events to be captured if full support
|
||||
.plyr--full-ui .plyr__video-embed iframe {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
right: -3px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
z-index: 3;
|
||||
|
||||
> div {
|
||||
overflow: hidden;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// --------------------------------------------------------------
|
||||
// Faux poster overlay
|
||||
// --------------------------------------------------------------
|
||||
|
||||
.plyr__poster {
|
||||
background-color: #000;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.plyr--stopped.plyr__poster-enabled .plyr__poster {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -6,10 +6,15 @@
|
||||
display: flex;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
margin-right: $plyr-range-thumb-height;
|
||||
left: $plyr-range-thumb-height / 2;
|
||||
|
||||
input[type='range'] {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
// Offset the range thumb in order to be able to calculate the relative progress (#954)
|
||||
width: calc(100% + #{$plyr-range-thumb-height}) !important;
|
||||
margin: 0 -#{$plyr-range-thumb-height / 2} !important;
|
||||
}
|
||||
|
||||
// Seek tooltip to show time
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
@include plyr-range-track();
|
||||
background-image: linear-gradient(to right, currentColor var(--value), transparent var(--value));
|
||||
background-image: linear-gradient(to right, currentColor var(--value, 0%), transparent var(--value, 0%));
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
transform: translate(-50%, 10px) scale(0.8);
|
||||
transform-origin: 50% 100%;
|
||||
transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;
|
||||
white-space: nowrap;
|
||||
z-index: 2;
|
||||
|
||||
// The background triangle
|
||||
|
||||
@@ -23,7 +23,12 @@
|
||||
// Hide sound controls on iOS
|
||||
// It's not supported to change volume using JavaScript:
|
||||
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
|
||||
.plyr--is-ios .plyr__volume,
|
||||
.plyr--is-ios [data-plyr='mute'] {
|
||||
.plyr--is-ios .plyr__volume {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// Vimeo has no toggle mute method so hide mute button
|
||||
// https://github.com/vimeo/player.js/issues/236#issuecomment-384663183
|
||||
.plyr--is-ios.plyr--vimeo [data-plyr='mute'] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
+1
-1
@@ -32,13 +32,13 @@
|
||||
@import 'components/embed';
|
||||
@import 'components/menus';
|
||||
@import 'components/progress';
|
||||
@import 'components/poster';
|
||||
@import 'components/sliders';
|
||||
@import 'components/times';
|
||||
@import 'components/tooltips';
|
||||
@import 'components/video';
|
||||
@import 'components/volume';
|
||||
|
||||
@import 'states/error';
|
||||
@import 'states/fullscreen';
|
||||
|
||||
@import 'plugins/ads';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,6 @@
|
||||
// Hiding content nicely
|
||||
// --------------------------------------------------------------
|
||||
|
||||
// Attributes
|
||||
.plyr--full-ui [hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.plyr--full-ui [aria-hidden='true'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Screen reader only elements
|
||||
.plyr__sr-only {
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
|
||||
Reference in New Issue
Block a user