This commit is contained in:
Sam Potts 2018-05-26 13:37:10 +10:00
parent edd67b0da3
commit 3e0a911418
24 changed files with 1396 additions and 617 deletions

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

128
demo/dist/demo.js vendored
View File

@ -1,4 +1,4 @@
(function () {
typeof navigator === "object" && (function () {
'use strict';
var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
@ -97,7 +97,7 @@ function isObject(what) {
// Yanked from https://git.io/vS8DV re-used under CC0
// with some tiny modifications
function isError(value) {
switch ({}.toString.call(value)) {
switch (Object.prototype.toString.call(value)) {
case '[object Error]':
return true;
case '[object Exception]':
@ -110,7 +110,15 @@ function isError(value) {
}
function isErrorEvent(value) {
return supportsErrorEvent() && {}.toString.call(value) === '[object ErrorEvent]';
return Object.prototype.toString.call(value) === '[object ErrorEvent]';
}
function isDOMError(value) {
return Object.prototype.toString.call(value) === '[object DOMError]';
}
function isDOMException(value) {
return Object.prototype.toString.call(value) === '[object DOMException]';
}
function isUndefined(what) {
@ -153,6 +161,24 @@ function supportsErrorEvent() {
}
}
function supportsDOMError() {
try {
new DOMError(''); // eslint-disable-line no-new
return true;
} catch (e) {
return false;
}
}
function supportsDOMException() {
try {
new DOMException(''); // eslint-disable-line no-new
return true;
} catch (e) {
return false;
}
}
function supportsFetch() {
if (!('fetch' in _window)) return false;
@ -668,6 +694,8 @@ var utils = {
isObject: isObject,
isError: isError,
isErrorEvent: isErrorEvent,
isDOMError: isDOMError,
isDOMException: isDOMException,
isUndefined: isUndefined,
isFunction: isFunction,
isPlainObject: isPlainObject,
@ -675,6 +703,8 @@ var utils = {
isArray: isArray,
isEmptyObject: isEmptyObject,
supportsErrorEvent: supportsErrorEvent,
supportsDOMError: supportsDOMError,
supportsDOMException: supportsDOMException,
supportsFetch: supportsFetch,
supportsReferrerPolicy: supportsReferrerPolicy,
supportsPromiseRejectionEvent: supportsPromiseRejectionEvent,
@ -729,10 +759,24 @@ var ERROR_TYPES_RE = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Ran
function getLocationHref() {
if (typeof document === 'undefined' || document.location == null) return '';
return document.location.href;
}
function getLocationOrigin() {
if (typeof document === 'undefined' || document.location == null) return '';
// Oh dear IE10...
if (!document.location.origin) {
document.location.origin =
document.location.protocol +
'//' +
document.location.hostname +
(document.location.port ? ':' + document.location.port : '');
}
return document.location.origin;
}
/**
* TraceKit.report: cross-browser processing of unhandled exceptions
*
@ -1140,6 +1184,44 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
element.func = UNKNOWN_FUNCTION;
}
if (element.url && element.url.substr(0, 5) === 'blob:') {
// Special case for handling JavaScript loaded into a blob.
// We use a synchronous AJAX request here as a blob is already in
// memory - it's not making a network request. This will generate a warning
// in the browser console, but there has already been an error so that's not
// that much of an issue.
var xhr = new XMLHttpRequest();
xhr.open('GET', element.url, false);
xhr.send(null);
// If we failed to download the source, skip this patch
if (xhr.status === 200) {
var source = xhr.responseText || '';
// We trim the source down to the last 300 characters as sourceMappingURL is always at the end of the file.
// Why 300? To be in line with: https://github.com/getsentry/sentry/blob/4af29e8f2350e20c28a6933354e4f42437b4ba42/src/sentry/lang/javascript/processor.py#L164-L175
source = source.slice(-300);
// Now we dig out the source map URL
var sourceMaps = source.match(/\/\/# sourceMappingURL=(.*)$/);
// If we don't find a source map comment or we find more than one, continue on to the next element.
if (sourceMaps) {
var sourceMapAddress = sourceMaps[1];
// Now we check to see if it's a relative URL.
// If it is, convert it to an absolute one.
if (sourceMapAddress.charAt(0) === '~') {
sourceMapAddress = getLocationOrigin() + sourceMapAddress.slice(1);
}
// Now we strip the '.map' off of the end of the URL and update the
// element so that Sentry can match the map to the blob.
element.url = sourceMapAddress.slice(0, -4);
}
}
}
stack.push(element);
}
@ -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 isObject$1 = utils.isObject;
var isPlainObject$1 = utils.isPlainObject;
var isErrorEvent$1 = utils.isErrorEvent;
var isUndefined$1 = utils.isUndefined;
var isFunction$1 = utils.isFunction;
var isString$1 = utils.isString;
@ -1782,7 +1866,7 @@ Raven.prototype = {
// webpack (using a build step causes webpack #1617). Grunt verifies that
// this value matches package.json during build.
// See: https://github.com/getsentry/raven-js/issues/465
VERSION: '3.24.2',
VERSION: '3.25.2',
debug: false,
@ -2114,6 +2198,23 @@ Raven.prototype = {
if (isErrorEvent$1(ex) && ex.error) {
// If it is an ErrorEvent with `error` property, extract it to get actual Error
ex = ex.error;
} else if (isDOMError$1(ex) || isDOMException$1(ex)) {
// If it is a DOMError or DOMException (which are legacy APIs, but still supported in some browsers)
// then we just extract the name and message, as they don't provide anything else
// https://developer.mozilla.org/en-US/docs/Web/API/DOMError
// https://developer.mozilla.org/en-US/docs/Web/API/DOMException
var name = ex.name || (isDOMError$1(ex) ? 'DOMError' : 'DOMException');
var message = ex.message ? name + ': ' + ex.message : name;
return this.captureMessage(
message,
objectMerge$1(options, {
// neither DOMError or DOMException provide stack trace and we most likely wont get it this way as well
// but it's barely any overhead so we may at least try
stacktrace: true,
trimHeadFrames: options.trimHeadFrames + 1
})
);
} else if (isError$1(ex)) {
// we have a real Error object
ex = ex;
@ -2125,6 +2226,7 @@ Raven.prototype = {
ex = new Error(options.message);
} else {
// If none of previous checks were valid, then it means that
// it's not a DOMError/DOMException
// it's not a plain Object
// it's not a valid ErrorEvent (one with an error property)
// it's not an Error
@ -3073,8 +3175,8 @@ Raven.prototype = {
var hasPushAndReplaceState =
!isChromePackagedApp &&
_window$2.history &&
history.pushState &&
history.replaceState;
_window$2.history.pushState &&
_window$2.history.replaceState;
if (autoBreadcrumbs.location && hasPushAndReplaceState) {
// TODO: remove onpopstate handler on uninstall()
var oldOnPopState = _window$2.onpopstate;
@ -3103,8 +3205,8 @@ Raven.prototype = {
};
};
fill$1(history, 'pushState', historyReplacementFunction, wrappedBuiltIns);
fill$1(history, 'replaceState', historyReplacementFunction, wrappedBuiltIns);
fill$1(_window$2.history, 'pushState', historyReplacementFunction, wrappedBuiltIns);
fill$1(_window$2.history, 'replaceState', historyReplacementFunction, wrappedBuiltIns);
}
if (autoBreadcrumbs.console && 'console' in _window$2 && console.log) {
@ -3320,7 +3422,7 @@ Raven.prototype = {
}
]
},
culprit: fileurl
transaction: fileurl
},
options
);
@ -3394,7 +3496,7 @@ Raven.prototype = {
if (this._hasNavigator && _navigator.userAgent) {
httpData.headers = {
'User-Agent': navigator.userAgent
'User-Agent': _navigator.userAgent
};
}
@ -3435,7 +3537,7 @@ Raven.prototype = {
if (
!last ||
current.message !== last.message || // defined for captureMessage
current.culprit !== last.culprit // defined for captureException/onerror
current.transaction !== last.transaction // defined for captureException/onerror
)
return false;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

581
dist/plyr.js vendored
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 define === 'function' && define.amd ? define('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
loadScript: function loadScript(url) {
return new Promise(function (resolve, reject) {
@ -730,7 +748,7 @@ var utils = {
// Add text node
if (utils.is.string(text)) {
element.textContent = text;
element.innerText = text;
}
// Return built element
@ -884,14 +902,16 @@ var utils = {
},
// Toggle class on an element
toggleClass: function toggleClass(element, className, toggle) {
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
toggleClass: function toggleClass(element, className, force) {
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);
return toggle && !contains || !toggle && contains;
element.classList[method](className);
return element.classList.contains(className);
}
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
closest: function closest(array, value) {
if (!utils.is.array(array) || !array.length) {
@ -2093,7 +2119,7 @@ var controls = {
break;
}
progress.textContent = '% ' + suffix.toLowerCase();
progress.innerText = '% ' + suffix.toLowerCase();
}
this.elements.display[type] = progress;
@ -2167,7 +2193,7 @@ var controls = {
var forceHours = utils.getHours(this.duration) > 0;
// 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
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
@ -2293,7 +2320,7 @@ var controls = {
// Calculate percentage
var percent = 0;
var clientRect = this.elements.inputs.seek.getBoundingClientRect();
var clientRect = this.elements.progress.getBoundingClientRect();
var visible = this.config.classNames.tooltip + '--visible';
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() {
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;
}
@ -3471,7 +3499,7 @@ var captions = {
// Set the span content
if (utils.is.string(caption)) {
content.textContent = caption.trim();
content.innerText = caption.trim();
} else {
content.appendChild(caption);
}
@ -3709,8 +3737,7 @@ var defaults$1 = {
},
youtube: {
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',
poster: 'https://img.youtube.com/vi/{0}/maxresdefault.jpg,https://img.youtube.com/vi/{0}/hqdefault.jpg'
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet'
},
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'
@ -3806,13 +3833,13 @@ var defaults$1 = {
embed: 'plyr__video-embed',
embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster',
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading',
error: 'plyr--has-error',
hover: 'plyr--hover',
tooltip: 'plyr__tooltip',
cues: 'plyr__cues',
@ -4207,8 +4234,10 @@ var ui = {
// Set the title
ui.setTitle.call(this);
// Set the poster image
ui.setPoster.call(this);
// Assure the poster image is set, if the property was added before the element was created
if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) {
ui.setPoster.call(this, this.poster);
}
},
@ -4250,17 +4279,43 @@ var ui = {
},
// Set the poster image
setPoster: function setPoster() {
if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) {
return;
// Toggle poster
togglePoster: function togglePoster(enable) {
utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
},
// 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
var posters = this.poster.split(',');
this.elements.poster.style.backgroundImage = posters.map(function (p) {
return 'url(\'' + p + '\')';
}).join(',');
// Load the image, and set poster if successful
var loadPromise = utils.loadImage(poster).then(function () {
_this2.elements.poster.style.backgroundImage = 'url(\'' + poster + '\')';
Object.assign(_this2.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(_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 +4335,13 @@ var ui = {
}
// Toggle controls
this.toggleControls(!this.playing);
ui.toggleControls.call(this);
},
// Check if media is loading
checkLoading: function checkLoading(event) {
var _this2 = this;
var _this3 = this;
this.loading = ['stalled', 'waiting'].includes(event.type);
@ -4295,38 +4350,24 @@ var ui = {
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(function () {
// Toggle container class hook
utils.toggleClass(_this2.elements.container, _this2.config.classNames.loading, _this2.loading);
// Update progress bar loading class state
utils.toggleClass(_this3.elements.container, _this3.config.classNames.loading, _this3.loading);
// Show controls if loading, hide if done
_this2.toggleControls(_this2.loading);
// Update controls visibility
ui.toggleControls.call(_this3);
}, this.loading ? 250 : 0);
},
// Check if media failed to load
checkFailed: function checkFailed() {
var _this3 = this;
// Toggle controls based on state and `force` argument
toggleControls: function toggleControls(force) {
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) {
utils.toggleClass(this.elements.container, this.config.classNames.loading, false);
utils.toggleClass(this.elements.container, this.config.classNames.error, true);
if (controls$$1 && this.config.hideControls) {
// Show controls if force, loading, paused, or button interaction, otherwise hide
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 +4605,35 @@ var Listeners = function () {
}, 0);
});
// Toggle controls visibility based on mouse movement
if (this.player.config.hideControls) {
// Toggle controls on mouse events and entering fullscreen
utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', function (event) {
_this2.player.toggleControls(event);
});
utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', function (event) {
var controls$$1 = _this2.player.elements.controls;
// 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
@ -4581,7 +4644,7 @@ var Listeners = function () {
var _this3 = this;
// 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);
});
@ -4607,7 +4670,7 @@ var Listeners = function () {
});
// 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);
});
@ -4626,9 +4689,6 @@ var Listeners = function () {
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
// 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 () {
@ -4850,9 +4910,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
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');
// Current time invert
@ -4887,23 +4985,48 @@ var Listeners = function () {
return controls.updateSeekTooltip.call(_this4.player, event);
});
// Toggle controls visibility based on mouse movement
if (this.player.config.hideControls) {
// Watch for cursor over controls so they don't hide when trying to interact
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
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) {
_this4.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
});
// Focus in/out on controls
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
on(this.player.elements.inputs.volume, 'wheel', function (event) {
@ -4955,6 +5078,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 = {
setup: function setup() {
var _this = this;
@ -5050,11 +5181,8 @@ var vimeo = {
// Get original image
url.pathname = url.pathname.split('_')[0] + '.jpg';
// Set attribute
player.media.setAttribute('poster', url.href);
// Update
ui.setPoster.call(player);
// Set and show poster
ui.setPoster.call(player, url.href);
});
// Setup instance
@ -5074,15 +5202,13 @@ var vimeo = {
// Create a faux HTML5 API using the Vimeo API
player.media.play = function () {
player.embed.play().then(function () {
player.media.paused = false;
});
assurePlaybackState.call(player, true);
return player.embed.play();
};
player.media.pause = function () {
player.embed.pause().then(function () {
player.media.paused = true;
});
assurePlaybackState.call(player, false);
return player.embed.pause();
};
player.media.stop = function () {
@ -5098,26 +5224,35 @@ var vimeo = {
return currentTime;
},
set: function set(time) {
// Get current paused state
// Vimeo will automatically play on seek
var paused = player.media.paused;
// Vimeo will automatically play on seek if the video hasn't been played before
// 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
utils.dispatchEvent.call(player, player.media, 'seeking');
media.seeking = true;
utils.dispatchEvent.call(player, media, 'seeking');
// Seek after events
player.embed.setCurrentTime(time).catch(function () {
// If paused, mute until seek is complete
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
});
// Restore pause state
if (paused) {
player.pause();
}
}
});
@ -5265,17 +5400,12 @@ var vimeo = {
});
player.embed.on('play', function () {
// Only fire play if paused before
if (player.media.paused) {
utils.dispatchEvent.call(player, player.media, 'play');
}
player.media.paused = false;
assurePlaybackState.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing');
});
player.embed.on('pause', function () {
player.media.paused = true;
utils.dispatchEvent.call(player, player.media, 'pause');
assurePlaybackState.call(player, false);
});
player.embed.on('timeupdate', function (data) {
@ -5306,7 +5436,6 @@ var vimeo = {
player.embed.on('seeked', function () {
player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked');
utils.dispatchEvent.call(player, player.media, 'play');
});
player.embed.on('ended', function () {
@ -5388,6 +5517,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 = {
setup: function setup() {
var _this = this;
@ -5491,7 +5628,26 @@ var youtube = {
player.media = utils.replaceElement(container, player.media);
// 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
// https://developers.google.com/youtube/iframe_api_reference
@ -5578,10 +5734,12 @@ var youtube = {
// Create a faux HTML5 API using the YouTube API
player.media.play = function () {
assurePlaybackState$1.call(player, true);
instance.playVideo();
};
player.media.pause = function () {
assurePlaybackState$1.call(player, false);
instance.pauseVideo();
};
@ -5599,23 +5757,17 @@ var youtube = {
return Number(instance.getCurrentTime());
},
set: function set(time) {
// Vimeo will automatically play on seek
var paused = player.media.paused;
// Set seeking flag
// If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused) {
player.embed.mute();
}
// Set seeking state and trigger event
player.media.seeking = true;
// Trigger seeking
utils.dispatchEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
// Restore pause state
if (paused) {
player.pause();
}
}
});
@ -5738,6 +5890,14 @@ var youtube = {
// Reset timer
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
// -1 Unstarted
// 0 Ended
@ -5757,7 +5917,7 @@ var youtube = {
break;
case 0:
player.media.paused = true;
assurePlaybackState$1.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
@ -5771,17 +5931,11 @@ var youtube = {
break;
case 1:
// If we were seeking, fire seeked event
if (player.media.seeking) {
utils.dispatchEvent.call(player, player.media, 'seeked');
}
player.media.seeking = false;
// Only fire play if paused before
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (player.media.paused) {
utils.dispatchEvent.call(player, player.media, 'play');
}
player.media.paused = false;
player.media.pause();
} else {
assurePlaybackState$1.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing');
@ -5800,13 +5954,16 @@ var youtube = {
// Get quality
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
}
break;
case 2:
player.media.paused = true;
utils.dispatchEvent.call(player, player.media, 'pause');
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.muted) {
player.embed.unMute();
}
assurePlaybackState$1.call(player, false);
break;
@ -6740,7 +6897,7 @@ var Plyr = function () {
}
// Set config
this.config = utils.extend({}, defaults$1, options || {}, function () {
this.config = utils.extend({}, defaults$1, Plyr.defaults, options || {}, function () {
try {
return JSON.parse(_this.media.getAttribute('data-plyr-config'));
} catch (e) {
@ -7180,114 +7337,35 @@ var Plyr = function () {
/**
* Toggle the player controls
* @param {boolean} toggle - Whether to show the controls
* @param {boolean} [toggle] - Whether to show the controls
*/
}, {
key: 'toggleControls',
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...
if (!utils.is.element(this.elements.controls)) {
return;
// Negate the argument if not undefined since adding the class to hides the controls
var force = typeof toggle === 'undefined' ? undefined : !toggle;
// Apply and get updated state
var hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
// Close menu
if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
controls.toggleMenu.call(this, false);
}
// Don't hide if no UI support or it's audio
if (!this.supported.ui || this.isAudio) {
return;
// Trigger event on change
if (hiding !== isHidden) {
var eventName = hiding ? 'controlshidden' : 'controlsshown';
utils.dispatchEvent.call(this, this.media, eventName);
}
var delay = 0;
var show = toggle;
var isEnterFullscreen = 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);
}
}
// Clear timer on every call
clearTimeout(this.timers.controls);
// If the mouse is not over the controls, set a timeout to hide them
if (show || this.paused || this.loading) {
// Check if controls toggled
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;
}
}
// 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);
return !hiding;
}
return false;
}
/**
@ -7325,7 +7403,7 @@ var Plyr = function () {
}, {
key: 'destroy',
value: function destroy(callback) {
var _this3 = this;
var _this2 = this;
var soft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
@ -7338,22 +7416,22 @@ var Plyr = function () {
document.body.style.overflow = '';
// GC for embed
_this3.embed = null;
_this2.embed = null;
// If it's a soft destroy, make minimal changes
if (soft) {
if (Object.keys(_this3.elements).length) {
if (Object.keys(_this2.elements).length) {
// Remove elements
utils.removeElement(_this3.elements.buttons.play);
utils.removeElement(_this3.elements.captions);
utils.removeElement(_this3.elements.controls);
utils.removeElement(_this3.elements.wrapper);
utils.removeElement(_this2.elements.buttons.play);
utils.removeElement(_this2.elements.captions);
utils.removeElement(_this2.elements.controls);
utils.removeElement(_this2.elements.wrapper);
// Clear for GC
_this3.elements.buttons.play = null;
_this3.elements.captions = null;
_this3.elements.controls = null;
_this3.elements.wrapper = null;
_this2.elements.buttons.play = null;
_this2.elements.captions = null;
_this2.elements.controls = null;
_this2.elements.wrapper = null;
}
// Callback
@ -7362,26 +7440,26 @@ var Plyr = function () {
}
} else {
// Unbind listeners
_this3.listeners.clear();
_this2.listeners.clear();
// 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
utils.dispatchEvent.call(_this3, _this3.elements.original, 'destroyed', true);
utils.dispatchEvent.call(_this2, _this2.elements.original, 'destroyed', true);
// Callback
if (utils.is.function(callback)) {
callback.call(_this3.elements.original);
callback.call(_this2.elements.original);
}
// Reset state
_this3.ready = false;
_this2.ready = false;
// Clear for garbage collection
setTimeout(function () {
_this3.elements = null;
_this3.media = null;
_this2.elements = null;
_this2.media = null;
}, 200);
}
};
@ -7900,10 +7978,7 @@ var Plyr = function () {
return;
}
if (utils.is.string(input)) {
this.media.setAttribute('poster', input);
ui.setPoster.call(this);
}
ui.setPoster.call(this, input);
}
/**
@ -8084,6 +8159,8 @@ var Plyr = function () {
return Plyr;
}();
Plyr.defaults = utils.cloneDeep(defaults$1);
return Plyr;
})));

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,6 +12,8 @@ const concat = require('gulp-concat');
const filter = require('gulp-filter');
const sass = require('gulp-sass');
const cleancss = require('gulp-clean-css');
const postcss = require('gulp-postcss');
const customprops = require('postcss-custom-properties');
const run = require('run-sequence');
const header = require('gulp-header');
const prefix = require('gulp-autoprefixer');
@ -171,6 +173,7 @@ const build = {
.on('error', gutil.log)
.pipe(concat(key))
.pipe(prefix(browsers, { cascade: false }))
.pipe(postcss([customprops()]))
.pipe(cleancss())
.pipe(size(sizeOptions))
.pipe(gulp.dest(paths[bundle].output)),

View File

@ -10,7 +10,7 @@
.plyr__captions {
animation: plyr-fade-in 0.3s ease;
bottom: 0;
color: $plyr-captions-color;
color: var(--plyr-captions-text-color);
display: none;
font-size: $plyr-font-size-captions-small;
left: 0;
@ -22,7 +22,7 @@
width: 100%;
span {
background: $plyr-captions-bg;
background: var(--plyr-captions-background);
border-radius: 2px;
box-decoration-break: clone;
line-height: 185%;

View File

@ -5,21 +5,21 @@
.plyr__control {
background: transparent;
border: 0;
border-radius: $plyr-control-radius;
border-radius: var(--plyr-control-radius);
color: inherit;
cursor: pointer;
flex-shrink: 0;
overflow: visible; // IE11
padding: $plyr-control-padding;
padding: var(--plyr-control-padding);
position: relative;
transition: all 0.3s ease;
svg {
display: block;
fill: currentColor;
height: $plyr-control-icon-size;
height: var(--plyr-control-icon-size);
pointer-events: none;
width: $plyr-control-icon-size;
width: var(--plyr-control-icon-size);
}
// Default focus
@ -46,18 +46,18 @@
&.plyr__tab-focus,
&:hover,
&[aria-expanded='true'] {
background: $plyr-audio-control-bg-hover;
color: $plyr-audio-control-color-hover;
background: var(--plyr-audio-control-bg-hover);
color: var(--plyr-audio-control-color-hover);
}
}
// Large play button (video only)
.plyr__control--overlaid {
background: rgba($plyr-video-control-bg-hover, 0.8);
background: var(--plyr-video-control-bg-hover);
border: 0;
border-radius: 100%;
box-shadow: 0 1px 1px rgba(#000, 0.15);
color: $plyr-video-control-color;
color: var(--plyr-video-control-color);
display: none;
left: 50%;
padding: ceil($plyr-control-spacing * 1.5);
@ -67,15 +67,15 @@
z-index: 2;
svg {
height: $plyr-control-icon-size-large;
height: var(--plyr-control-icon-size-large);
left: 2px; // Offset to make the play button look right
position: relative;
width: $plyr-control-icon-size-large;
width: var(--plyr-control-icon-size-large);
}
&:hover,
&:focus {
background: $plyr-video-control-bg-hover;
background: var(--plyr-video-control-bg-hover);
}
}

131
src/sass/lib/css-vars.scss Normal file
View File

@ -0,0 +1,131 @@
// Downloaded from https://github.com/malyw/css-vars
//// VARIABLES ////
// global map to be filled via variables
$css-vars: ();
// the variable may be set to "true" anywhere in the code,
// so native CSS custom properties will be used instead of the Sass global map
$css-vars-use-native: false !default;
// enables the output of debug messages
$css-vars-debug-log: false !default;
//// FUNCTIONS ////
///
// Assigns a variable to the global map
///
@function _cssVarAssign($varName: null, $varValue: null) {
// CHECK PARAMS
@if ($varName==null) {
@error 'Variable name is expected, instead got: null';
}
@if ($varValue==null) {
@error 'Variable value is expected, instead got: null';
}
// assign to the global map
@if ($css-vars-debug-log and map-get($css-vars, $varName)) {
@debug "'#{$varName}' variable is reassigned";
}
@return map-merge($css-vars, ($varName: $varValue));
}
///
// Emulates var() CSS native function behavior
//
// $args[0] {String} "--" + variable name
// [$args[1]] Optional default value if variable is not assigned yet
//
// E.G.:
// color: var(--main-color);
// background: var(--main-bg, green);
///
@function var($args...) {
// CHECK PARAMS
@if (length($args) ==0) {
@error 'Variable name is expected to be passed to the var() function';
}
@if (str-length(nth($args, 1)) < 2 or str-slice(nth($args, 1), 0, 2) != '--') {
@error "Variable name is expected to start from '--'";
}
// PROCESS
$varName: nth($args, 1);
$varValue: map-get($css-vars, $varName);
@if ($css-vars-debug-log or not $css-vars-use-native) {
// Sass or debug
@if ($varValue==null) {
// variable is not provided so far
@if (length($args) ==2) {
// the default value is passed
@if ($css-vars-debug-log) {
@debug "Provided default value is used for the variable: '#{$varName}'";
}
$varValue: nth($args, 2);
} @else if ($css-vars-debug-log) {
@debug "Variable '#{$varName}' is not assigned";
@if (not $css-vars-use-native) {
@debug "The 'var(#{$varName}...)' usage will be skipped in the output CSS";
}
}
}
}
@if ($css-vars-use-native) {
// CSS variables
// Native CSS: don't process function in case of native
@return unquote('var(' + $args + ')');
} @else {
// Sass: return value from the map
@return $varValue;
}
}
//// MIXIN ////
///
// CSS mixin to provide variables
// E.G.:
// @include css-vars((
// --color: rebeccapurple,
// --height: 68px,
// --margin-top: calc(2vh + 20px)
// ));
///
@mixin css-vars($varMap: null) {
// CHECK PARAMS
@if ($varMap==null) {
@error 'Map of variables is expected, instead got: null';
}
@if (type_of($varMap) !=map) {
@error 'Map of variables is expected, instead got another type passed: #{type_of($varMap)}';
}
// PROCESS
@if ($css-vars-debug-log or not $css-vars-use-native) {
// Sass or debug
// merge variables and values to the global map (provides no output)
@each $varName, $varValue in $varMap {
$css-vars: _cssVarAssign($varName, $varValue) !global; // store in global variable
}
}
@if ($css-vars-use-native) {
// CSS variables
// Native CSS: assign CSS custom properties to the global scope
@at-root :root {
@each $varName, $varValue in $varMap {
@if (type_of($varValue) ==string) {
#{$varName}: $varValue; // to prevent quotes interpolation
} @else {
#{$varName}: #{$varValue};
}
}
}
}
}

View File

@ -5,6 +5,9 @@
// ==========================================================================
@charset 'UTF-8';
@import 'lib/css-vars';
$css-vars-use-native: true;
@import 'settings/breakpoints';
@import 'settings/colors';
@import 'settings/cosmetics';

View File

@ -2,9 +2,17 @@
// Captions
// ==========================================================================
$plyr-captions-bg: rgba(#000, 0.8) !default;
$plyr-captions-color: #fff !default;
$plyr-captions-background: rgba(#000, 0.8) !default;
$plyr-captions-text-color: #fff !default;
$plyr-font-size-captions-base: $plyr-font-size-base !default;
$plyr-font-size-captions-small: $plyr-font-size-small !default;
$plyr-font-size-captions-medium: $plyr-font-size-large !default;
$plyr-font-size-captions-large: $plyr-font-size-xlarge !default;
@include css-vars(
(
--plyr-captions-background: $plyr-captions-background,
--plyr-captions-text-color: $plyr-captions-text-color
)
);

View File

@ -7,3 +7,13 @@ $plyr-color-gunmetal: #2f343d !default;
$plyr-color-fiord: #4f5b5f !default;
$plyr-color-lynch: #6b7d85 !default;
$plyr-color-heather: #b7c5cd !default;
@include css-vars(
(
--plyr-color-main: $plyr-color-main,
--plyr-color-gunmetal: $plyr-color-gunmetal,
--plyr-color-fiord: $plyr-color-fiord,
--plyr-color-lynch: $plyr-color-lynch,
--plyr-color-heather: $plyr-color-heather
)
);

View File

@ -11,9 +11,27 @@ $plyr-control-radius: 3px !default;
$plyr-video-controls-bg: #000 !default;
$plyr-video-control-color: #fff !default;
$plyr-video-control-color-hover: #fff !default;
$plyr-video-control-bg-hover: $plyr-color-main !default;
$plyr-video-control-bg-hover: var(--plyr-color-main) !default;
$plyr-audio-controls-bg: #fff !default;
$plyr-audio-control-color: $plyr-color-fiord !default;
$plyr-audio-control-color: var(--plyr-color-fiord) !default;
$plyr-audio-control-color-hover: #fff !default;
$plyr-audio-control-bg-hover: $plyr-color-main !default;
$plyr-audio-control-bg-hover: var(--plyr-color-main) !default;
@include css-vars(
(
--plyr-control-icon-size: $plyr-control-icon-size,
--plyr-control-icon-size-large: $plyr-control-icon-size-large,
--plyr-control-spacing: $plyr-control-spacing,
--plyr-control-padding: $plyr-control-padding,
--plyr-control-radius: $plyr-control-radius,
--plyr-video-controls-bg: $plyr-video-controls-bg,
--plyr-video-control-color: $plyr-video-control-color,
--plyr-video-control-color-hover: $plyr-video-control-color-hover,
--plyr-video-control-bg-hover: $plyr-video-control-bg-hover,
--plyr-audio-controls-bg: $plyr-audio-controls-bg,
--plyr-audio-control-color: $plyr-audio-control-color,
--plyr-audio-control-color-hover: $plyr-audio-control-color-hover,
--plyr-audio-control-bg-hover: $plyr-audio-control-bg-hover
)
);

View File

@ -3,3 +3,5 @@
// ==========================================================================
$plyr-tab-focus-default-color: $plyr-color-main !default;
@include css-vars((--plyr-tab-focus-default-color: $plyr-tab-focus-default-color));

View File

@ -16,7 +16,7 @@ $plyr-range-track-height: 6px !default;
$plyr-range-max-height: ($plyr-range-thumb-active-shadow-width * 2) + $plyr-range-thumb-height !default;
// Fill
$plyr-range-fill-bg: $plyr-color-main !default;
$plyr-range-fill-bg: var(--plyr-color-main);
// Type specific
$plyr-video-range-track-bg: $plyr-video-progress-buffered-bg !default;