Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4caba120c | |||
| 969a877a34 | |||
| fb22a90d33 | |||
| 108bd3dfa0 | |||
| 5a445ae647 | |||
| 56668f58b6 | |||
| eec96e5879 | |||
| c6c9d877e4 | |||
| 61f4b998e1 | |||
| 25a319d884 | |||
| 4bf678fe6c | |||
| 359acd6bb9 | |||
| 2fce385691 | |||
| a82c61c539 | |||
| a8fa125a96 | |||
| 6435ced707 | |||
| 94dc0d176c | |||
| 23c21252e8 | |||
| 41c7dff0e8 | |||
| e3bae562fc | |||
| 1c1668bfc3 | |||
| e0c09c51f2 | |||
| 8de06fb862 | |||
| 64412868d8 | |||
| 450958c290 | |||
| 963fe11ad6 | |||
| ce199e4b6b | |||
| 9d798893b5 | |||
| 64399e0717 | |||
| f58e23b325 | |||
| 812e07b734 | |||
| c9298fde76 | |||
| 0109454a34 | |||
| 813f703211 | |||
| 7aad747c25 | |||
| d70a787af1 | |||
| 69bb0917ad | |||
| 6f256d09b2 | |||
| e9684c2021 | |||
| bf91a0e73f | |||
| 14b6309aef | |||
| 6391ced99f | |||
| fac8a185ba | |||
| c69aa8a42b | |||
| f34bf22125 | |||
| f0be913dc3 | |||
| cd51788b98 | |||
| edd67b0da3 | |||
| d733454d7f | |||
| 41f9a87e0e | |||
| f4858f0c62 | |||
| 121093ae71 | |||
| aa8fc313a9 | |||
| 723298a07b | |||
| f8c89e3e95 | |||
| 333435a9c2 | |||
| 3ab2295fe7 | |||
| c41bb657ac | |||
| 55bbf64f2b | |||
| 3bba65f2c2 | |||
| 1bab0d07b5 | |||
| 602353f4d9 | |||
| 51814249af | |||
| 37c5fbfe16 | |||
| d7356726a1 | |||
| 4db6bf7a2e | |||
| 28826f6402 | |||
| c845558d96 | |||
| 16c3a7d9e5 | |||
| 90d5b48845 | |||
| d1acc4abb3 | |||
| 797b70998f | |||
| 4a01027da0 | |||
| 7ca2169790 | |||
| 054f522aa9 | |||
| f2fc3f5ea5 | |||
| 765c01e83d | |||
| 33a11fb53a | |||
| d1d41ca49a | |||
| c06e0ee5e9 | |||
| 83f80ccc40 | |||
| 069065ea3a | |||
| 1672e78041 | |||
| 34401de3d0 | |||
| f687b81b70 | |||
| bbb11e611e | |||
| 90919411e9 | |||
| 1491b017a0 | |||
| 403df36af6 | |||
| 24d833a5d1 | |||
| 44b30380f7 | |||
| f13260c10a | |||
| e1183d6049 |
@@ -32,6 +32,7 @@
|
|||||||
"message": "Use local parameter instead."
|
"message": "Use local parameter instead."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"no-param-reassign": [2, { "props": false }],
|
||||||
"array-bracket-newline": [2, { "minItems": 2 }],
|
"array-bracket-newline": [2, { "minItems": 2 }],
|
||||||
"array-element-newline": [2, { "minItems": 2 }]
|
"array-element-newline": [2, { "minItems": 2 }]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
### Link to related issue (if applicable)
|
### Link to related issue (if applicable)
|
||||||
|
|
||||||
### Sumary of proposed changes
|
### Summary of proposed changes
|
||||||
|
|
||||||
### Task list
|
### Checklist
|
||||||
|
- [ ] Use `develop` as the base branch
|
||||||
- [ ] Tested on [supported browsers](https://github.com/sampotts/plyr#browser-support)
|
- [ ] Exclude the gulp build from the PR
|
||||||
- [ ] Gulp build completed
|
- [ ] Test on [supported browsers](https://github.com/sampotts/plyr#browser-support)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- 'lts/*'
|
||||||
|
|
||||||
|
script:
|
||||||
|
- npm run lint
|
||||||
|
- npm run build
|
||||||
@@ -1,3 +1,46 @@
|
|||||||
|
# v3.3.10
|
||||||
|
|
||||||
|
* Fix for buffer display alignment and incorrect BEM classname
|
||||||
|
* Fix for playback not resuming position after quality swap (fixes #991, thanks @philipgiuliani!)
|
||||||
|
* Travis integration (thanks @friday!)
|
||||||
|
* Translate quality badges and quality names (thanks @philipgiuliani!)
|
||||||
|
* Improve captions handling for streaming (thanks @friday!)
|
||||||
|
* Call duration update method manually if user config has duration (thanks @friday!)
|
||||||
|
|
||||||
|
# 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
|
# v3.3.5
|
||||||
|
|
||||||
* Removed `.load()` call as it breaks HLS (see #870)
|
* Removed `.load()` call as it breaks HLS (see #870)
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+148
-13
@@ -1,4 +1,4 @@
|
|||||||
(function () {
|
typeof navigator === "object" && (function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
||||||
@@ -97,7 +97,7 @@ function isObject(what) {
|
|||||||
// Yanked from https://git.io/vS8DV re-used under CC0
|
// Yanked from https://git.io/vS8DV re-used under CC0
|
||||||
// with some tiny modifications
|
// with some tiny modifications
|
||||||
function isError(value) {
|
function isError(value) {
|
||||||
switch ({}.toString.call(value)) {
|
switch (Object.prototype.toString.call(value)) {
|
||||||
case '[object Error]':
|
case '[object Error]':
|
||||||
return true;
|
return true;
|
||||||
case '[object Exception]':
|
case '[object Exception]':
|
||||||
@@ -110,7 +110,15 @@ function isError(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isErrorEvent(value) {
|
function isErrorEvent(value) {
|
||||||
return supportsErrorEvent() && {}.toString.call(value) === '[object ErrorEvent]';
|
return Object.prototype.toString.call(value) === '[object ErrorEvent]';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDOMError(value) {
|
||||||
|
return Object.prototype.toString.call(value) === '[object DOMError]';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDOMException(value) {
|
||||||
|
return Object.prototype.toString.call(value) === '[object DOMException]';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUndefined(what) {
|
function isUndefined(what) {
|
||||||
@@ -153,6 +161,24 @@ function supportsErrorEvent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function supportsDOMError() {
|
||||||
|
try {
|
||||||
|
new DOMError(''); // eslint-disable-line no-new
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsDOMException() {
|
||||||
|
try {
|
||||||
|
new DOMException(''); // eslint-disable-line no-new
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function supportsFetch() {
|
function supportsFetch() {
|
||||||
if (!('fetch' in _window)) return false;
|
if (!('fetch' in _window)) return false;
|
||||||
|
|
||||||
@@ -668,6 +694,8 @@ var utils = {
|
|||||||
isObject: isObject,
|
isObject: isObject,
|
||||||
isError: isError,
|
isError: isError,
|
||||||
isErrorEvent: isErrorEvent,
|
isErrorEvent: isErrorEvent,
|
||||||
|
isDOMError: isDOMError,
|
||||||
|
isDOMException: isDOMException,
|
||||||
isUndefined: isUndefined,
|
isUndefined: isUndefined,
|
||||||
isFunction: isFunction,
|
isFunction: isFunction,
|
||||||
isPlainObject: isPlainObject,
|
isPlainObject: isPlainObject,
|
||||||
@@ -675,6 +703,8 @@ var utils = {
|
|||||||
isArray: isArray,
|
isArray: isArray,
|
||||||
isEmptyObject: isEmptyObject,
|
isEmptyObject: isEmptyObject,
|
||||||
supportsErrorEvent: supportsErrorEvent,
|
supportsErrorEvent: supportsErrorEvent,
|
||||||
|
supportsDOMError: supportsDOMError,
|
||||||
|
supportsDOMException: supportsDOMException,
|
||||||
supportsFetch: supportsFetch,
|
supportsFetch: supportsFetch,
|
||||||
supportsReferrerPolicy: supportsReferrerPolicy,
|
supportsReferrerPolicy: supportsReferrerPolicy,
|
||||||
supportsPromiseRejectionEvent: supportsPromiseRejectionEvent,
|
supportsPromiseRejectionEvent: supportsPromiseRejectionEvent,
|
||||||
@@ -729,10 +759,24 @@ var ERROR_TYPES_RE = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Ran
|
|||||||
|
|
||||||
function getLocationHref() {
|
function getLocationHref() {
|
||||||
if (typeof document === 'undefined' || document.location == null) return '';
|
if (typeof document === 'undefined' || document.location == null) return '';
|
||||||
|
|
||||||
return document.location.href;
|
return document.location.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLocationOrigin() {
|
||||||
|
if (typeof document === 'undefined' || document.location == null) return '';
|
||||||
|
|
||||||
|
// Oh dear IE10...
|
||||||
|
if (!document.location.origin) {
|
||||||
|
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
|
* TraceKit.report: cross-browser processing of unhandled exceptions
|
||||||
*
|
*
|
||||||
@@ -1140,6 +1184,44 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
|
|||||||
element.func = UNKNOWN_FUNCTION;
|
element.func = UNKNOWN_FUNCTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (element.url && element.url.substr(0, 5) === 'blob:') {
|
||||||
|
// Special case for handling JavaScript loaded into a blob.
|
||||||
|
// We use a synchronous AJAX request here as a blob is already in
|
||||||
|
// memory - it's not making a network request. This will generate a warning
|
||||||
|
// in the browser console, but there has already been an error so that's not
|
||||||
|
// that much of an issue.
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', element.url, false);
|
||||||
|
xhr.send(null);
|
||||||
|
|
||||||
|
// If we failed to download the source, skip this patch
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
var source = xhr.responseText || '';
|
||||||
|
|
||||||
|
// We trim the source down to the last 300 characters as sourceMappingURL is always at the end of the file.
|
||||||
|
// Why 300? To be in line with: https://github.com/getsentry/sentry/blob/4af29e8f2350e20c28a6933354e4f42437b4ba42/src/sentry/lang/javascript/processor.py#L164-L175
|
||||||
|
source = source.slice(-300);
|
||||||
|
|
||||||
|
// Now we dig out the source map URL
|
||||||
|
var sourceMaps = source.match(/\/\/# sourceMappingURL=(.*)$/);
|
||||||
|
|
||||||
|
// If we don't find a source map comment or we find more than one, continue on to the next element.
|
||||||
|
if (sourceMaps) {
|
||||||
|
var sourceMapAddress = sourceMaps[1];
|
||||||
|
|
||||||
|
// Now we check to see if it's a relative URL.
|
||||||
|
// If it is, convert it to an absolute one.
|
||||||
|
if (sourceMapAddress.charAt(0) === '~') {
|
||||||
|
sourceMapAddress = getLocationOrigin() + sourceMapAddress.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we strip the '.map' off of the end of the URL and update the
|
||||||
|
// element so that Sentry can match the map to the blob.
|
||||||
|
element.url = sourceMapAddress.slice(0, -4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stack.push(element);
|
stack.push(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1651,10 +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 isError$1 = utils.isError;
|
||||||
var isObject$1 = utils.isObject;
|
var isObject$1 = utils.isObject;
|
||||||
var isPlainObject$1 = utils.isPlainObject;
|
var isPlainObject$1 = utils.isPlainObject;
|
||||||
var isErrorEvent$1 = utils.isErrorEvent;
|
|
||||||
var isUndefined$1 = utils.isUndefined;
|
var isUndefined$1 = utils.isUndefined;
|
||||||
var isFunction$1 = utils.isFunction;
|
var isFunction$1 = utils.isFunction;
|
||||||
var isString$1 = utils.isString;
|
var isString$1 = utils.isString;
|
||||||
@@ -1782,7 +1866,7 @@ Raven.prototype = {
|
|||||||
// webpack (using a build step causes webpack #1617). Grunt verifies that
|
// webpack (using a build step causes webpack #1617). Grunt verifies that
|
||||||
// this value matches package.json during build.
|
// this value matches package.json during build.
|
||||||
// See: https://github.com/getsentry/raven-js/issues/465
|
// See: https://github.com/getsentry/raven-js/issues/465
|
||||||
VERSION: '3.24.2',
|
VERSION: '3.25.2',
|
||||||
|
|
||||||
debug: false,
|
debug: false,
|
||||||
|
|
||||||
@@ -2114,6 +2198,23 @@ Raven.prototype = {
|
|||||||
if (isErrorEvent$1(ex) && ex.error) {
|
if (isErrorEvent$1(ex) && ex.error) {
|
||||||
// If it is an ErrorEvent with `error` property, extract it to get actual Error
|
// If it is an ErrorEvent with `error` property, extract it to get actual Error
|
||||||
ex = ex.error;
|
ex = ex.error;
|
||||||
|
} else if (isDOMError$1(ex) || isDOMException$1(ex)) {
|
||||||
|
// If it is a DOMError or DOMException (which are legacy APIs, but still supported in some browsers)
|
||||||
|
// then we just extract the name and message, as they don't provide anything else
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/DOMError
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/DOMException
|
||||||
|
var name = ex.name || (isDOMError$1(ex) ? 'DOMError' : 'DOMException');
|
||||||
|
var message = ex.message ? name + ': ' + ex.message : name;
|
||||||
|
|
||||||
|
return this.captureMessage(
|
||||||
|
message,
|
||||||
|
objectMerge$1(options, {
|
||||||
|
// neither DOMError or DOMException provide stack trace and we most likely wont get it this way as well
|
||||||
|
// but it's barely any overhead so we may at least try
|
||||||
|
stacktrace: true,
|
||||||
|
trimHeadFrames: options.trimHeadFrames + 1
|
||||||
|
})
|
||||||
|
);
|
||||||
} else if (isError$1(ex)) {
|
} else if (isError$1(ex)) {
|
||||||
// we have a real Error object
|
// we have a real Error object
|
||||||
ex = ex;
|
ex = ex;
|
||||||
@@ -2125,6 +2226,7 @@ Raven.prototype = {
|
|||||||
ex = new Error(options.message);
|
ex = new Error(options.message);
|
||||||
} else {
|
} else {
|
||||||
// If none of previous checks were valid, then it means that
|
// If none of previous checks were valid, then it means that
|
||||||
|
// it's not a DOMError/DOMException
|
||||||
// it's not a plain Object
|
// it's not a plain Object
|
||||||
// it's not a valid ErrorEvent (one with an error property)
|
// it's not a valid ErrorEvent (one with an error property)
|
||||||
// it's not an Error
|
// it's not an Error
|
||||||
@@ -3073,8 +3175,8 @@ Raven.prototype = {
|
|||||||
var hasPushAndReplaceState =
|
var hasPushAndReplaceState =
|
||||||
!isChromePackagedApp &&
|
!isChromePackagedApp &&
|
||||||
_window$2.history &&
|
_window$2.history &&
|
||||||
history.pushState &&
|
_window$2.history.pushState &&
|
||||||
history.replaceState;
|
_window$2.history.replaceState;
|
||||||
if (autoBreadcrumbs.location && hasPushAndReplaceState) {
|
if (autoBreadcrumbs.location && hasPushAndReplaceState) {
|
||||||
// TODO: remove onpopstate handler on uninstall()
|
// TODO: remove onpopstate handler on uninstall()
|
||||||
var oldOnPopState = _window$2.onpopstate;
|
var oldOnPopState = _window$2.onpopstate;
|
||||||
@@ -3103,8 +3205,8 @@ Raven.prototype = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
fill$1(history, 'pushState', historyReplacementFunction, wrappedBuiltIns);
|
fill$1(_window$2.history, 'pushState', historyReplacementFunction, wrappedBuiltIns);
|
||||||
fill$1(history, 'replaceState', historyReplacementFunction, wrappedBuiltIns);
|
fill$1(_window$2.history, 'replaceState', historyReplacementFunction, wrappedBuiltIns);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (autoBreadcrumbs.console && 'console' in _window$2 && console.log) {
|
if (autoBreadcrumbs.console && 'console' in _window$2 && console.log) {
|
||||||
@@ -3320,7 +3422,7 @@ Raven.prototype = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
culprit: fileurl
|
transaction: fileurl
|
||||||
},
|
},
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
@@ -3394,7 +3496,7 @@ Raven.prototype = {
|
|||||||
|
|
||||||
if (this._hasNavigator && _navigator.userAgent) {
|
if (this._hasNavigator && _navigator.userAgent) {
|
||||||
httpData.headers = {
|
httpData.headers = {
|
||||||
'User-Agent': navigator.userAgent
|
'User-Agent': _navigator.userAgent
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3435,7 +3537,7 @@ Raven.prototype = {
|
|||||||
if (
|
if (
|
||||||
!last ||
|
!last ||
|
||||||
current.message !== last.message || // defined for captureMessage
|
current.message !== last.message || // defined for captureMessage
|
||||||
current.culprit !== last.culprit // defined for captureException/onerror
|
current.transaction !== last.transaction // defined for captureException/onerror
|
||||||
)
|
)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -3948,6 +4050,39 @@ singleton.Client = Client;
|
|||||||
'airplay',
|
'airplay',
|
||||||
'fullscreen',
|
'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: {
|
captions: {
|
||||||
active: true
|
active: true
|
||||||
},
|
},
|
||||||
|
|||||||
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
@@ -74,6 +74,39 @@ import Raven from 'raven-js';
|
|||||||
'airplay',
|
'airplay',
|
||||||
'fullscreen',
|
'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: {
|
captions: {
|
||||||
active: true,
|
active: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
// Layout
|
// Layout
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
$container-max-width: 1280px;
|
$container-max-width: 1260px;
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1026
-921
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
+1375
-921
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+11
-4
@@ -13,6 +13,7 @@ const filter = require('gulp-filter');
|
|||||||
const sass = require('gulp-sass');
|
const sass = require('gulp-sass');
|
||||||
const cleancss = require('gulp-clean-css');
|
const cleancss = require('gulp-clean-css');
|
||||||
const run = require('run-sequence');
|
const run = require('run-sequence');
|
||||||
|
const header = require('gulp-header');
|
||||||
const prefix = require('gulp-autoprefixer');
|
const prefix = require('gulp-autoprefixer');
|
||||||
const gitbranch = require('git-branch');
|
const gitbranch = require('git-branch');
|
||||||
const svgstore = require('gulp-svgstore');
|
const svgstore = require('gulp-svgstore');
|
||||||
@@ -146,6 +147,7 @@ const build = {
|
|||||||
options,
|
options,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
|
||||||
.pipe(sourcemaps.write(''))
|
.pipe(sourcemaps.write(''))
|
||||||
.pipe(gulp.dest(output))
|
.pipe(gulp.dest(output))
|
||||||
.pipe(filter('**/*.js'))
|
.pipe(filter('**/*.js'))
|
||||||
@@ -224,9 +226,14 @@ gulp.task('watch', () => {
|
|||||||
gulp.watch(paths.demo.src.sass, tasks.sass);
|
gulp.watch(paths.demo.src.sass, tasks.sass);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Build distribution
|
||||||
|
gulp.task('build', () => {
|
||||||
|
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite);
|
||||||
|
});
|
||||||
|
|
||||||
// Default gulp task
|
// Default gulp task
|
||||||
gulp.task('default', () => {
|
gulp.task('default', () => {
|
||||||
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'watch');
|
run('build', 'watch');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Publish a version to CDN and demo
|
// Publish a version to CDN and demo
|
||||||
@@ -239,11 +246,11 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
|||||||
const branch = {
|
const branch = {
|
||||||
current: gitbranch.sync(),
|
current: gitbranch.sync(),
|
||||||
master: 'master',
|
master: 'master',
|
||||||
beta: 'beta',
|
develop: 'develop',
|
||||||
};
|
};
|
||||||
const allowed = [
|
const allowed = [
|
||||||
branch.master,
|
branch.master,
|
||||||
branch.beta,
|
branch.develop,
|
||||||
];
|
];
|
||||||
|
|
||||||
const maxAge = 31536000; // 1 year
|
const maxAge = 31536000; // 1 year
|
||||||
@@ -255,7 +262,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
demo: {
|
demo: {
|
||||||
uploadPath: branch.current === branch.beta ? 'beta/' : null,
|
uploadPath: branch.current === branch.develop ? 'beta/' : null,
|
||||||
headers: {
|
headers: {
|
||||||
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
|
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
|
||||||
Vary: 'Accept-Encoding',
|
Vary: 'Accept-Encoding',
|
||||||
|
|||||||
+14
-9
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "plyr",
|
"name": "plyr",
|
||||||
"version": "3.3.5",
|
"version": "3.3.10",
|
||||||
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||||
"homepage": "https://plyr.io",
|
"homepage": "https://plyr.io",
|
||||||
"main": "./dist/plyr.js",
|
"main": "./dist/plyr.js",
|
||||||
@@ -11,12 +11,12 @@
|
|||||||
"babel-core": "^6.26.3",
|
"babel-core": "^6.26.3",
|
||||||
"babel-eslint": "^8.2.3",
|
"babel-eslint": "^8.2.3",
|
||||||
"babel-plugin-external-helpers": "^6.22.0",
|
"babel-plugin-external-helpers": "^6.22.0",
|
||||||
"babel-preset-env": "^1.6.1",
|
"babel-preset-env": "^1.7.0",
|
||||||
"del": "^3.0.0",
|
"del": "^3.0.0",
|
||||||
"eslint": "^4.19.1",
|
"eslint": "^4.19.1",
|
||||||
"eslint-config-airbnb-base": "^12.1.0",
|
"eslint-config-airbnb-base": "^12.1.0",
|
||||||
"eslint-config-prettier": "^2.9.0",
|
"eslint-config-prettier": "^2.9.0",
|
||||||
"eslint-plugin-import": "^2.11.0",
|
"eslint-plugin-import": "^2.12.0",
|
||||||
"git-branch": "^2.0.1",
|
"git-branch": "^2.0.1",
|
||||||
"gulp": "^3.9.1",
|
"gulp": "^3.9.1",
|
||||||
"gulp-autoprefixer": "^5.0.0",
|
"gulp-autoprefixer": "^5.0.0",
|
||||||
@@ -24,24 +24,27 @@
|
|||||||
"gulp-clean-css": "^3.9.4",
|
"gulp-clean-css": "^3.9.4",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
"gulp-filter": "^5.1.0",
|
"gulp-filter": "^5.1.0",
|
||||||
|
"gulp-header": "^2.0.5",
|
||||||
"gulp-open": "^3.0.1",
|
"gulp-open": "^3.0.1",
|
||||||
"gulp-rename": "^1.2.2",
|
"gulp-postcss": "^7.0.1",
|
||||||
"gulp-replace": "^0.6.1",
|
"gulp-rename": "^1.2.3",
|
||||||
|
"gulp-replace": "^1.0.0",
|
||||||
"gulp-s3": "^0.11.0",
|
"gulp-s3": "^0.11.0",
|
||||||
"gulp-sass": "^4.0.1",
|
"gulp-sass": "^4.0.1",
|
||||||
"gulp-size": "^3.0.0",
|
"gulp-size": "^3.0.0",
|
||||||
"gulp-sourcemaps": "^2.6.4",
|
"gulp-sourcemaps": "^2.6.4",
|
||||||
"gulp-svgmin": "^1.2.4",
|
"gulp-svgmin": "^1.2.4",
|
||||||
"gulp-svgstore": "^6.1.1",
|
"gulp-svgstore": "^6.1.1",
|
||||||
"gulp-uglify-es": "^1.0.1",
|
"gulp-uglify-es": "^1.0.4",
|
||||||
"gulp-util": "^3.0.8",
|
"gulp-util": "^3.0.8",
|
||||||
|
"postcss-custom-properties": "^7.0.0",
|
||||||
"prettier-eslint": "^8.8.1",
|
"prettier-eslint": "^8.8.1",
|
||||||
"prettier-stylelint": "^0.4.2",
|
"prettier-stylelint": "^0.4.2",
|
||||||
"rollup-plugin-babel": "^3.0.4",
|
"rollup-plugin-babel": "^3.0.4",
|
||||||
"rollup-plugin-commonjs": "^9.1.3",
|
"rollup-plugin-commonjs": "^9.1.3",
|
||||||
"rollup-plugin-node-resolve": "^3.3.0",
|
"rollup-plugin-node-resolve": "^3.3.0",
|
||||||
"run-sequence": "^2.2.1",
|
"run-sequence": "^2.2.1",
|
||||||
"stylelint": "^9.2.0",
|
"stylelint": "^9.2.1",
|
||||||
"stylelint-config-prettier": "^3.2.0",
|
"stylelint-config-prettier": "^3.2.0",
|
||||||
"stylelint-config-recommended": "^2.1.0",
|
"stylelint-config-recommended": "^2.1.0",
|
||||||
"stylelint-config-sass-guidelines": "^5.0.0",
|
"stylelint-config-sass-guidelines": "^5.0.0",
|
||||||
@@ -62,6 +65,8 @@
|
|||||||
"doc": "readme.md"
|
"doc": "readme.md"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build": "gulp build",
|
||||||
|
"lint": "eslint src/js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "Sam Potts <sam@potts.es>",
|
"author": "Sam Potts <sam@potts.es>",
|
||||||
@@ -69,7 +74,7 @@
|
|||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"custom-event-polyfill": "^0.3.0",
|
"custom-event-polyfill": "^0.3.0",
|
||||||
"loadjs": "^3.5.4",
|
"loadjs": "^3.5.4",
|
||||||
"npm": "^6.0.0",
|
"raven-js": "^3.25.2",
|
||||||
"raven-js": "^3.24.2"
|
"url-polyfill": "^1.0.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
See [initialising](#initialising) for more information on advanced setups.
|
||||||
|
|
||||||
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following:
|
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="https://cdn.plyr.io/3.3.5/plyr.js"></script>
|
<script src="https://cdn.plyr.io/3.3.10/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.10/plyr.polyfilled.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
### CSS
|
### 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:
|
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.5/plyr.css">
|
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.10/plyr.css">
|
||||||
```
|
```
|
||||||
|
|
||||||
### SVG Sprite
|
### SVG Sprite
|
||||||
|
|
||||||
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
|
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
|
||||||
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.5/plyr.svg`.
|
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.10/plyr.svg`.
|
||||||
|
|
||||||
## Ads
|
## 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 [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
|
||||||
* A [jQuery](https://jquery.com) object
|
* 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
|
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'));
|
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
|
```javascript
|
||||||
const player = new Plyr(document.querySelectorAll('.js-player'));
|
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
|
##### Setting up multiple players
|
||||||
|
|
||||||
|
You have two choices here. You can either use a simple array loop to map the constructor:
|
||||||
|
|
||||||
```javascript
|
```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:
|
The second argument for the constructor is the [options](#options) object:
|
||||||
|
|
||||||
```javascript
|
```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
|
#### Options
|
||||||
|
|
||||||
@@ -287,7 +303,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
|||||||
| `invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter. |
|
| `invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter. |
|
||||||
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
|
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
|
||||||
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
|
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
|
||||||
| `captions` | Object | `{ active: false, language: window.navigator.language.split('-')[0] }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). |
|
| `captions` | Object | `{ active: false, language: 'auto', update: false }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). 'auto' uses the browser language. `update`: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in unselectable language options). |
|
||||||
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) |
|
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) |
|
||||||
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. |
|
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. |
|
||||||
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
|
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
|
||||||
@@ -345,7 +361,7 @@ player.fullscreen.enter(); // Enter fullscreen
|
|||||||
| `fullscreen.exit()` | - | Exit fullscreen. |
|
| `fullscreen.exit()` | - | Exit fullscreen. |
|
||||||
| `fullscreen.toggle()` | - | Toggle fullscreen. |
|
| `fullscreen.toggle()` | - | Toggle fullscreen. |
|
||||||
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
|
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
|
||||||
| `toggleControls(toggle)` | Boolean | Toggle the controls based on the specified boolean. |
|
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
|
||||||
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
|
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
|
||||||
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
|
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
|
||||||
| `supports(type)` | String | Check support for a mime type. |
|
| `supports(type)` | String | Check support for a mime type. |
|
||||||
|
|||||||
+38
-58
@@ -16,28 +16,6 @@ const captions = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default language if not set
|
|
||||||
const stored = this.storage.get('language');
|
|
||||||
|
|
||||||
if (!utils.is.empty(stored)) {
|
|
||||||
this.captions.language = stored;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utils.is.empty(this.captions.language)) {
|
|
||||||
this.captions.language = this.config.captions.language.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set captions enabled state if not set
|
|
||||||
if (!utils.is.boolean(this.captions.active)) {
|
|
||||||
const active = this.storage.get('captions');
|
|
||||||
|
|
||||||
if (utils.is.boolean(active)) {
|
|
||||||
this.captions.active = active;
|
|
||||||
} else {
|
|
||||||
this.captions.active = this.config.captions.active;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only Vimeo and HTML5 video supported at this point
|
// Only Vimeo and HTML5 video supported at this point
|
||||||
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
|
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
|
||||||
// Clear menu and hide
|
// Clear menu and hide
|
||||||
@@ -55,17 +33,6 @@ const captions = {
|
|||||||
utils.insertAfter(this.elements.captions, this.elements.wrapper);
|
utils.insertAfter(this.elements.captions, this.elements.wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the class hook
|
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this)));
|
|
||||||
|
|
||||||
// Get tracks
|
|
||||||
const tracks = captions.getTracks.call(this);
|
|
||||||
|
|
||||||
// If no caption file exists, hide container for caption text
|
|
||||||
if (utils.is.empty(tracks)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get browser info
|
// Get browser info
|
||||||
const browser = utils.getBrowser();
|
const browser = utils.getBrowser();
|
||||||
|
|
||||||
@@ -94,14 +61,45 @@ const captions = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set language
|
// Try to load the value from storage
|
||||||
captions.setLanguage.call(this);
|
let active = this.storage.get('captions');
|
||||||
|
|
||||||
// Enable UI
|
// Otherwise fall back to the default config
|
||||||
captions.show.call(this);
|
if (!utils.is.boolean(active)) {
|
||||||
|
({ active } = this.config.captions);
|
||||||
|
}
|
||||||
|
|
||||||
// Set available languages in list
|
// Set toggled state
|
||||||
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
|
this.toggleCaptions(active);
|
||||||
|
|
||||||
|
// Watch changes to textTracks and update captions menu
|
||||||
|
if (this.config.captions.update) {
|
||||||
|
utils.on(this.media.textTracks, 'addtrack removetrack', captions.update.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update available languages in list next tick (the event must not be triggered before the listeners)
|
||||||
|
setTimeout(captions.update.bind(this), 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
// Update tracks
|
||||||
|
const tracks = captions.getTracks.call(this);
|
||||||
|
this.options.captions = tracks.map(({language}) => language);
|
||||||
|
|
||||||
|
// Set language if it hasn't been set already
|
||||||
|
if (!this.language) {
|
||||||
|
let { language } = this.config.captions;
|
||||||
|
if (language === 'auto') {
|
||||||
|
[ language ] = (navigator.language || navigator.userLanguage).split('-');
|
||||||
|
}
|
||||||
|
this.language = this.storage.get('language') || (language || '').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the class hooks
|
||||||
|
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this)));
|
||||||
|
|
||||||
|
// Update available languages in list
|
||||||
|
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
|
||||||
controls.setCaptionsMenu.call(this);
|
controls.setCaptionsMenu.call(this);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -236,7 +234,7 @@ const captions = {
|
|||||||
|
|
||||||
// Set the span content
|
// Set the span content
|
||||||
if (utils.is.string(caption)) {
|
if (utils.is.string(caption)) {
|
||||||
content.textContent = caption.trim();
|
content.innerText = caption.trim();
|
||||||
} else {
|
} else {
|
||||||
content.appendChild(caption);
|
content.appendChild(caption);
|
||||||
}
|
}
|
||||||
@@ -247,24 +245,6 @@ const captions = {
|
|||||||
this.debug.warn('No captions element to render to');
|
this.debug.warn('No captions element to render to');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Display captions container and button (for initialization)
|
|
||||||
show() {
|
|
||||||
// Try to load the value from storage
|
|
||||||
let active = this.storage.get('captions');
|
|
||||||
|
|
||||||
// Otherwise fall back to the default config
|
|
||||||
if (!utils.is.boolean(active)) {
|
|
||||||
({ active } = this.config.captions);
|
|
||||||
} else {
|
|
||||||
this.captions.active = active;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (active) {
|
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true);
|
|
||||||
utils.toggleState(this.elements.buttons.captions, true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default captions;
|
export default captions;
|
||||||
|
|||||||
Vendored
+171
-52
@@ -6,34 +6,13 @@ import captions from './captions';
|
|||||||
import html5 from './html5';
|
import html5 from './html5';
|
||||||
import i18n from './i18n';
|
import i18n from './i18n';
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import ui from './ui';
|
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
|
|
||||||
// Sniff out the browser
|
// Sniff out the browser
|
||||||
const browser = utils.getBrowser();
|
const browser = utils.getBrowser();
|
||||||
|
|
||||||
const controls = {
|
const controls = {
|
||||||
// Webkit polyfill for lower fill range
|
|
||||||
updateRangeFill(target) {
|
|
||||||
// Get range from event if event passed
|
|
||||||
const range = utils.is.event(target) ? target.target : target;
|
|
||||||
|
|
||||||
// Needs to be a valid <input type='range'>
|
|
||||||
if (!utils.is.element(range) || range.getAttribute('type') !== 'range') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set aria value for https://github.com/sampotts/plyr/issues/905
|
|
||||||
range.setAttribute('aria-valuenow', range.value);
|
|
||||||
|
|
||||||
// WebKit only
|
|
||||||
if (!browser.isWebkit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set CSS custom property
|
|
||||||
range.style.setProperty('--value', `${range.value / range.max * 100}%`);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get icon URL
|
// Get icon URL
|
||||||
getIconUrl() {
|
getIconUrl() {
|
||||||
@@ -373,7 +352,7 @@ const controls = {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.textContent = `% ${suffix.toLowerCase()}`;
|
progress.innerText = `% ${suffix.toLowerCase()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.elements.display[type] = progress;
|
this.elements.display[type] = progress;
|
||||||
@@ -429,6 +408,124 @@ const controls = {
|
|||||||
list.appendChild(item);
|
list.appendChild(item);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Update the displayed time
|
||||||
|
updateTimeDisplay(target = null, time = 0, inverted = false) {
|
||||||
|
// Bail if there's no element to display or the value isn't a number
|
||||||
|
if (!utils.is.element(target) || !utils.is.number(time)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always display hours if duration is over an hour
|
||||||
|
const forceHours = utils.getHours(this.duration) > 0;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
target.innerText = utils.formatTime(time, forceHours, inverted);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update volume UI and storage
|
||||||
|
updateVolume() {
|
||||||
|
if (!this.supported.ui) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update range
|
||||||
|
if (utils.is.element(this.elements.inputs.volume)) {
|
||||||
|
controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update mute state
|
||||||
|
if (utils.is.element(this.elements.buttons.mute)) {
|
||||||
|
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update seek value and lower fill
|
||||||
|
setRange(target, value = 0) {
|
||||||
|
if (!utils.is.element(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
target.value = value;
|
||||||
|
|
||||||
|
// Webkit range fill
|
||||||
|
controls.updateRangeFill.call(this, target);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update <progress> elements
|
||||||
|
updateProgress(event) {
|
||||||
|
if (!this.supported.ui || !utils.is.event(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = 0;
|
||||||
|
|
||||||
|
const setProgress = (target, input) => {
|
||||||
|
const value = utils.is.number(input) ? input : 0;
|
||||||
|
const progress = utils.is.element(target) ? target : this.elements.display.buffer;
|
||||||
|
|
||||||
|
// Update value and label
|
||||||
|
if (utils.is.element(progress)) {
|
||||||
|
progress.value = value;
|
||||||
|
|
||||||
|
// Update text label inside
|
||||||
|
const label = progress.getElementsByTagName('span')[0];
|
||||||
|
if (utils.is.element(label)) {
|
||||||
|
label.childNodes[0].nodeValue = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
switch (event.type) {
|
||||||
|
// Video playing
|
||||||
|
case 'timeupdate':
|
||||||
|
case 'seeking':
|
||||||
|
case 'seeked':
|
||||||
|
value = utils.getPercentage(this.currentTime, this.duration);
|
||||||
|
|
||||||
|
// Set seek range value only if it's a 'natural' time event
|
||||||
|
if (event.type === 'timeupdate') {
|
||||||
|
controls.setRange.call(this, this.elements.inputs.seek, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Check buffer status
|
||||||
|
case 'playing':
|
||||||
|
case 'progress':
|
||||||
|
setProgress(this.elements.display.buffer, this.buffered * 100);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Webkit polyfill for lower fill range
|
||||||
|
updateRangeFill(target) {
|
||||||
|
// Get range from event if event passed
|
||||||
|
const range = utils.is.event(target) ? target.target : target;
|
||||||
|
|
||||||
|
// Needs to be a valid <input type='range'>
|
||||||
|
if (!utils.is.element(range) || range.getAttribute('type') !== 'range') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set aria value for https://github.com/sampotts/plyr/issues/905
|
||||||
|
range.setAttribute('aria-valuenow', range.value);
|
||||||
|
|
||||||
|
// WebKit only
|
||||||
|
if (!browser.isWebkit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set CSS custom property
|
||||||
|
range.style.setProperty('--value', `${range.value / range.max * 100}%`);
|
||||||
|
},
|
||||||
|
|
||||||
// Update hover tooltip for seeking
|
// Update hover tooltip for seeking
|
||||||
updateSeekTooltip(event) {
|
updateSeekTooltip(event) {
|
||||||
// Bail if setting not true
|
// Bail if setting not true
|
||||||
@@ -443,7 +540,7 @@ const controls = {
|
|||||||
|
|
||||||
// Calculate percentage
|
// Calculate percentage
|
||||||
let percent = 0;
|
let percent = 0;
|
||||||
const clientRect = this.elements.inputs.seek.getBoundingClientRect();
|
const clientRect = this.elements.progress.getBoundingClientRect();
|
||||||
const visible = `${this.config.classNames.tooltip}--visible`;
|
const visible = `${this.config.classNames.tooltip}--visible`;
|
||||||
|
|
||||||
const toggle = toggle => {
|
const toggle = toggle => {
|
||||||
@@ -473,7 +570,7 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display the time a click would seek to
|
// Display the time a click would seek to
|
||||||
ui.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent);
|
controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent);
|
||||||
|
|
||||||
// Set position
|
// Set position
|
||||||
this.elements.display.seekTooltip.style.left = `${percent}%`;
|
this.elements.display.seekTooltip.style.left = `${percent}%`;
|
||||||
@@ -488,6 +585,47 @@ const controls = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Handle time change event
|
||||||
|
timeUpdate(event) {
|
||||||
|
// Only invert if only one time element is displayed and used for both duration and currentTime
|
||||||
|
const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
|
||||||
|
|
||||||
|
// Duration
|
||||||
|
controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
|
||||||
|
|
||||||
|
// Ignore updates while seeking
|
||||||
|
if (event && event.type === 'timeupdate' && this.media.seeking) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playing progress
|
||||||
|
controls.updateProgress.call(this, event);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show the duration on metadataloaded or durationchange events
|
||||||
|
durationUpdate() {
|
||||||
|
// Bail if no ui or durationchange event triggered after playing/seek when invertTime is false
|
||||||
|
if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a spot to display duration
|
||||||
|
const hasDuration = utils.is.element(this.elements.display.duration);
|
||||||
|
|
||||||
|
// If there's only one time display, display duration there
|
||||||
|
if (!hasDuration && this.config.displayDuration && this.paused) {
|
||||||
|
controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a duration element, update content
|
||||||
|
if (hasDuration) {
|
||||||
|
controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the tooltip (if visible)
|
||||||
|
controls.updateSeekTooltip.call(this);
|
||||||
|
},
|
||||||
|
|
||||||
// Hide/show a tab
|
// Hide/show a tab
|
||||||
toggleTab(setting, toggle) {
|
toggleTab(setting, toggle) {
|
||||||
utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
|
utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
|
||||||
@@ -526,27 +664,7 @@ const controls = {
|
|||||||
|
|
||||||
// Get the badge HTML for HD, 4K etc
|
// Get the badge HTML for HD, 4K etc
|
||||||
const getBadge = quality => {
|
const getBadge = quality => {
|
||||||
let label = '';
|
const label = i18n.get(`qualityBadge.${quality}`, this.config);
|
||||||
|
|
||||||
switch (quality) {
|
|
||||||
case 2160:
|
|
||||||
label = '4K';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1440:
|
|
||||||
case 1080:
|
|
||||||
case 720:
|
|
||||||
label = 'HD';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 576:
|
|
||||||
case 480:
|
|
||||||
label = 'SD';
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!label.length) {
|
if (!label.length) {
|
||||||
return null;
|
return null;
|
||||||
@@ -570,7 +688,6 @@ const controls = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Translate a value into a nice label
|
// Translate a value into a nice label
|
||||||
// TODO: Localisation
|
|
||||||
getLabel(setting, value) {
|
getLabel(setting, value) {
|
||||||
switch (setting) {
|
switch (setting) {
|
||||||
case 'speed':
|
case 'speed':
|
||||||
@@ -578,7 +695,13 @@ const controls = {
|
|||||||
|
|
||||||
case 'quality':
|
case 'quality':
|
||||||
if (utils.is.number(value)) {
|
if (utils.is.number(value)) {
|
||||||
return `${value}p`;
|
const label = i18n.get(`qualityLabel.${value}`, this.config);
|
||||||
|
|
||||||
|
if (!label.length) {
|
||||||
|
return `${value}p`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.toTitleCase(value);
|
return utils.toTitleCase(value);
|
||||||
@@ -745,13 +868,10 @@ const controls = {
|
|||||||
'language',
|
'language',
|
||||||
track.label,
|
track.label,
|
||||||
track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null,
|
track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null,
|
||||||
track.language.toLowerCase() === this.captions.language.toLowerCase(),
|
track.language.toLowerCase() === this.language,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store reference
|
|
||||||
this.options.captions = tracks.map(track => track.language);
|
|
||||||
|
|
||||||
controls.updateSetting.call(this, type, list);
|
controls.updateSetting.call(this, type, list);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1027,7 +1147,6 @@ const controls = {
|
|||||||
const tooltip = utils.createElement(
|
const tooltip = utils.createElement(
|
||||||
'span',
|
'span',
|
||||||
{
|
{
|
||||||
role: 'tooltip',
|
|
||||||
class: this.config.classNames.tooltip,
|
class: this.config.classNames.tooltip,
|
||||||
},
|
},
|
||||||
'00:00',
|
'00:00',
|
||||||
|
|||||||
+24
-14
@@ -56,7 +56,7 @@ const defaults = {
|
|||||||
// Sprite (for icons)
|
// Sprite (for icons)
|
||||||
loadSprite: true,
|
loadSprite: true,
|
||||||
iconPrefix: 'plyr',
|
iconPrefix: 'plyr',
|
||||||
iconUrl: 'https://cdn.plyr.io/3.3.5/plyr.svg',
|
iconUrl: 'https://cdn.plyr.io/3.3.10/plyr.svg',
|
||||||
|
|
||||||
// Blank video (used to prevent errors on source change)
|
// Blank video (used to prevent errors on source change)
|
||||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||||
@@ -115,7 +115,10 @@ const defaults = {
|
|||||||
// Captions settings
|
// Captions settings
|
||||||
captions: {
|
captions: {
|
||||||
active: false,
|
active: false,
|
||||||
language: (navigator.language || navigator.userLanguage).split('-')[0],
|
language: 'auto',
|
||||||
|
// Listen to new tracks added after Plyr is initialized.
|
||||||
|
// This is needed for streaming captions, but may result in unselectable options
|
||||||
|
update: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fullscreen settings
|
// Fullscreen settings
|
||||||
@@ -157,10 +160,10 @@ const defaults = {
|
|||||||
// Localisation
|
// Localisation
|
||||||
i18n: {
|
i18n: {
|
||||||
restart: 'Restart',
|
restart: 'Restart',
|
||||||
rewind: 'Rewind {seektime} secs',
|
rewind: 'Rewind {seektime}s',
|
||||||
play: 'Play',
|
play: 'Play',
|
||||||
pause: 'Pause',
|
pause: 'Pause',
|
||||||
fastForward: 'Forward {seektime} secs',
|
fastForward: 'Forward {seektime}s',
|
||||||
seek: 'Seek',
|
seek: 'Seek',
|
||||||
played: 'Played',
|
played: 'Played',
|
||||||
buffered: 'Buffered',
|
buffered: 'Buffered',
|
||||||
@@ -187,6 +190,14 @@ const defaults = {
|
|||||||
disabled: 'Disabled',
|
disabled: 'Disabled',
|
||||||
enabled: 'Enabled',
|
enabled: 'Enabled',
|
||||||
advertisement: 'Ad',
|
advertisement: 'Ad',
|
||||||
|
qualityBadge: {
|
||||||
|
2160: '4K',
|
||||||
|
1440: 'HD',
|
||||||
|
1080: 'HD',
|
||||||
|
720: 'HD',
|
||||||
|
576: 'SD',
|
||||||
|
480: 'SD',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// URLs
|
// URLs
|
||||||
@@ -199,7 +210,6 @@ const defaults = {
|
|||||||
youtube: {
|
youtube: {
|
||||||
sdk: 'https://www.youtube.com/iframe_api',
|
sdk: 'https://www.youtube.com/iframe_api',
|
||||||
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
|
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
|
||||||
poster: 'https://img.youtube.com/vi/{0}/maxresdefault.jpg,https://img.youtube.com/vi/{0}/hqdefault.jpg',
|
|
||||||
},
|
},
|
||||||
googleIMA: {
|
googleIMA: {
|
||||||
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
||||||
@@ -312,9 +322,8 @@ const defaults = {
|
|||||||
display: {
|
display: {
|
||||||
currentTime: '.plyr__time--current',
|
currentTime: '.plyr__time--current',
|
||||||
duration: '.plyr__time--duration',
|
duration: '.plyr__time--duration',
|
||||||
buffer: '.plyr__progress--buffer',
|
buffer: '.plyr__progress__buffer',
|
||||||
played: '.plyr__progress--played',
|
loop: '.plyr__progress__loop', // Used later
|
||||||
loop: '.plyr__progress--loop',
|
|
||||||
volume: '.plyr__volume--display',
|
volume: '.plyr__volume--display',
|
||||||
},
|
},
|
||||||
progress: '.plyr__progress',
|
progress: '.plyr__progress',
|
||||||
@@ -326,18 +335,19 @@ const defaults = {
|
|||||||
|
|
||||||
// Class hooks added to the player in different states
|
// Class hooks added to the player in different states
|
||||||
classNames: {
|
classNames: {
|
||||||
video: 'plyr__video-wrapper',
|
|
||||||
embed: 'plyr__video-embed',
|
|
||||||
poster: 'plyr__poster',
|
|
||||||
ads: 'plyr__ads',
|
|
||||||
control: 'plyr__control',
|
|
||||||
type: 'plyr--{0}',
|
type: 'plyr--{0}',
|
||||||
provider: 'plyr--{0}',
|
provider: 'plyr--{0}',
|
||||||
|
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',
|
playing: 'plyr--playing',
|
||||||
paused: 'plyr--paused',
|
paused: 'plyr--paused',
|
||||||
stopped: 'plyr--stopped',
|
stopped: 'plyr--stopped',
|
||||||
loading: 'plyr--loading',
|
loading: 'plyr--loading',
|
||||||
error: 'plyr--has-error',
|
|
||||||
hover: 'plyr--hover',
|
hover: 'plyr--hover',
|
||||||
tooltip: 'plyr__tooltip',
|
tooltip: 'plyr__tooltip',
|
||||||
cues: 'plyr__cues',
|
cues: 'plyr__cues',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function onChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trigger an event
|
// Trigger an event
|
||||||
utils.dispatchEvent(this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
utils.dispatchEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
|
||||||
|
|
||||||
// Trap focus in container
|
// Trap focus in container
|
||||||
if (!browser.isIos) {
|
if (!browser.isIos) {
|
||||||
|
|||||||
+7
-3
@@ -99,6 +99,13 @@ const html5 = {
|
|||||||
// Set new source
|
// Set new source
|
||||||
player.media.src = supported[0].getAttribute('src');
|
player.media.src = supported[0].getAttribute('src');
|
||||||
|
|
||||||
|
// Restore time
|
||||||
|
const onLoadedMetaData = () => {
|
||||||
|
player.currentTime = currentTime;
|
||||||
|
player.off('loadedmetadata', onLoadedMetaData);
|
||||||
|
};
|
||||||
|
player.on('loadedmetadata', onLoadedMetaData);
|
||||||
|
|
||||||
// Load new source
|
// Load new source
|
||||||
player.media.load();
|
player.media.load();
|
||||||
|
|
||||||
@@ -107,9 +114,6 @@ const html5 = {
|
|||||||
player.play();
|
player.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore time
|
|
||||||
player.currentTime = currentTime;
|
|
||||||
|
|
||||||
// Trigger change event
|
// Trigger change event
|
||||||
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
|
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
|
||||||
quality: input,
|
quality: input,
|
||||||
|
|||||||
+6
-2
@@ -6,11 +6,15 @@ import utils from './utils';
|
|||||||
|
|
||||||
const i18n = {
|
const i18n = {
|
||||||
get(key = '', config = {}) {
|
get(key = '', config = {}) {
|
||||||
if (utils.is.empty(key) || utils.is.empty(config) || !Object.keys(config.i18n).includes(key)) {
|
if (utils.is.empty(key) || utils.is.empty(config)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
let string = config.i18n[key];
|
let string = utils.getDeep(config.i18n, key);
|
||||||
|
|
||||||
|
if (utils.is.empty(string)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
const replace = {
|
const replace = {
|
||||||
'{seektime}': config.seekTime,
|
'{seektime}': config.seekTime,
|
||||||
|
|||||||
+116
-34
@@ -238,22 +238,45 @@ class Listeners {
|
|||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle controls visibility based on mouse movement
|
// Toggle controls on mouse events and entering fullscreen
|
||||||
if (this.player.config.hideControls) {
|
utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {
|
||||||
// Toggle controls on mouse events and entering fullscreen
|
const { controls } = this.player.elements;
|
||||||
utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => {
|
|
||||||
this.player.toggleControls(event);
|
// Remove button states for fullscreen
|
||||||
});
|
if (event.type === 'enterfullscreen') {
|
||||||
}
|
controls.pressed = false;
|
||||||
|
controls.hover = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show, then hide after a timeout unless another control event occurs
|
||||||
|
const show = [
|
||||||
|
'touchstart',
|
||||||
|
'touchmove',
|
||||||
|
'mousemove',
|
||||||
|
].includes(event.type);
|
||||||
|
|
||||||
|
let delay = 0;
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
ui.toggleControls.call(this.player, true);
|
||||||
|
// Use longer timeout for touch devices
|
||||||
|
delay = this.player.touch ? 3000 : 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear timer
|
||||||
|
clearTimeout(this.player.timers.controls);
|
||||||
|
// Timer to prevent flicker when seeking
|
||||||
|
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for media events
|
// Listen for media events
|
||||||
media() {
|
media() {
|
||||||
// Time change on media
|
// Time change on media
|
||||||
utils.on(this.player.media, 'timeupdate seeking', event => ui.timeUpdate.call(this.player, event));
|
utils.on(this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event));
|
||||||
|
|
||||||
// Display duration
|
// Display duration
|
||||||
utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => ui.durationUpdate.call(this.player, event));
|
utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event));
|
||||||
|
|
||||||
// Check for audio tracks on load
|
// Check for audio tracks on load
|
||||||
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
|
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
|
||||||
@@ -272,10 +295,10 @@ class Listeners {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check for buffer progress
|
// Check for buffer progress
|
||||||
utils.on(this.player.media, 'progress playing', event => ui.updateProgress.call(this.player, event));
|
utils.on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event));
|
||||||
|
|
||||||
// Handle volume changes
|
// Handle volume changes
|
||||||
utils.on(this.player.media, 'volumechange', event => ui.updateVolume.call(this.player, event));
|
utils.on(this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event));
|
||||||
|
|
||||||
// Handle play/pause
|
// Handle play/pause
|
||||||
utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event));
|
utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event));
|
||||||
@@ -283,9 +306,6 @@ class Listeners {
|
|||||||
// Loading state
|
// Loading state
|
||||||
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
|
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
|
||||||
|
|
||||||
// Check if media failed to load
|
|
||||||
// utils.on(this.player.media, 'play', event => ui.checkFailed.call(this.player, event));
|
|
||||||
|
|
||||||
// If autoplay, then load advertisement if required
|
// If autoplay, then load advertisement if required
|
||||||
// TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
|
// TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
|
||||||
utils.on(this.player.media, 'playing', () => {
|
utils.on(this.player.media, 'playing', () => {
|
||||||
@@ -529,12 +549,54 @@ class Listeners {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set range input alternative "value", which matches the tooltip time (#954)
|
||||||
|
on(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
|
||||||
|
const clientRect = this.player.elements.progress.getBoundingClientRect();
|
||||||
|
const percent = 100 / clientRect.width * (event.pageX - clientRect.left);
|
||||||
|
event.currentTarget.setAttribute('seek-value', percent);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pause while seeking
|
||||||
|
on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
|
||||||
|
const seek = event.currentTarget;
|
||||||
|
|
||||||
|
// Was playing before?
|
||||||
|
const play = seek.hasAttribute('play-on-seeked');
|
||||||
|
|
||||||
|
// Done seeking
|
||||||
|
const done = [
|
||||||
|
'mouseup',
|
||||||
|
'touchend',
|
||||||
|
'keyup',
|
||||||
|
].includes(event.type);
|
||||||
|
|
||||||
|
// If we're done seeking and it was playing, resume playback
|
||||||
|
if (play && done) {
|
||||||
|
seek.removeAttribute('play-on-seeked');
|
||||||
|
this.player.play();
|
||||||
|
} else if (!done && this.player.playing) {
|
||||||
|
seek.setAttribute('play-on-seeked', '');
|
||||||
|
this.player.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Seek
|
// Seek
|
||||||
on(
|
on(
|
||||||
this.player.elements.inputs.seek,
|
this.player.elements.inputs.seek,
|
||||||
inputEvent,
|
inputEvent,
|
||||||
event => {
|
event => {
|
||||||
this.player.currentTime = event.target.value / event.target.max * this.player.duration;
|
const seek = event.currentTarget;
|
||||||
|
|
||||||
|
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
|
||||||
|
let seekTo = seek.getAttribute('seek-value');
|
||||||
|
|
||||||
|
if (utils.is.empty(seekTo)) {
|
||||||
|
seekTo = seek.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
seek.removeAttribute('seek-value');
|
||||||
|
|
||||||
|
this.player.currentTime = seekTo / seek.max * this.player.duration;
|
||||||
},
|
},
|
||||||
'seek',
|
'seek',
|
||||||
);
|
);
|
||||||
@@ -549,7 +611,8 @@ class Listeners {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.player.config.invertTime = !this.player.config.invertTime;
|
this.player.config.invertTime = !this.player.config.invertTime;
|
||||||
ui.timeUpdate.call(this.player);
|
|
||||||
|
controls.timeUpdate.call(this.player);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,26 +636,45 @@ class Listeners {
|
|||||||
// Seek tooltip
|
// Seek tooltip
|
||||||
on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event));
|
on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event));
|
||||||
|
|
||||||
// Toggle controls visibility based on mouse movement
|
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
|
||||||
if (this.player.config.hideControls) {
|
on(this.player.elements.controls, 'mouseenter mouseleave', event => {
|
||||||
// Watch for cursor over controls so they don't hide when trying to interact
|
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
|
||||||
on(this.player.elements.controls, 'mouseenter mouseleave', event => {
|
});
|
||||||
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Watch for cursor over controls so they don't hide when trying to interact
|
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
|
||||||
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
||||||
this.player.elements.controls.pressed = [
|
this.player.elements.controls.pressed = [
|
||||||
'mousedown',
|
'mousedown',
|
||||||
'touchstart',
|
'touchstart',
|
||||||
].includes(event.type);
|
].includes(event.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Focus in/out on controls
|
// Focus in/out on controls
|
||||||
on(this.player.elements.controls, 'focusin focusout', event => {
|
on(this.player.elements.controls, 'focusin focusout', event => {
|
||||||
this.player.toggleControls(event);
|
const { config, elements, timers } = this.player;
|
||||||
});
|
|
||||||
}
|
// Skip transition to prevent focus from scrolling the parent element
|
||||||
|
utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
|
||||||
|
|
||||||
|
// Toggle
|
||||||
|
ui.toggleControls.call(this.player, event.type === 'focusin');
|
||||||
|
|
||||||
|
// If focusin, hide again after delay
|
||||||
|
if (event.type === 'focusin') {
|
||||||
|
// Restore transition
|
||||||
|
setTimeout(() => {
|
||||||
|
utils.toggleClass(elements.controls, config.classNames.noTransition, false);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Delay a little more for keyboard users
|
||||||
|
const delay = this.touch ? 3000 : 4000;
|
||||||
|
|
||||||
|
// Clear timer
|
||||||
|
clearTimeout(timers.controls);
|
||||||
|
// Hide
|
||||||
|
timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Mouse wheel for volume
|
// Mouse wheel for volume
|
||||||
on(
|
on(
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ const media = {
|
|||||||
utils.wrap(this.media, this.elements.wrapper);
|
utils.wrap(this.media, this.elements.wrapper);
|
||||||
|
|
||||||
// Faux poster container
|
// Faux poster container
|
||||||
this.elements.poster = utils.createElement('span', {
|
this.elements.poster = utils.createElement('div', {
|
||||||
class: this.config.classNames.poster,
|
class: this.config.classNames.poster,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+39
-37
@@ -7,6 +7,14 @@ import controls from './../controls';
|
|||||||
import ui from './../ui';
|
import ui from './../ui';
|
||||||
import utils from './../utils';
|
import utils from './../utils';
|
||||||
|
|
||||||
|
// Set playback state and trigger change (only on actual change)
|
||||||
|
function assurePlaybackState(play) {
|
||||||
|
if (this.media.paused === play) {
|
||||||
|
this.media.paused = !play;
|
||||||
|
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const vimeo = {
|
const vimeo = {
|
||||||
setup() {
|
setup() {
|
||||||
// Add embed class for responsive
|
// Add embed class for responsive
|
||||||
@@ -53,6 +61,7 @@ const vimeo = {
|
|||||||
const options = {
|
const options = {
|
||||||
loop: player.config.loop.active,
|
loop: player.config.loop.active,
|
||||||
autoplay: player.autoplay,
|
autoplay: player.autoplay,
|
||||||
|
// muted: player.muted,
|
||||||
byline: false,
|
byline: false,
|
||||||
portrait: false,
|
portrait: false,
|
||||||
title: false,
|
title: false,
|
||||||
@@ -82,7 +91,7 @@ const vimeo = {
|
|||||||
iframe.setAttribute('allow', 'autoplay');
|
iframe.setAttribute('allow', 'autoplay');
|
||||||
|
|
||||||
// Inject the package
|
// Inject the package
|
||||||
const wrapper = utils.createElement('div');
|
const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer });
|
||||||
wrapper.appendChild(iframe);
|
wrapper.appendChild(iframe);
|
||||||
player.media = utils.replaceElement(wrapper, player.media);
|
player.media = utils.replaceElement(wrapper, player.media);
|
||||||
|
|
||||||
@@ -98,16 +107,16 @@ const vimeo = {
|
|||||||
// Get original image
|
// Get original image
|
||||||
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
|
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
|
||||||
|
|
||||||
// Set attribute
|
// Set and show poster
|
||||||
player.media.setAttribute('poster', url.href);
|
ui.setPoster.call(player, url.href);
|
||||||
|
|
||||||
// Update
|
|
||||||
ui.setPoster.call(player);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup instance
|
// Setup instance
|
||||||
// https://github.com/vimeo/player.js
|
// 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.paused = true;
|
||||||
player.media.currentTime = 0;
|
player.media.currentTime = 0;
|
||||||
@@ -119,15 +128,13 @@ const vimeo = {
|
|||||||
|
|
||||||
// Create a faux HTML5 API using the Vimeo API
|
// Create a faux HTML5 API using the Vimeo API
|
||||||
player.media.play = () => {
|
player.media.play = () => {
|
||||||
player.embed.play().then(() => {
|
assurePlaybackState.call(player, true);
|
||||||
player.media.paused = false;
|
return player.embed.play();
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
player.media.pause = () => {
|
player.media.pause = () => {
|
||||||
player.embed.pause().then(() => {
|
assurePlaybackState.call(player, false);
|
||||||
player.media.paused = true;
|
return player.embed.pause();
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
player.media.stop = () => {
|
player.media.stop = () => {
|
||||||
@@ -142,25 +149,26 @@ const vimeo = {
|
|||||||
return currentTime;
|
return currentTime;
|
||||||
},
|
},
|
||||||
set(time) {
|
set(time) {
|
||||||
// Get current paused state
|
// Vimeo will automatically play on seek if the video hasn't been played before
|
||||||
// Vimeo will automatically play on seek
|
|
||||||
const { paused } = player.media;
|
|
||||||
|
|
||||||
// Set seeking flag
|
// Get current paused state and volume etc
|
||||||
player.media.seeking = true;
|
const { embed, media, paused, volume } = player;
|
||||||
|
|
||||||
// Trigger seeking
|
// Set seeking state and trigger event
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeking');
|
media.seeking = true;
|
||||||
|
utils.dispatchEvent.call(player, media, 'seeking');
|
||||||
|
|
||||||
// Seek after events
|
// If paused, mute until seek is complete
|
||||||
player.embed.setCurrentTime(time).catch(() => {
|
Promise.resolve(paused && embed.setVolume(0))
|
||||||
// Do nothing
|
// Seek
|
||||||
});
|
.then(() => embed.setCurrentTime(time))
|
||||||
|
// Restore paused
|
||||||
// Restore pause state
|
.then(() => paused && embed.pause())
|
||||||
if (paused) {
|
// Restore volume
|
||||||
player.pause();
|
.then(() => paused && embed.setVolume(volume))
|
||||||
}
|
.catch(() => {
|
||||||
|
// Do nothing
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -314,17 +322,12 @@ const vimeo = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('play', () => {
|
player.embed.on('play', () => {
|
||||||
// Only fire play if paused before
|
assurePlaybackState.call(player, true);
|
||||||
if (player.media.paused) {
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'play');
|
|
||||||
}
|
|
||||||
player.media.paused = false;
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'playing');
|
utils.dispatchEvent.call(player, player.media, 'playing');
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('pause', () => {
|
player.embed.on('pause', () => {
|
||||||
player.media.paused = true;
|
assurePlaybackState.call(player, false);
|
||||||
utils.dispatchEvent.call(player, player.media, 'pause');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('timeupdate', data => {
|
player.embed.on('timeupdate', data => {
|
||||||
@@ -355,7 +358,6 @@ const vimeo = {
|
|||||||
player.embed.on('seeked', () => {
|
player.embed.on('seeked', () => {
|
||||||
player.media.seeking = false;
|
player.media.seeking = false;
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeked');
|
utils.dispatchEvent.call(player, player.media, 'seeked');
|
||||||
utils.dispatchEvent.call(player, player.media, 'play');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('ended', () => {
|
player.embed.on('ended', () => {
|
||||||
|
|||||||
+67
-42
@@ -64,6 +64,14 @@ function mapQualityUnits(levels) {
|
|||||||
return utils.dedupe(levels.map(level => mapQualityUnit(level)));
|
return utils.dedupe(levels.map(level => mapQualityUnit(level)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set playback state and trigger change (only on actual change)
|
||||||
|
function assurePlaybackState(play) {
|
||||||
|
if (this.media.paused === play) {
|
||||||
|
this.media.paused = !play;
|
||||||
|
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const youtube = {
|
const youtube = {
|
||||||
setup() {
|
setup() {
|
||||||
// Add embed class for responsive
|
// Add embed class for responsive
|
||||||
@@ -162,7 +170,19 @@ const youtube = {
|
|||||||
player.media = utils.replaceElement(container, player.media);
|
player.media = utils.replaceElement(container, player.media);
|
||||||
|
|
||||||
// Set poster image
|
// Set poster image
|
||||||
player.media.setAttribute('poster', utils.format(player.config.urls.youtube.poster, videoId));
|
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
|
||||||
|
|
||||||
|
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
|
||||||
|
utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
|
||||||
|
.catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
|
||||||
|
.catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
|
||||||
|
.then(image => ui.setPoster.call(player, image.src))
|
||||||
|
.then(posterSrc => {
|
||||||
|
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
|
||||||
|
if (!posterSrc.includes('maxres')) {
|
||||||
|
player.elements.poster.style.backgroundSize = 'cover';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Setup instance
|
// Setup instance
|
||||||
// https://developers.google.com/youtube/iframe_api_reference
|
// https://developers.google.com/youtube/iframe_api_reference
|
||||||
@@ -252,10 +272,12 @@ const youtube = {
|
|||||||
|
|
||||||
// Create a faux HTML5 API using the YouTube API
|
// Create a faux HTML5 API using the YouTube API
|
||||||
player.media.play = () => {
|
player.media.play = () => {
|
||||||
|
assurePlaybackState.call(player, true);
|
||||||
instance.playVideo();
|
instance.playVideo();
|
||||||
};
|
};
|
||||||
|
|
||||||
player.media.pause = () => {
|
player.media.pause = () => {
|
||||||
|
assurePlaybackState.call(player, false);
|
||||||
instance.pauseVideo();
|
instance.pauseVideo();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -273,22 +295,17 @@ const youtube = {
|
|||||||
return Number(instance.getCurrentTime());
|
return Number(instance.getCurrentTime());
|
||||||
},
|
},
|
||||||
set(time) {
|
set(time) {
|
||||||
// Vimeo will automatically play on seek
|
// If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
|
||||||
const { paused } = player.media;
|
if (player.paused) {
|
||||||
|
player.embed.mute();
|
||||||
|
}
|
||||||
|
|
||||||
// Set seeking flag
|
// Set seeking state and trigger event
|
||||||
player.media.seeking = true;
|
player.media.seeking = true;
|
||||||
|
|
||||||
// Trigger seeking
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeking');
|
utils.dispatchEvent.call(player, player.media, 'seeking');
|
||||||
|
|
||||||
// Seek after events sent
|
// Seek after events sent
|
||||||
instance.seekTo(time);
|
instance.seekTo(time);
|
||||||
|
|
||||||
// Restore pause state
|
|
||||||
if (paused) {
|
|
||||||
player.pause();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -407,6 +424,17 @@ const youtube = {
|
|||||||
// Reset timer
|
// Reset timer
|
||||||
clearInterval(player.timers.playing);
|
clearInterval(player.timers.playing);
|
||||||
|
|
||||||
|
const seeked = player.media.seeking && [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
].includes(event.data);
|
||||||
|
|
||||||
|
if (seeked) {
|
||||||
|
// Unset seeking and fire seeked event
|
||||||
|
player.media.seeking = false;
|
||||||
|
utils.dispatchEvent.call(player, player.media, 'seeked');
|
||||||
|
}
|
||||||
|
|
||||||
// Handle events
|
// Handle events
|
||||||
// -1 Unstarted
|
// -1 Unstarted
|
||||||
// 0 Ended
|
// 0 Ended
|
||||||
@@ -426,7 +454,7 @@ const youtube = {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 0:
|
case 0:
|
||||||
player.media.paused = true;
|
assurePlaybackState.call(player, false);
|
||||||
|
|
||||||
// YouTube doesn't support loop for a single video, so mimick it.
|
// YouTube doesn't support loop for a single video, so mimick it.
|
||||||
if (player.media.loop) {
|
if (player.media.loop) {
|
||||||
@@ -440,42 +468,39 @@ const youtube = {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
// If we were seeking, fire seeked event
|
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
|
||||||
if (player.media.seeking) {
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeked');
|
|
||||||
}
|
|
||||||
player.media.seeking = false;
|
|
||||||
|
|
||||||
// Only fire play if paused before
|
|
||||||
if (player.media.paused) {
|
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;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
player.media.paused = true;
|
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
|
||||||
|
if (!player.muted) {
|
||||||
utils.dispatchEvent.call(player, player.media, 'pause');
|
player.embed.unMute();
|
||||||
|
}
|
||||||
|
assurePlaybackState.call(player, false);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
+65
-139
@@ -1,6 +1,6 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Plyr
|
// Plyr
|
||||||
// plyr.js v3.3.5
|
// plyr.js v3.3.10
|
||||||
// https://github.com/sampotts/plyr
|
// https://github.com/sampotts/plyr
|
||||||
// License: The MIT License (MIT)
|
// License: The MIT License (MIT)
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -55,6 +55,7 @@ class Plyr {
|
|||||||
this.config = utils.extend(
|
this.config = utils.extend(
|
||||||
{},
|
{},
|
||||||
defaults,
|
defaults,
|
||||||
|
Plyr.defaults,
|
||||||
options || {},
|
options || {},
|
||||||
(() => {
|
(() => {
|
||||||
try {
|
try {
|
||||||
@@ -431,21 +432,16 @@ class Plyr {
|
|||||||
* @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
|
* @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
|
||||||
*/
|
*/
|
||||||
set currentTime(input) {
|
set currentTime(input) {
|
||||||
let targetTime = 0;
|
// Bail if media duration isn't available yet
|
||||||
|
if (!this.duration) {
|
||||||
if (utils.is.number(input)) {
|
return;
|
||||||
targetTime = input;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalise targetTime
|
// Validate input
|
||||||
if (targetTime < 0) {
|
const inputIsValid = utils.is.number(input) && input > 0;
|
||||||
targetTime = 0;
|
|
||||||
} else if (targetTime > this.duration) {
|
|
||||||
targetTime = this.duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set
|
// Set
|
||||||
this.media.currentTime = targetTime;
|
this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
this.debug.log(`Seeking to ${this.currentTime} seconds`);
|
this.debug.log(`Seeking to ${this.currentTime} seconds`);
|
||||||
@@ -493,11 +489,11 @@ class Plyr {
|
|||||||
// Faux duration set via config
|
// Faux duration set via config
|
||||||
const fauxDuration = parseFloat(this.config.duration);
|
const fauxDuration = parseFloat(this.config.duration);
|
||||||
|
|
||||||
// True duration
|
// Media duration can be NaN before the media has loaded
|
||||||
const realDuration = this.media ? Number(this.media.duration) : 0;
|
const duration = (this.media || {}).duration || 0;
|
||||||
|
|
||||||
// If custom duration is funky, use regular duration
|
// If config duration is funky, use regular duration
|
||||||
return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration;
|
return fauxDuration || duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -679,7 +675,7 @@ class Plyr {
|
|||||||
quality = Number(input);
|
quality = Number(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!utils.is.number(quality) || quality === 0) {
|
if (!utils.is.number(quality)) {
|
||||||
quality = this.storage.get('quality');
|
quality = this.storage.get('quality');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,10 +797,7 @@ class Plyr {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (utils.is.string(input)) {
|
ui.setPoster.call(this, input);
|
||||||
this.media.setAttribute('poster', input);
|
|
||||||
ui.setPoster.call(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -845,24 +838,19 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the method is called without parameter, toggle based on current value
|
// If the method is called without parameter, toggle based on current value
|
||||||
const show = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
|
const active = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
|
||||||
|
|
||||||
// Nothing to change...
|
|
||||||
if (this.captions.active === show) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set global
|
|
||||||
this.captions.active = show;
|
|
||||||
|
|
||||||
// Toggle state
|
// Toggle state
|
||||||
utils.toggleState(this.elements.buttons.captions, this.captions.active);
|
utils.toggleState(this.elements.buttons.captions, active);
|
||||||
|
|
||||||
// Add class hook
|
// Add class hook
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, this.captions.active);
|
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, active);
|
||||||
|
|
||||||
// Trigger an event
|
// Update state and trigger event
|
||||||
utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
|
if (active !== this.captions.active) {
|
||||||
|
this.captions.active = active;
|
||||||
|
utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -970,119 +958,32 @@ class Plyr {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle the player controls
|
* Toggle the player controls
|
||||||
* @param {boolean} toggle - Whether to show the controls
|
* @param {boolean} [toggle] - Whether to show the controls
|
||||||
*/
|
*/
|
||||||
toggleControls(toggle) {
|
toggleControls(toggle) {
|
||||||
// We need controls of course...
|
// Don't toggle if missing UI support or if it's audio
|
||||||
if (!utils.is.element(this.elements.controls)) {
|
if (this.supported.ui && !this.isAudio) {
|
||||||
return;
|
// Get state before change
|
||||||
}
|
const isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
|
||||||
|
|
||||||
// Don't hide if no UI support or it's audio
|
// Negate the argument if not undefined since adding the class to hides the controls
|
||||||
if (!this.supported.ui || this.isAudio) {
|
const force = typeof toggle === 'undefined' ? undefined : !toggle;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let delay = 0;
|
// Apply and get updated state
|
||||||
let show = toggle;
|
const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
|
||||||
let isEnterFullscreen = false;
|
|
||||||
|
|
||||||
// Get toggle state if not set
|
// Close menu
|
||||||
if (!utils.is.boolean(toggle)) {
|
if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
||||||
if (utils.is.event(toggle)) {
|
controls.toggleMenu.call(this, false);
|
||||||
// Is the enter fullscreen event
|
|
||||||
isEnterFullscreen = toggle.type === 'enterfullscreen';
|
|
||||||
|
|
||||||
// Events that show the controls
|
|
||||||
const showEvents = [
|
|
||||||
'touchstart',
|
|
||||||
'touchmove',
|
|
||||||
'mouseenter',
|
|
||||||
'mousemove',
|
|
||||||
'focusin',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Events that delay hiding
|
|
||||||
const delayEvents = [
|
|
||||||
'touchmove',
|
|
||||||
'touchend',
|
|
||||||
'mousemove',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Whether to show controls
|
|
||||||
show = showEvents.includes(toggle.type);
|
|
||||||
|
|
||||||
// Delay hiding on move events
|
|
||||||
if (delayEvents.includes(toggle.type)) {
|
|
||||||
delay = 2000;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delay a little more for keyboard users
|
|
||||||
if (!this.touch && toggle.type === 'focusin') {
|
|
||||||
delay = 3000;
|
|
||||||
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
show = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
|
|
||||||
}
|
}
|
||||||
}
|
// Trigger event on change
|
||||||
|
if (hiding !== isHidden) {
|
||||||
// Clear timer on every call
|
const eventName = hiding ? 'controlshidden' : 'controlsshown';
|
||||||
clearTimeout(this.timers.controls);
|
utils.dispatchEvent.call(this, this.media, eventName);
|
||||||
|
|
||||||
// If the mouse is not over the controls, set a timeout to hide them
|
|
||||||
if (show || this.paused || this.loading) {
|
|
||||||
// Check if controls toggled
|
|
||||||
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false);
|
|
||||||
|
|
||||||
// Trigger event
|
|
||||||
if (toggled) {
|
|
||||||
utils.dispatchEvent.call(this, this.media, 'controlsshown');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always show controls when paused or if touch
|
|
||||||
if (this.paused || this.loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delay for hiding on touch
|
|
||||||
if (this.touch) {
|
|
||||||
delay = 3000;
|
|
||||||
}
|
}
|
||||||
|
return !hiding;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
// If toggle is false or if we're playing (regardless of toggle),
|
|
||||||
// then set the timer to hide the controls
|
|
||||||
if (!show || this.playing) {
|
|
||||||
this.timers.controls = setTimeout(() => {
|
|
||||||
// We need controls of course...
|
|
||||||
if (!utils.is.element(this.elements.controls)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the mouse is over the controls (and not entering fullscreen), bail
|
|
||||||
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore transition behaviour
|
|
||||||
if (!utils.hasClass(this.elements.container, this.config.classNames.hideControls)) {
|
|
||||||
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set hideControls class
|
|
||||||
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, this.config.hideControls);
|
|
||||||
|
|
||||||
// Trigger event and close menu
|
|
||||||
if (toggled) {
|
|
||||||
utils.dispatchEvent.call(this, this.media, 'controlshidden');
|
|
||||||
|
|
||||||
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
|
||||||
controls.toggleMenu.call(this, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, delay);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1244,6 +1145,31 @@ class Plyr {
|
|||||||
static loadSprite(url, id) {
|
static loadSprite(url, id) {
|
||||||
return utils.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;
|
export default Plyr;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Plyr Polyfilled Build
|
// Plyr Polyfilled Build
|
||||||
// plyr.js v3.3.5
|
// plyr.js v3.3.10
|
||||||
// https://github.com/sampotts/plyr
|
// https://github.com/sampotts/plyr
|
||||||
// License: The MIT License (MIT)
|
// License: The MIT License (MIT)
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import 'babel-polyfill';
|
import 'babel-polyfill';
|
||||||
import 'custom-event-polyfill';
|
import 'custom-event-polyfill';
|
||||||
|
import 'url-polyfill';
|
||||||
import Plyr from './plyr';
|
import Plyr from './plyr';
|
||||||
|
|
||||||
export default Plyr;
|
export default Plyr;
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(key) {
|
get(key) {
|
||||||
if (!Storage.supported) {
|
if (!Storage.supported || !this.enabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ const support = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
window.addEventListener('test', null, options);
|
window.addEventListener('test', null, options);
|
||||||
|
window.removeEventListener('test', null, options);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-172
@@ -55,8 +55,10 @@ const ui = {
|
|||||||
// Remove native controls
|
// Remove native controls
|
||||||
ui.toggleNativeControls.call(this);
|
ui.toggleNativeControls.call(this);
|
||||||
|
|
||||||
// Captions
|
// Setup captions for HTML5
|
||||||
captions.setup.call(this);
|
if (this.isHTML5) {
|
||||||
|
captions.setup.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
// Reset volume
|
// Reset volume
|
||||||
this.volume = null;
|
this.volume = null;
|
||||||
@@ -74,10 +76,10 @@ const ui = {
|
|||||||
this.quality = null;
|
this.quality = null;
|
||||||
|
|
||||||
// Reset volume display
|
// Reset volume display
|
||||||
ui.updateVolume.call(this);
|
controls.updateVolume.call(this);
|
||||||
|
|
||||||
// Reset time display
|
// Reset time display
|
||||||
ui.timeUpdate.call(this);
|
controls.timeUpdate.call(this);
|
||||||
|
|
||||||
// Update the UI
|
// Update the UI
|
||||||
ui.checkPlaying.call(this);
|
ui.checkPlaying.call(this);
|
||||||
@@ -105,8 +107,16 @@ const ui = {
|
|||||||
// Set the title
|
// Set the title
|
||||||
ui.setTitle.call(this);
|
ui.setTitle.call(this);
|
||||||
|
|
||||||
// Set the poster image
|
// Assure the poster image is set, if the property was added before the element was created
|
||||||
ui.setPoster.call(this);
|
if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) {
|
||||||
|
ui.setPoster.call(this, this.poster);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually set the duration if user has overridden it.
|
||||||
|
// The event listeners for it doesn't get called if preload is disabled (#701)
|
||||||
|
if (this.config.duration) {
|
||||||
|
controls.durationUpdate.call(this);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Setup aria attribute for play and iframe title
|
// Setup aria attribute for play and iframe title
|
||||||
@@ -146,15 +156,39 @@ const ui = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set the poster image
|
// Toggle poster
|
||||||
setPoster() {
|
togglePoster(enable) {
|
||||||
if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) {
|
utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
|
||||||
return;
|
},
|
||||||
|
|
||||||
|
// Set the poster image (async)
|
||||||
|
setPoster(poster) {
|
||||||
|
// Set property regardless of validity
|
||||||
|
this.media.setAttribute('poster', poster);
|
||||||
|
|
||||||
|
// Bail if element is missing
|
||||||
|
if (!utils.is.element(this.elements.poster)) {
|
||||||
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the inline style
|
// Load the image, and set poster if successful
|
||||||
const posters = this.poster.split(',');
|
const loadPromise = utils.loadImage(poster)
|
||||||
this.elements.poster.style.backgroundImage = posters.map(p => `url('${p}')`).join(',');
|
.then(() => {
|
||||||
|
this.elements.poster.style.backgroundImage = `url('${poster}')`;
|
||||||
|
Object.assign(this.elements.poster.style, {
|
||||||
|
backgroundImage: `url('${poster}')`,
|
||||||
|
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
|
||||||
|
backgroundSize: '',
|
||||||
|
});
|
||||||
|
ui.togglePoster.call(this, true);
|
||||||
|
return poster;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video)
|
||||||
|
loadPromise.catch(() => ui.togglePoster.call(this, false));
|
||||||
|
|
||||||
|
// Return the promise so the caller can use it as well
|
||||||
|
return loadPromise;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check playing state
|
// Check playing state
|
||||||
@@ -173,7 +207,7 @@ const ui = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Toggle controls
|
// Toggle controls
|
||||||
this.toggleControls(!this.playing);
|
ui.toggleControls.call(this);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check if media is loading
|
// Check if media is loading
|
||||||
@@ -188,171 +222,22 @@ const ui = {
|
|||||||
|
|
||||||
// Timer to prevent flicker when seeking
|
// Timer to prevent flicker when seeking
|
||||||
this.timers.loading = setTimeout(() => {
|
this.timers.loading = setTimeout(() => {
|
||||||
// Toggle container class hook
|
// Update progress bar loading class state
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||||
|
|
||||||
// Show controls if loading, hide if done
|
// Update controls visibility
|
||||||
this.toggleControls(this.loading);
|
ui.toggleControls.call(this);
|
||||||
}, this.loading ? 250 : 0);
|
}, this.loading ? 250 : 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check if media failed to load
|
// Toggle controls based on state and `force` argument
|
||||||
checkFailed() {
|
toggleControls(force) {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState
|
const { controls } = this.elements;
|
||||||
this.failed = this.media.networkState === 3;
|
|
||||||
|
|
||||||
if (this.failed) {
|
if (controls && this.config.hideControls) {
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, false);
|
// Show controls if force, loading, paused, or button interaction, otherwise hide
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.error, true);
|
this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear timer
|
|
||||||
clearTimeout(this.timers.failed);
|
|
||||||
|
|
||||||
// Timer to prevent flicker when seeking
|
|
||||||
this.timers.loading = setTimeout(() => {
|
|
||||||
// Toggle container class hook
|
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
|
||||||
|
|
||||||
// Show controls if loading, hide if done
|
|
||||||
this.toggleControls(this.loading);
|
|
||||||
}, this.loading ? 250 : 0);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update volume UI and storage
|
|
||||||
updateVolume() {
|
|
||||||
if (!this.supported.ui) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update range
|
|
||||||
if (utils.is.element(this.elements.inputs.volume)) {
|
|
||||||
ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update mute state
|
|
||||||
if (utils.is.element(this.elements.buttons.mute)) {
|
|
||||||
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update seek value and lower fill
|
|
||||||
setRange(target, value = 0) {
|
|
||||||
if (!utils.is.element(target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
target.value = value;
|
|
||||||
|
|
||||||
// Webkit range fill
|
|
||||||
controls.updateRangeFill.call(this, target);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Set <progress> value
|
|
||||||
setProgress(target, input) {
|
|
||||||
const value = utils.is.number(input) ? input : 0;
|
|
||||||
const progress = utils.is.element(target) ? target : this.elements.display.buffer;
|
|
||||||
|
|
||||||
// Update value and label
|
|
||||||
if (utils.is.element(progress)) {
|
|
||||||
progress.value = value;
|
|
||||||
|
|
||||||
// Update text label inside
|
|
||||||
const label = progress.getElementsByTagName('span')[0];
|
|
||||||
if (utils.is.element(label)) {
|
|
||||||
label.childNodes[0].nodeValue = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update <progress> elements
|
|
||||||
updateProgress(event) {
|
|
||||||
if (!this.supported.ui || !utils.is.event(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = 0;
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
switch (event.type) {
|
|
||||||
// Video playing
|
|
||||||
case 'timeupdate':
|
|
||||||
case 'seeking':
|
|
||||||
value = utils.getPercentage(this.currentTime, this.duration);
|
|
||||||
|
|
||||||
// Set seek range value only if it's a 'natural' time event
|
|
||||||
if (event.type === 'timeupdate') {
|
|
||||||
ui.setRange.call(this, this.elements.inputs.seek, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Check buffer status
|
|
||||||
case 'playing':
|
|
||||||
case 'progress':
|
|
||||||
ui.setProgress.call(this, this.elements.display.buffer, this.buffered * 100);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update the displayed time
|
|
||||||
updateTimeDisplay(target = null, time = 0, inverted = false) {
|
|
||||||
// Bail if there's no element to display or the value isn't a number
|
|
||||||
if (!utils.is.element(target) || !utils.is.number(time)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always display hours if duration is over an hour
|
|
||||||
const forceHours = utils.getHours(this.duration) > 0;
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
target.textContent = utils.formatTime(time, forceHours, inverted);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Handle time change event
|
|
||||||
timeUpdate(event) {
|
|
||||||
// Only invert if only one time element is displayed and used for both duration and currentTime
|
|
||||||
const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
|
|
||||||
|
|
||||||
// Duration
|
|
||||||
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
|
|
||||||
|
|
||||||
// Ignore updates while seeking
|
|
||||||
if (event && event.type === 'timeupdate' && this.media.seeking) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Playing progress
|
|
||||||
ui.updateProgress.call(this, event);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Show the duration on metadataloaded
|
|
||||||
durationUpdate() {
|
|
||||||
if (!this.supported.ui) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's a spot to display duration
|
|
||||||
const hasDuration = utils.is.element(this.elements.display.duration);
|
|
||||||
|
|
||||||
// If there's only one time display, display duration there
|
|
||||||
if (!hasDuration && this.config.displayDuration && this.paused) {
|
|
||||||
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's a duration element, update content
|
|
||||||
if (hasDuration) {
|
|
||||||
ui.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the tooltip (if visible)
|
|
||||||
controls.updateSeekTooltip.call(this);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+46
-19
@@ -3,15 +3,13 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import loadjs from 'loadjs';
|
import loadjs from 'loadjs';
|
||||||
|
import Storage from './storage';
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import { providers } from './types';
|
import { providers } from './types';
|
||||||
|
|
||||||
const utils = {
|
const utils = {
|
||||||
// Check variable types
|
// Check variable types
|
||||||
is: {
|
is: {
|
||||||
plyr(input) {
|
|
||||||
return this.instanceof(input, window.Plyr);
|
|
||||||
},
|
|
||||||
object(input) {
|
object(input) {
|
||||||
return this.getConstructor(input) === Object;
|
return this.getConstructor(input) === Object;
|
||||||
},
|
},
|
||||||
@@ -31,19 +29,19 @@ const utils = {
|
|||||||
return !this.nullOrUndefined(input) && Array.isArray(input);
|
return !this.nullOrUndefined(input) && Array.isArray(input);
|
||||||
},
|
},
|
||||||
weakMap(input) {
|
weakMap(input) {
|
||||||
return this.instanceof(input, window.WeakMap);
|
return this.instanceof(input, WeakMap);
|
||||||
},
|
},
|
||||||
nodeList(input) {
|
nodeList(input) {
|
||||||
return this.instanceof(input, window.NodeList);
|
return this.instanceof(input, NodeList);
|
||||||
},
|
},
|
||||||
element(input) {
|
element(input) {
|
||||||
return this.instanceof(input, window.Element);
|
return this.instanceof(input, Element);
|
||||||
},
|
},
|
||||||
textNode(input) {
|
textNode(input) {
|
||||||
return this.getConstructor(input) === Text;
|
return this.getConstructor(input) === Text;
|
||||||
},
|
},
|
||||||
event(input) {
|
event(input) {
|
||||||
return this.instanceof(input, window.Event);
|
return this.instanceof(input, Event);
|
||||||
},
|
},
|
||||||
cue(input) {
|
cue(input) {
|
||||||
return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue);
|
return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue);
|
||||||
@@ -122,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
|
// Load an external script
|
||||||
loadScript(url) {
|
loadScript(url) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -159,6 +172,8 @@ const utils = {
|
|||||||
|
|
||||||
// Only load once if ID set
|
// Only load once if ID set
|
||||||
if (!hasId || !exists()) {
|
if (!hasId || !exists()) {
|
||||||
|
const useStorage = Storage.supported;
|
||||||
|
|
||||||
// Create container
|
// Create container
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
utils.toggleHidden(container, true);
|
utils.toggleHidden(container, true);
|
||||||
@@ -168,7 +183,7 @@ const utils = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check in cache
|
// Check in cache
|
||||||
if (support.storage) {
|
if (useStorage) {
|
||||||
const cached = window.localStorage.getItem(prefix + id);
|
const cached = window.localStorage.getItem(prefix + id);
|
||||||
isCached = cached !== null;
|
isCached = cached !== null;
|
||||||
|
|
||||||
@@ -187,7 +202,7 @@ const utils = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (support.storage) {
|
if (useStorage) {
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
prefix + id,
|
prefix + id,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -250,7 +265,7 @@ const utils = {
|
|||||||
|
|
||||||
// Add text node
|
// Add text node
|
||||||
if (utils.is.string(text)) {
|
if (utils.is.string(text)) {
|
||||||
element.textContent = text;
|
element.innerText = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return built element
|
// Return built element
|
||||||
@@ -393,14 +408,16 @@ const utils = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Toggle class on an element
|
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
|
||||||
toggleClass(element, className, toggle) {
|
toggleClass(element, className, force) {
|
||||||
if (utils.is.element(element)) {
|
if (utils.is.element(element)) {
|
||||||
const contains = element.classList.contains(className);
|
let method = 'toggle';
|
||||||
|
if (typeof force !== 'undefined') {
|
||||||
|
method = force ? 'add' : 'remove';
|
||||||
|
}
|
||||||
|
|
||||||
element.classList[toggle ? 'add' : 'remove'](className);
|
element.classList[method](className);
|
||||||
|
return element.classList.contains(className);
|
||||||
return (toggle && !contains) || (!toggle && contains);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -547,7 +564,7 @@ const utils = {
|
|||||||
const event = new CustomEvent(type, {
|
const event = new CustomEvent(type, {
|
||||||
bubbles,
|
bubbles,
|
||||||
detail: Object.assign({}, detail, {
|
detail: Object.assign({}, detail, {
|
||||||
plyr: utils.is.plyr(this) ? this : null,
|
plyr: this,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -583,7 +600,7 @@ const utils = {
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
return input.toString().replace(/{(\d+)}/g, (match, i) => utils.is.string(args[i]) ? args[i] : '');
|
return input.toString().replace(/{(\d+)}/g, (match, i) => (utils.is.string(args[i]) ? args[i] : ''));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get percentage
|
// Get percentage
|
||||||
@@ -706,6 +723,16 @@ const utils = {
|
|||||||
return array.filter((item, index) => array.indexOf(item) === index);
|
return array.filter((item, index) => array.indexOf(item) === index);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Clone nested objects
|
||||||
|
cloneDeep(object) {
|
||||||
|
return JSON.parse(JSON.stringify(object));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get a nested value in an object
|
||||||
|
getDeep(object, path) {
|
||||||
|
return path.split('.').reduce((obj, key) => obj && obj[key], object);
|
||||||
|
},
|
||||||
|
|
||||||
// Get the closest value in an array
|
// Get the closest value in an array
|
||||||
closest(array, value) {
|
closest(array, value) {
|
||||||
if (!utils.is.array(array) || !array.length) {
|
if (!utils.is.array(array) || !array.length) {
|
||||||
@@ -723,7 +750,7 @@ const utils = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vimeo
|
// 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;
|
return providers.vimeo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ $embed-padding: ((100 / 16) * 9);
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vimeo hack
|
// Only used for Vimeo
|
||||||
> div {
|
> .plyr__video-embed__container {
|
||||||
padding-bottom: to-percentage($height);
|
padding-bottom: to-percentage($height);
|
||||||
position: relative;
|
position: relative;
|
||||||
transform: translateY(-$offset);
|
transform: translateY(-$offset);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
background-color: #000;
|
background-color: #000;
|
||||||
background-position: 50% 50%;
|
background-position: 50% 50%;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 100% 100%;
|
background-size: contain;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -15,8 +15,9 @@
|
|||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr--stopped .plyr__poster {
|
.plyr--stopped.plyr__poster-enabled .plyr__poster {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,18 @@
|
|||||||
.plyr__progress {
|
.plyr__progress {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
left: $plyr-range-thumb-height / 2;
|
||||||
|
margin-right: $plyr-range-thumb-height;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
input[type='range'],
|
||||||
|
&__buffer {
|
||||||
|
margin-left: -($plyr-range-thumb-height / 2);
|
||||||
|
margin-right: -($plyr-range-thumb-height / 2);
|
||||||
|
// Offset the range thumb in order to be able to calculate the relative progress (#954)
|
||||||
|
width: calc(100% + #{$plyr-range-thumb-height});
|
||||||
|
}
|
||||||
|
|
||||||
input[type='range'] {
|
input[type='range'] {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -19,18 +29,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr__progress--buffer {
|
.plyr__progress__buffer {
|
||||||
-webkit-appearance: none; /* stylelint-disable-line */
|
-webkit-appearance: none; /* stylelint-disable-line */
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
height: $plyr-range-track-height;
|
height: $plyr-range-track-height;
|
||||||
left: 0;
|
left: 0;
|
||||||
margin: -($plyr-range-track-height / 2) 0 0;
|
margin-top: -($plyr-range-track-height / 2);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&::-webkit-progress-bar {
|
&::-webkit-progress-bar {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -58,17 +67,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr--video .plyr__progress--buffer {
|
.plyr--video .plyr__progress__buffer {
|
||||||
box-shadow: 0 1px 1px rgba(#000, 0.15);
|
box-shadow: 0 1px 1px rgba(#000, 0.15);
|
||||||
color: $plyr-video-progress-buffered-bg;
|
color: $plyr-video-progress-buffered-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr--audio .plyr__progress--buffer {
|
.plyr--audio .plyr__progress__buffer {
|
||||||
color: $plyr-audio-progress-buffered-bg;
|
color: $plyr-audio-progress-buffered-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading state
|
// Loading state
|
||||||
.plyr--loading .plyr__progress--buffer {
|
.plyr--loading .plyr__progress__buffer {
|
||||||
animation: plyr-progress 1s linear infinite;
|
animation: plyr-progress 1s linear infinite;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
-45deg,
|
-45deg,
|
||||||
@@ -85,10 +94,10 @@
|
|||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr--video.plyr--loading .plyr__progress--buffer {
|
.plyr--video.plyr--loading .plyr__progress__buffer {
|
||||||
background-color: $plyr-video-progress-buffered-bg;
|
background-color: $plyr-video-progress-buffered-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr--audio.plyr--loading .plyr__progress--buffer {
|
.plyr--audio.plyr--loading .plyr__progress__buffer {
|
||||||
background-color: $plyr-audio-progress-buffered-bg;
|
background-color: $plyr-audio-progress-buffered-bg;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
transform: translate(-50%, 10px) scale(0.8);
|
transform: translate(-50%, 10px) scale(0.8);
|
||||||
transform-origin: 50% 100%;
|
transform-origin: 50% 100%;
|
||||||
transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;
|
transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;
|
||||||
|
white-space: nowrap;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
// The background triangle
|
// The background triangle
|
||||||
|
|||||||
+2
-3
@@ -31,15 +31,14 @@
|
|||||||
@import 'components/controls';
|
@import 'components/controls';
|
||||||
@import 'components/embed';
|
@import 'components/embed';
|
||||||
@import 'components/menus';
|
@import 'components/menus';
|
||||||
@import 'components/progress';
|
|
||||||
@import 'components/poster';
|
|
||||||
@import 'components/sliders';
|
@import 'components/sliders';
|
||||||
|
@import 'components/poster';
|
||||||
@import 'components/times';
|
@import 'components/times';
|
||||||
@import 'components/tooltips';
|
@import 'components/tooltips';
|
||||||
@import 'components/video';
|
@import 'components/video';
|
||||||
|
@import 'components/progress';
|
||||||
@import 'components/volume';
|
@import 'components/volume';
|
||||||
|
|
||||||
@import 'states/error';
|
|
||||||
@import 'states/fullscreen';
|
@import 'states/fullscreen';
|
||||||
|
|
||||||
@import 'plugins/ads';
|
@import 'plugins/ads';
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
// --------------------------------------------------------------
|
|
||||||
// Error state
|
|
||||||
// --------------------------------------------------------------
|
|
||||||
|
|
||||||
.plyr--has-error {
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
align-items: center;
|
|
||||||
background: rgba(#000, 90%);
|
|
||||||
color: #fff;
|
|
||||||
content: attr(data-plyr-error);
|
|
||||||
display: flex;
|
|
||||||
font-size: $plyr-font-size-base;
|
|
||||||
height: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
text-shadow: 0 1px 1px rgba(#000, 10%);
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user