Merge pull request #975 from sampotts/develop

v3.3.8
This commit is contained in:
Sam Potts
2018-05-26 13:55:22 +10:00
committed by GitHub
30 changed files with 1560 additions and 919 deletions
+12
View File
@@ -1,3 +1,15 @@
# v3.3.8
* 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 # v3.3.7
* Poster fixes (thanks @friday) * Poster fixes (thanks @friday)
+1 -1
View File
File diff suppressed because one or more lines are too long
+115 -13
View File
@@ -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;
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+356 -280
View File
@@ -1,4 +1,4 @@
(function (global, factory) { typeof navigator === "object" && (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define('Plyr', factory) : typeof define === 'function' && define.amd ? define('Plyr', factory) :
(global.Plyr = factory()); (global.Plyr = factory());
@@ -602,6 +602,24 @@ var 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: function loadImage(src) {
var minWidth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
return new Promise(function (resolve, reject) {
var image = new Image();
var handler = function handler() {
delete image.onload;
delete image.onerror;
(image.naturalWidth >= minWidth ? resolve : reject)(image);
};
Object.assign(image, { onload: handler, onerror: handler, src: src });
});
},
// Load an external script // Load an external script
loadScript: function loadScript(url) { loadScript: function loadScript(url) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
@@ -730,7 +748,7 @@ var 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
@@ -884,14 +902,16 @@ var utils = {
}, },
// Toggle class on an element // Mirror Element.classList.toggle, with IE compatibility for "force" argument
toggleClass: function toggleClass(element, className, toggle) { toggleClass: function toggleClass(element, className, force) {
if (utils.is.element(element)) { if (utils.is.element(element)) {
var contains = element.classList.contains(className); var 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;
@@ -1274,6 +1294,12 @@ var utils = {
}, },
// Clone nested objects
cloneDeep: function cloneDeep(object) {
return JSON.parse(JSON.stringify(object));
},
// Get the closest value in an array // Get the closest value in an array
closest: function closest(array, value) { closest: function closest(array, value) {
if (!utils.is.array(array) || !array.length) { if (!utils.is.array(array) || !array.length) {
@@ -2093,7 +2119,7 @@ var controls = {
break; break;
} }
progress.textContent = '% ' + suffix.toLowerCase(); progress.innerText = '% ' + suffix.toLowerCase();
} }
this.elements.display[type] = progress; this.elements.display[type] = progress;
@@ -2167,7 +2193,7 @@ var controls = {
var forceHours = utils.getHours(this.duration) > 0; var forceHours = utils.getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
target.textContent = utils.formatTime(time, forceHours, inverted); target.innerText = utils.formatTime(time, forceHours, inverted);
}, },
@@ -2236,6 +2262,7 @@ var controls = {
// Video playing // Video playing
case 'timeupdate': case 'timeupdate':
case 'seeking': case 'seeking':
case 'seeked':
value = utils.getPercentage(this.currentTime, this.duration); value = utils.getPercentage(this.currentTime, this.duration);
// Set seek range value only if it's a 'natural' time event // Set seek range value only if it's a 'natural' time event
@@ -2293,7 +2320,7 @@ var controls = {
// Calculate percentage // Calculate percentage
var percent = 0; var percent = 0;
var clientRect = this.elements.inputs.seek.getBoundingClientRect(); var clientRect = this.elements.progress.getBoundingClientRect();
var visible = this.config.classNames.tooltip + '--visible'; var visible = this.config.classNames.tooltip + '--visible';
var toggle = function toggle(_toggle) { var toggle = function toggle(_toggle) {
@@ -2354,9 +2381,10 @@ var controls = {
}, },
// Show the duration on metadataloaded // Show the duration on metadataloaded or durationchange events
durationUpdate: function durationUpdate() { durationUpdate: function durationUpdate() {
if (!this.supported.ui) { // 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; return;
} }
@@ -2920,7 +2948,6 @@ var controls = {
// Seek tooltip // Seek tooltip
if (this.config.tooltips.seek) { if (this.config.tooltips.seek) {
var tooltip = utils.createElement('span', { var tooltip = utils.createElement('span', {
role: 'tooltip',
class: this.config.classNames.tooltip class: this.config.classNames.tooltip
}, '00:00'); }, '00:00');
@@ -3471,7 +3498,7 @@ var 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);
} }
@@ -3601,7 +3628,7 @@ var defaults$1 = {
// Sprite (for icons) // Sprite (for icons)
loadSprite: true, loadSprite: true,
iconPrefix: 'plyr', iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.3.7/plyr.svg', iconUrl: 'https://cdn.plyr.io/3.3.8/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',
@@ -3709,8 +3736,7 @@ var defaults$1 = {
}, },
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'
@@ -3806,13 +3832,13 @@ var defaults$1 = {
embed: 'plyr__video-embed', embed: 'plyr__video-embed',
embedContainer: 'plyr__video-embed__container', embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster', poster: 'plyr__poster',
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads', ads: 'plyr__ads',
control: 'plyr__control', control: 'plyr__control',
playing: 'plyr--playing', playing: 'plyr--playing',
paused: 'plyr--paused', paused: 'plyr--paused',
stopped: 'plyr--stopped', stopped: 'plyr--stopped',
loading: 'plyr--loading', loading: 'plyr--loading',
error: 'plyr--has-error',
hover: 'plyr--hover', hover: 'plyr--hover',
tooltip: 'plyr__tooltip', tooltip: 'plyr__tooltip',
cues: 'plyr__cues', cues: 'plyr__cues',
@@ -4207,8 +4233,10 @@ var 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);
}
}, },
@@ -4250,17 +4278,43 @@ var ui = {
}, },
// Set the poster image // Toggle poster
setPoster: function setPoster() { togglePoster: function 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: function setPoster(poster) {
var _this2 = this;
// 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
var posters = this.poster.split(','); var loadPromise = utils.loadImage(poster).then(function () {
this.elements.poster.style.backgroundImage = posters.map(function (p) { _this2.elements.poster.style.backgroundImage = 'url(\'' + poster + '\')';
return 'url(\'' + p + '\')'; Object.assign(_this2.elements.poster.style, {
}).join(','); backgroundImage: 'url(\'' + poster + '\')',
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: ''
});
ui.togglePoster.call(_this2, 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(function () {
return ui.togglePoster.call(_this2, false);
});
// Return the promise so the caller can use it as well
return loadPromise;
}, },
@@ -4280,13 +4334,13 @@ var ui = {
} }
// Toggle controls // Toggle controls
this.toggleControls(!this.playing); ui.toggleControls.call(this);
}, },
// Check if media is loading // Check if media is loading
checkLoading: function checkLoading(event) { checkLoading: function checkLoading(event) {
var _this2 = this; var _this3 = this;
this.loading = ['stalled', 'waiting'].includes(event.type); this.loading = ['stalled', 'waiting'].includes(event.type);
@@ -4295,38 +4349,24 @@ var ui = {
// Timer to prevent flicker when seeking // Timer to prevent flicker when seeking
this.timers.loading = setTimeout(function () { this.timers.loading = setTimeout(function () {
// Toggle container class hook // Update progress bar loading class state
utils.toggleClass(_this2.elements.container, _this2.config.classNames.loading, _this2.loading); utils.toggleClass(_this3.elements.container, _this3.config.classNames.loading, _this3.loading);
// Show controls if loading, hide if done // Update controls visibility
_this2.toggleControls(_this2.loading); ui.toggleControls.call(_this3);
}, this.loading ? 250 : 0); }, this.loading ? 250 : 0);
}, },
// Check if media failed to load // Toggle controls based on state and `force` argument
checkFailed: function checkFailed() { toggleControls: function toggleControls(force) {
var _this3 = this; var controls$$1 = this.elements.controls;
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState
this.failed = this.media.networkState === 3;
if (this.failed) { if (controls$$1 && 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$$1.pressed || controls$$1.hover));
} }
// Clear timer
clearTimeout(this.timers.failed);
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(function () {
// Toggle container class hook
utils.toggleClass(_this3.elements.container, _this3.config.classNames.loading, _this3.loading);
// Show controls if loading, hide if done
_this3.toggleControls(_this3.loading);
}, this.loading ? 250 : 0);
} }
}; };
@@ -4564,13 +4604,35 @@ var Listeners = function () {
}, 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', function (event) {
// Toggle controls on mouse events and entering fullscreen var controls$$1 = _this2.player.elements.controls;
utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', function (event) {
_this2.player.toggleControls(event); // Remove button states for fullscreen
});
} if (event.type === 'enterfullscreen') {
controls$$1.pressed = false;
controls$$1.hover = false;
}
// Show, then hide after a timeout unless another control event occurs
var show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
var delay = 0;
if (show) {
ui.toggleControls.call(_this2.player, true);
// Use longer timeout for touch devices
delay = _this2.player.touch ? 3000 : 2000;
}
// Clear timer
clearTimeout(_this2.player.timers.controls);
// Timer to prevent flicker when seeking
_this2.player.timers.controls = setTimeout(function () {
return ui.toggleControls.call(_this2.player, false);
}, delay);
});
} }
// Listen for media events // Listen for media events
@@ -4581,7 +4643,7 @@ var Listeners = function () {
var _this3 = this; var _this3 = this;
// Time change on media // Time change on media
utils.on(this.player.media, 'timeupdate seeking', function (event) { utils.on(this.player.media, 'timeupdate seeking seeked', function (event) {
return controls.timeUpdate.call(_this3.player, event); return controls.timeUpdate.call(_this3.player, event);
}); });
@@ -4607,7 +4669,7 @@ var Listeners = function () {
}); });
// Check for buffer progress // Check for buffer progress
utils.on(this.player.media, 'progress playing', function (event) { utils.on(this.player.media, 'progress playing seeking seeked', function (event) {
return controls.updateProgress.call(_this3.player, event); return controls.updateProgress.call(_this3.player, event);
}); });
@@ -4626,9 +4688,6 @@ var Listeners = function () {
return ui.checkLoading.call(_this3.player, event); return ui.checkLoading.call(_this3.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', function () { utils.on(this.player.media, 'playing', function () {
@@ -4850,9 +4909,47 @@ var Listeners = function () {
} }
}); });
// Set range input alternative "value", which matches the tooltip time (#954)
on(this.player.elements.inputs.seek, 'mousedown mousemove', function (event) {
var clientRect = _this4.player.elements.progress.getBoundingClientRect();
var 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', function (event) {
var seek = event.currentTarget;
// Was playing before?
var play = seek.hasAttribute('play-on-seeked');
// Done seeking
var 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');
_this4.player.play();
} else if (!done && _this4.player.playing) {
seek.setAttribute('play-on-seeked', '');
_this4.player.pause();
}
});
// Seek // Seek
on(this.player.elements.inputs.seek, inputEvent, function (event) { on(this.player.elements.inputs.seek, inputEvent, function (event) {
_this4.player.currentTime = event.target.value / event.target.max * _this4.player.duration; var seek = event.currentTarget;
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
var seekTo = seek.getAttribute('seek-value');
if (utils.is.empty(seekTo)) {
seekTo = seek.value;
}
seek.removeAttribute('seek-value');
_this4.player.currentTime = seekTo / seek.max * _this4.player.duration;
}, 'seek'); }, 'seek');
// Current time invert // Current time invert
@@ -4887,23 +4984,48 @@ var Listeners = function () {
return controls.updateSeekTooltip.call(_this4.player, event); return controls.updateSeekTooltip.call(_this4.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', function (event) {
// Watch for cursor over controls so they don't hide when trying to interact _this4.player.elements.controls.hover = !_this4.player.touch && event.type === 'mouseenter';
on(this.player.elements.controls, 'mouseenter mouseleave', function (event) { });
_this4.player.elements.controls.hover = !_this4.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', function (event) { on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
_this4.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); _this4.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
}); });
// Focus in/out on controls // Focus in/out on controls
on(this.player.elements.controls, 'focusin focusout', function (event) { on(this.player.elements.controls, 'focusin focusout', function (event) {
_this4.player.toggleControls(event); var _player = _this4.player,
}); config = _player.config,
} elements = _player.elements,
timers = _player.timers;
// Skip transition to prevent focus from scrolling the parent element
utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
// Toggle
ui.toggleControls.call(_this4.player, event.type === 'focusin');
// If focusin, hide again after delay
if (event.type === 'focusin') {
// Restore transition
setTimeout(function () {
utils.toggleClass(elements.controls, config.classNames.noTransition, false);
}, 0);
// Delay a little more for keyboard users
var delay = _this4.touch ? 3000 : 4000;
// Clear timer
clearTimeout(timers.controls);
// Hide
timers.controls = setTimeout(function () {
return ui.toggleControls.call(_this4.player, false);
}, delay);
}
});
// Mouse wheel for volume // Mouse wheel for volume
on(this.player.elements.inputs.volume, 'wheel', function (event) { on(this.player.elements.inputs.volume, 'wheel', function (event) {
@@ -4955,6 +5077,14 @@ var Listeners = function () {
// ========================================================================== // ==========================================================================
// 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');
}
}
var vimeo = { var vimeo = {
setup: function setup() { setup: function setup() {
var _this = this; var _this = this;
@@ -5050,11 +5180,8 @@ var 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
@@ -5074,15 +5201,13 @@ var vimeo = {
// Create a faux HTML5 API using the Vimeo API // Create a faux HTML5 API using the Vimeo API
player.media.play = function () { player.media.play = function () {
player.embed.play().then(function () { assurePlaybackState.call(player, true);
player.media.paused = false; return player.embed.play();
});
}; };
player.media.pause = function () { player.media.pause = function () {
player.embed.pause().then(function () { assurePlaybackState.call(player, false);
player.media.paused = true; return player.embed.pause();
});
}; };
player.media.stop = function () { player.media.stop = function () {
@@ -5098,26 +5223,35 @@ var vimeo = {
return currentTime; return currentTime;
}, },
set: function set(time) { set: function 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
var paused = player.media.paused;
// Set seeking flag // Get current paused state and volume etc
var embed = player.embed,
media = player.media,
paused = player.paused,
volume = player.volume;
player.media.seeking = true; // Set seeking state and trigger event
// Trigger seeking media.seeking = true;
utils.dispatchEvent.call(player, player.media, 'seeking'); utils.dispatchEvent.call(player, media, 'seeking');
// Seek after events // If paused, mute until seek is complete
player.embed.setCurrentTime(time).catch(function () { Promise.resolve(paused && embed.setVolume(0))
// Seek
.then(function () {
return embed.setCurrentTime(time);
})
// Restore paused
.then(function () {
return paused && embed.pause();
})
// Restore volume
.then(function () {
return paused && embed.setVolume(volume);
}).catch(function () {
// Do nothing // Do nothing
}); });
// Restore pause state
if (paused) {
player.pause();
}
} }
}); });
@@ -5265,17 +5399,12 @@ var vimeo = {
}); });
player.embed.on('play', function () { player.embed.on('play', function () {
// 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', function () { player.embed.on('pause', function () {
player.media.paused = true; assurePlaybackState.call(player, false);
utils.dispatchEvent.call(player, player.media, 'pause');
}); });
player.embed.on('timeupdate', function (data) { player.embed.on('timeupdate', function (data) {
@@ -5306,7 +5435,6 @@ var vimeo = {
player.embed.on('seeked', function () { player.embed.on('seeked', function () {
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', function () { player.embed.on('ended', function () {
@@ -5388,6 +5516,14 @@ function mapQualityUnits(levels) {
})); }));
} }
// Set playback state and trigger change (only on actual change)
function assurePlaybackState$1(play) {
if (this.media.paused === play) {
this.media.paused = !play;
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
var youtube = { var youtube = {
setup: function setup() { setup: function setup() {
var _this = this; var _this = this;
@@ -5491,7 +5627,26 @@ var 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)); var posterSrc = function posterSrc(format) {
return '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(function () {
return utils.loadImage(posterSrc('sd'), 121);
}) // 480p padded 4:3
.catch(function () {
return utils.loadImage(posterSrc('hq'));
}) // 360p padded 4:3. Always exists
.then(function (image) {
return ui.setPoster.call(player, image.src);
}).then(function (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
@@ -5578,10 +5733,12 @@ var youtube = {
// Create a faux HTML5 API using the YouTube API // Create a faux HTML5 API using the YouTube API
player.media.play = function () { player.media.play = function () {
assurePlaybackState$1.call(player, true);
instance.playVideo(); instance.playVideo();
}; };
player.media.pause = function () { player.media.pause = function () {
assurePlaybackState$1.call(player, false);
instance.pauseVideo(); instance.pauseVideo();
}; };
@@ -5599,23 +5756,17 @@ var youtube = {
return Number(instance.getCurrentTime()); return Number(instance.getCurrentTime());
}, },
set: function set(time) { set: function 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).
var paused = player.media.paused; 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();
}
} }
}); });
@@ -5738,6 +5889,14 @@ var youtube = {
// Reset timer // Reset timer
clearInterval(player.timers.playing); clearInterval(player.timers.playing);
var 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
@@ -5757,7 +5916,7 @@ var youtube = {
break; break;
case 0: case 0:
player.media.paused = true; assurePlaybackState$1.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) {
@@ -5771,42 +5930,39 @@ var 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$1.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(function () {
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(function () {
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$1.call(player, false);
break; break;
@@ -6740,7 +6896,7 @@ var Plyr = function () {
} }
// Set config // Set config
this.config = utils.extend({}, defaults$1, options || {}, function () { this.config = utils.extend({}, defaults$1, Plyr.defaults, options || {}, function () {
try { try {
return JSON.parse(_this.media.getAttribute('data-plyr-config')); return JSON.parse(_this.media.getAttribute('data-plyr-config'));
} catch (e) { } catch (e) {
@@ -7180,114 +7336,35 @@ var Plyr = function () {
/** /**
* Toggle the player controls * Toggle the player controls
* @param {boolean} toggle - Whether to show the controls * @param {boolean} [toggle] - Whether to show the controls
*/ */
}, { }, {
key: 'toggleControls', key: 'toggleControls',
value: function toggleControls(toggle) { value: function toggleControls(toggle) {
var _this2 = this; // Don't toggle if missing UI support or if it's audio
if (this.supported.ui && !this.isAudio) {
// Get state before change
var isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
// We need controls of course... // Negate the argument if not undefined since adding the class to hides the controls
if (!utils.is.element(this.elements.controls)) { var force = typeof toggle === 'undefined' ? undefined : !toggle;
return;
}
// Don't hide if no UI support or it's audio // Apply and get updated state
if (!this.supported.ui || this.isAudio) { var hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
return;
}
var delay = 0; // Close menu
var show = toggle; if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
var isEnterFullscreen = false; controls.toggleMenu.call(this, false);
// Get toggle state if not set
if (!utils.is.boolean(toggle)) {
if (utils.is.event(toggle)) {
// Is the enter fullscreen event
isEnterFullscreen = toggle.type === 'enterfullscreen';
// Events that show the controls
var showEvents = ['touchstart', 'touchmove', 'mouseenter', 'mousemove', 'focusin'];
// Events that delay hiding
var 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 var 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
var 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(function () {
// We need controls of course...
if (!utils.is.element(_this2.elements.controls)) {
return;
}
// If the mouse is over the controls (and not entering fullscreen), bail
if ((_this2.elements.controls.pressed || _this2.elements.controls.hover) && !isEnterFullscreen) {
return;
}
// Restore transition behaviour
if (!utils.hasClass(_this2.elements.container, _this2.config.classNames.hideControls)) {
utils.toggleClass(_this2.elements.controls, _this2.config.classNames.noTransition, false);
}
// Set hideControls class
var toggled = utils.toggleClass(_this2.elements.container, _this2.config.classNames.hideControls, _this2.config.hideControls);
// Trigger event and close menu
if (toggled) {
utils.dispatchEvent.call(_this2, _this2.media, 'controlshidden');
if (_this2.config.controls.includes('settings') && !utils.is.empty(_this2.config.settings)) {
controls.toggleMenu.call(_this2, false);
}
}
}, delay);
}
} }
/** /**
@@ -7325,7 +7402,7 @@ var Plyr = function () {
}, { }, {
key: 'destroy', key: 'destroy',
value: function destroy(callback) { value: function destroy(callback) {
var _this3 = this; var _this2 = this;
var soft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var soft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
@@ -7338,22 +7415,22 @@ var Plyr = function () {
document.body.style.overflow = ''; document.body.style.overflow = '';
// GC for embed // GC for embed
_this3.embed = null; _this2.embed = null;
// If it's a soft destroy, make minimal changes // If it's a soft destroy, make minimal changes
if (soft) { if (soft) {
if (Object.keys(_this3.elements).length) { if (Object.keys(_this2.elements).length) {
// Remove elements // Remove elements
utils.removeElement(_this3.elements.buttons.play); utils.removeElement(_this2.elements.buttons.play);
utils.removeElement(_this3.elements.captions); utils.removeElement(_this2.elements.captions);
utils.removeElement(_this3.elements.controls); utils.removeElement(_this2.elements.controls);
utils.removeElement(_this3.elements.wrapper); utils.removeElement(_this2.elements.wrapper);
// Clear for GC // Clear for GC
_this3.elements.buttons.play = null; _this2.elements.buttons.play = null;
_this3.elements.captions = null; _this2.elements.captions = null;
_this3.elements.controls = null; _this2.elements.controls = null;
_this3.elements.wrapper = null; _this2.elements.wrapper = null;
} }
// Callback // Callback
@@ -7362,26 +7439,26 @@ var Plyr = function () {
} }
} else { } else {
// Unbind listeners // Unbind listeners
_this3.listeners.clear(); _this2.listeners.clear();
// Replace the container with the original element provided // Replace the container with the original element provided
utils.replaceElement(_this3.elements.original, _this3.elements.container); utils.replaceElement(_this2.elements.original, _this2.elements.container);
// Event // Event
utils.dispatchEvent.call(_this3, _this3.elements.original, 'destroyed', true); utils.dispatchEvent.call(_this2, _this2.elements.original, 'destroyed', true);
// Callback // Callback
if (utils.is.function(callback)) { if (utils.is.function(callback)) {
callback.call(_this3.elements.original); callback.call(_this2.elements.original);
} }
// Reset state // Reset state
_this3.ready = false; _this2.ready = false;
// Clear for garbage collection // Clear for garbage collection
setTimeout(function () { setTimeout(function () {
_this3.elements = null; _this2.elements = null;
_this3.media = null; _this2.media = null;
}, 200); }, 200);
} }
}; };
@@ -7900,10 +7977,7 @@ var Plyr = function () {
return; return;
} }
if (utils.is.string(input)) { ui.setPoster.call(this, input);
this.media.setAttribute('poster', input);
ui.setPoster.call(this);
}
} }
/** /**
@@ -8084,6 +8158,8 @@ var Plyr = function () {
return Plyr; return Plyr;
}(); }();
Plyr.defaults = utils.cloneDeep(defaults$1);
return Plyr; return Plyr;
}))); })));
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+746 -298
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -241,11 +241,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
@@ -257,7 +257,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',
+11 -9
View File
@@ -1,6 +1,6 @@
{ {
"name": "plyr", "name": "plyr",
"version": "3.3.7", "version": "3.3.8",
"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",
@@ -26,23 +26,25 @@
"gulp-filter": "^5.1.0", "gulp-filter": "^5.1.0",
"gulp-header": "^2.0.5", "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",
@@ -70,7 +72,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"
} }
} }
+5 -5
View File
@@ -128,13 +128,13 @@ See [initialising](#initialising) for more information on advanced setups.
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. You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
```html ```html
<script src="https://cdn.plyr.io/3.3.7/plyr.js"></script> <script src="https://cdn.plyr.io/3.3.8/plyr.js"></script>
``` ```
...or... ...or...
```html ```html
<script src="https://cdn.plyr.io/3.3.7/plyr.polyfilled.js"></script> <script src="https://cdn.plyr.io/3.3.8/plyr.polyfilled.js"></script>
``` ```
### CSS ### CSS
@@ -148,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.7/plyr.css"> <link rel="stylesheet" href="https://cdn.plyr.io/3.3.8/plyr.css">
``` ```
### SVG Sprite ### SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.7/plyr.svg`. reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.8/plyr.svg`.
## Ads ## Ads
@@ -361,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. |
+4 -3
View File
@@ -481,6 +481,7 @@ const controls = {
// Video playing // Video playing
case 'timeupdate': case 'timeupdate':
case 'seeking': case 'seeking':
case 'seeked':
value = utils.getPercentage(this.currentTime, this.duration); value = utils.getPercentage(this.currentTime, this.duration);
// Set seek range value only if it's a 'natural' time event // Set seek range value only if it's a 'natural' time event
@@ -601,9 +602,10 @@ const controls = {
controls.updateProgress.call(this, event); controls.updateProgress.call(this, event);
}, },
// Show the duration on metadataloaded // Show the duration on metadataloaded or durationchange events
durationUpdate() { durationUpdate() {
if (!this.supported.ui) { // 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; return;
} }
@@ -1163,7 +1165,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',
+2 -3
View File
@@ -56,7 +56,7 @@ const defaults = {
// Sprite (for icons) // Sprite (for icons)
loadSprite: true, loadSprite: true,
iconPrefix: 'plyr', iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.3.7/plyr.svg', iconUrl: 'https://cdn.plyr.io/3.3.8/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',
@@ -199,7 +199,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',
@@ -332,13 +331,13 @@ const defaults = {
embed: 'plyr__video-embed', embed: 'plyr__video-embed',
embedContainer: 'plyr__video-embed__container', embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster', poster: 'plyr__poster',
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads', ads: 'plyr__ads',
control: 'plyr__control', control: 'plyr__control',
playing: 'plyr--playing', playing: 'plyr--playing',
paused: 'plyr--paused', paused: 'plyr--paused',
stopped: 'plyr--stopped', stopped: 'plyr--stopped',
loading: 'plyr--loading', loading: 'plyr--loading',
error: 'plyr--has-error',
hover: 'plyr--hover', hover: 'plyr--hover',
tooltip: 'plyr__tooltip', tooltip: 'plyr__tooltip',
cues: 'plyr__cues', cues: 'plyr__cues',
+104 -41
View File
@@ -238,19 +238,42 @@ 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 => controls.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 => controls.durationUpdate.call(this.player, event)); utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event));
@@ -272,7 +295,7 @@ class Listeners {
}); });
// Check for buffer progress // Check for buffer progress
utils.on(this.player.media, 'progress playing', event => controls.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 => controls.updateVolume.call(this.player, event)); utils.on(this.player.media, 'volumechange', event => controls.updateVolume.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', () => {
@@ -530,15 +550,35 @@ class Listeners {
}); });
// Set range input alternative "value", which matches the tooltip time (#954) // Set range input alternative "value", which matches the tooltip time (#954)
on( on(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
this.player.elements.inputs.seek, const clientRect = this.player.elements.progress.getBoundingClientRect();
'mousedown mousemove', const percent = 100 / clientRect.width * (event.pageX - clientRect.left);
event => { event.currentTarget.setAttribute('seek-value', percent);
const clientRect = this.player.elements.progress.getBoundingClientRect(); });
const percent = 100 / clientRect.width * (event.pageX - clientRect.left);
event.currentTarget.setAttribute('seekNext', 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(
@@ -546,12 +586,16 @@ class Listeners {
inputEvent, inputEvent,
event => { event => {
const seek = event.currentTarget; const seek = event.currentTarget;
// If it exists, use seekNext instead of "value" for consistency with tooltip time (#954)
let seekTo = seek.getAttribute('seekNext'); // 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)) { if (utils.is.empty(seekTo)) {
seekTo = seek.value; seekTo = seek.value;
} }
seek.removeAttribute('seekNext');
seek.removeAttribute('seek-value');
this.player.currentTime = seekTo / seek.max * this.player.duration; this.player.currentTime = seekTo / seek.max * this.player.duration;
}, },
'seek', 'seek',
@@ -592,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(
+33 -35
View File
@@ -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
@@ -99,11 +107,8 @@ const vimeo = {
// Get original image // Get original image
url.pathname = `${url.pathname.split('_')[0]}.jpg`; url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Set attribute // Set and show poster
player.media.setAttribute('poster', url.href); ui.setPoster.call(player, url.href);
// Update
ui.setPoster.call(player);
}); });
// Setup instance // Setup instance
@@ -123,15 +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 = () => {
@@ -146,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
});
}, },
}); });
@@ -318,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 => {
@@ -359,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
View File
@@ -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;
+20 -110
View File
@@ -1,6 +1,6 @@
// ========================================================================== // ==========================================================================
// Plyr // Plyr
// plyr.js v3.3.7 // plyr.js v3.3.8
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================
@@ -802,10 +802,7 @@ class Plyr {
return; return;
} }
if (utils.is.string(input)) { ui.setPoster.call(this, input);
this.media.setAttribute('poster', input);
ui.setPoster.call(this);
}
} }
/** /**
@@ -971,119 +968,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);
}
} }
/** /**
+2 -1
View File
@@ -1,12 +1,13 @@
// ========================================================================== // ==========================================================================
// Plyr Polyfilled Build // Plyr Polyfilled Build
// plyr.js v3.3.7 // plyr.js v3.3.8
// 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;
+45 -32
View File
@@ -105,8 +105,10 @@ 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);
}
}, },
// Setup aria attribute for play and iframe title // Setup aria attribute for play and iframe title
@@ -146,15 +148,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 +199,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,35 +214,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);
}, },
}; };
+23 -6
View File
@@ -120,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) => {
@@ -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;
+1 -1
View File
@@ -18,6 +18,6 @@
pointer-events: none; pointer-events: none;
} }
.plyr--stopped .plyr__poster { .plyr--stopped.plyr__poster-enabled .plyr__poster {
opacity: 1; opacity: 1;
} }
-1
View File
@@ -39,7 +39,6 @@
@import 'components/video'; @import 'components/video';
@import 'components/volume'; @import 'components/volume';
@import 'states/error';
@import 'states/fullscreen'; @import 'states/fullscreen';
@import 'plugins/ads'; @import 'plugins/ads';
-25
View File
@@ -1,25 +0,0 @@
// --------------------------------------------------------------
// Error state
// --------------------------------------------------------------
.plyr--has-error {
pointer-events: none;
&::after {
align-items: center;
background: rgba(#000, 90%);
color: #fff;
content: attr(data-plyr-error);
display: flex;
font-size: $plyr-font-size-base;
height: 100%;
justify-content: center;
left: 0;
position: absolute;
text-align: center;
text-shadow: 0 1px 1px rgba(#000, 10%);
top: 0;
width: 100%;
z-index: 10;
}
}