diff options
Diffstat (limited to 'src/js')
-rw-r--r-- | src/js/plyr.js | 800 |
1 files changed, 543 insertions, 257 deletions
diff --git a/src/js/plyr.js b/src/js/plyr.js index d6b16b7b..f01866bd 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v1.6.11 +// plyr.js v1.8.12 // https://github.com/selz/plyr // License: The MIT License (MIT) // ========================================================================== @@ -23,13 +23,13 @@ } }(typeof window !== 'undefined' ? window : this, function(window, document) { 'use strict'; - /*global YT,$f*/ // Globals - var fullscreen, api = {}; + var fullscreen, + scroll = { x: 0, y: 0 }, // Default config - var defaults = { + defaults = { enabled: true, debug: false, autoplay: false, @@ -41,16 +41,20 @@ volumeStep: 1, duration: null, displayDuration: true, + loadSprite: true, iconPrefix: 'plyr', - iconUrl: '', + iconUrl: 'https://cdn.plyr.io/1.8.12/plyr.svg', clickToPlay: true, hideControls: true, showPosterOnEnd: false, + disableContextMenu: true, tooltips: { controls: false, seek: true }, selectors: { + html5: 'video, audio', + embed: '[data-type]', container: '.plyr', controls: { container: null, @@ -141,7 +145,7 @@ // URLs urls: { vimeo: { - api: 'https://cdn.plyr.io/froogaloop/1.0.1/plyr.froogaloop.js', + api: 'https://player.vimeo.com/api/player.js', }, youtube: { api: 'https://www.youtube.com/iframe_api' @@ -170,51 +174,61 @@ // Credits: http://paypal.github.io/accessible-html5-video-player/ // Unfortunately, due to mixed support, UA sniffing is required function _browserSniff() { - var nAgt = navigator.userAgent, + var ua = navigator.userAgent, name = navigator.appName, fullVersion = '' + parseFloat(navigator.appVersion), majorVersion = parseInt(navigator.appVersion, 10), nameOffset, verOffset, - ix; + ix, + isIE = false, + isFirefox = false, + isChrome = false, + isSafari = false; // MSIE 11 if ((navigator.appVersion.indexOf('Windows NT') !== -1) && (navigator.appVersion.indexOf('rv:11') !== -1)) { + isIE = true; name = 'IE'; - fullVersion = '11;'; + fullVersion = '11'; } // MSIE - else if ((verOffset=nAgt.indexOf('MSIE')) !== -1) { + else if ((verOffset = ua.indexOf('MSIE')) !== -1) { + isIE = true; name = 'IE'; - fullVersion = nAgt.substring(verOffset + 5); + fullVersion = ua.substring(verOffset + 5); } // Chrome - else if ((verOffset=nAgt.indexOf('Chrome')) !== -1) { + else if ((verOffset = ua.indexOf('Chrome')) !== -1) { + isChrome = true; name = 'Chrome'; - fullVersion = nAgt.substring(verOffset + 7); + fullVersion = ua.substring(verOffset + 7); } // Safari - else if ((verOffset=nAgt.indexOf('Safari')) !== -1) { + else if ((verOffset = ua.indexOf('Safari')) !== -1) { + isSafari = true; name = 'Safari'; - fullVersion = nAgt.substring(verOffset + 7); - if ((verOffset = nAgt.indexOf('Version')) !== -1) { - fullVersion = nAgt.substring(verOffset + 8); + fullVersion = ua.substring(verOffset + 7); + if ((verOffset = ua.indexOf('Version')) !== -1) { + fullVersion = ua.substring(verOffset + 8); } } // Firefox - else if ((verOffset=nAgt.indexOf('Firefox')) !== -1) { + else if ((verOffset = ua.indexOf('Firefox')) !== -1) { + isFirefox = true; name = 'Firefox'; - fullVersion = nAgt.substring(verOffset + 8); + fullVersion = ua.substring(verOffset + 8); } // In most other browsers, 'name/version' is at the end of userAgent - else if ((nameOffset=nAgt.lastIndexOf(' ') + 1) < (verOffset=nAgt.lastIndexOf('/'))) { - name = nAgt.substring(nameOffset,verOffset); - fullVersion = nAgt.substring(verOffset + 1); + else if ((nameOffset = ua.lastIndexOf(' ') + 1) < (verOffset = ua.lastIndexOf('/'))) { + name = ua.substring(nameOffset,verOffset); + fullVersion = ua.substring(verOffset + 1); if (name.toLowerCase() == name.toUpperCase()) { name = navigator.appName; } } + // Trim the fullVersion string at semicolon/space if present if ((ix = fullVersion.indexOf(';')) !== -1) { fullVersion = fullVersion.substring(0, ix); @@ -222,6 +236,7 @@ if ((ix = fullVersion.indexOf(' ')) !== -1) { fullVersion = fullVersion.substring(0, ix); } + // Get major version majorVersion = parseInt('' + fullVersion, 10); if (isNaN(majorVersion)) { @@ -233,8 +248,12 @@ return { name: name, version: majorVersion, - ios: /(iPad|iPhone|iPod)/g.test(navigator.platform), - touch: 'ontouchstart' in document.documentElement + isIE: isIE, + isFirefox: isFirefox, + isChrome: isChrome, + isSafari: isSafari, + isIos: /(iPad|iPhone|iPod)/g.test(navigator.platform), + isTouch: 'ontouchstart' in document.documentElement }; } @@ -320,6 +339,8 @@ else { parent.appendChild(child); } + + return child; } } @@ -354,7 +375,7 @@ // Set attributes function _setAttributes(element, attributes) { for (var key in attributes) { - element.setAttribute(key, (typeof attributes[key] === 'boolean' && attributes[key]) ? '' : attributes[key]); + element.setAttribute(key, (_is.boolean(attributes[key]) && attributes[key]) ? '' : attributes[key]); } } @@ -401,6 +422,17 @@ return false; } + // Element matches selector + function _matches(element, selector) { + var p = Element.prototype; + + var f = p.matches || p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector || function(s) { + return [].indexOf.call(document.querySelectorAll(s), this) !== -1; + }; + + return f.call(element, selector); + } + // Bind event function _on(element, events, callback, useCapture) { if (element) { @@ -431,7 +463,7 @@ // Whether the listener is a capturing listener or not // Default to false - if (typeof useCapture !== 'boolean') { + if (!_is.boolean(useCapture)) { useCapture = false; } @@ -459,7 +491,7 @@ } // Default bubbles to false - if (typeof bubbles !== 'boolean') { + if (!_is.boolean(bubbles)) { bubbles = false; } @@ -482,7 +514,7 @@ } // Get state - state = (typeof state === 'boolean' ? state : !target.getAttribute('aria-pressed')); + state = (_is.boolean(state) ? state : !target.getAttribute('aria-pressed')); // Set the attribute on target target.setAttribute('aria-pressed', state); @@ -537,6 +569,34 @@ return destination; } + // Check variable types + var _is = { + object: function(input) { + return input !== null && typeof(input) === 'object'; + }, + array: function(input) { + return input !== null && typeof(input) === 'object' && input.constructor === Array; + }, + number: function(input) { + return typeof(input) === 'number' && !isNaN(input - 0) || (typeof input == 'object' && input.constructor === Number); + }, + string: function(input) { + return typeof input === 'string' || (typeof input == 'object' && input.constructor === String); + }, + boolean: function(input) { + return typeof input === 'boolean'; + }, + nodeList: function(input) { + return input instanceof NodeList; + }, + htmlElement: function(input) { + return input instanceof HTMLElement; + }, + undefined: function(input) { + return typeof input === 'undefined'; + } + }; + // Fullscreen API function _fullscreen() { var fullscreen = { @@ -551,7 +611,7 @@ browserPrefixes = 'webkit moz o ms khtml'.split(' '); // Check for native support - if (typeof document.cancelFullScreen !== 'undefined') { + if (!_is.undefined(document.cancelFullScreen)) { fullscreen.supportsFullScreen = true; } else { @@ -559,12 +619,12 @@ for (var i = 0, il = browserPrefixes.length; i < il; i++ ) { fullscreen.prefix = browserPrefixes[i]; - if (typeof document[fullscreen.prefix + 'CancelFullScreen'] !== 'undefined') { + if (!_is.undefined(document[fullscreen.prefix + 'CancelFullScreen'])) { fullscreen.supportsFullScreen = true; break; } // Special case for MS (when isn't it?) - else if (typeof document.msExitFullscreen !== 'undefined' && document.msFullscreenEnabled) { + else if (!_is.undefined(document.msExitFullscreen) && document.msFullscreenEnabled) { fullscreen.prefix = 'ms'; fullscreen.supportsFullScreen = true; break; @@ -579,7 +639,7 @@ fullscreen.fullScreenEventName = (fullscreen.prefix == 'ms' ? 'MSFullscreenChange' : fullscreen.prefix + 'fullscreenchange'); fullscreen.isFullScreen = function(element) { - if (typeof element === 'undefined') { + if (_is.undefined(element)) { element = document.body; } switch (this.prefix) { @@ -592,7 +652,7 @@ } }; fullscreen.requestFullScreen = function(element) { - if (typeof element === 'undefined') { + if (_is.undefined(element)) { element = document.body; } return (this.prefix === '') ? element.requestFullScreen() : element[this.prefix + (this.prefix == 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')](); @@ -651,17 +711,31 @@ _log(config); // Debugging - function _log(text, warn) { + function _log() { + if (config.debug && window.console) { + console.log.apply(console, arguments); + } + } + function _warn() { if (config.debug && window.console) { - console[(warn ? 'warn' : 'log')](text); + console.warn.apply(console, arguments); } } + // Get icon URL + function _getIconUrl() { + return { + url: config.iconUrl, + absolute: (config.iconUrl.indexOf("http") === 0) || plyr.browser.isIE + }; + } + // Build the default HTML function _buildControls() { // Create html array - var html = [], - iconPath = config.iconUrl + '#' + config.iconPrefix; + var html = [], + iconUrl = _getIconUrl(), + iconPath = (!iconUrl.absolute ? iconUrl.url : '') + '#' + config.iconPrefix; // Larger overlaid play button if (_inArray(config.controls, 'play-large')) { @@ -831,7 +905,9 @@ } // Toggle state - _toggleState(plyr.buttons.fullscreen, false); + if (plyr.buttons && plyr.buttons.fullscreen) { + _toggleState(plyr.buttons.fullscreen, false); + } // Setup focus trap _focusTrap(); @@ -840,6 +916,7 @@ // Setup captions function _setupCaptions() { + // Bail if not HTML5 video if (plyr.type !== 'video') { return; } @@ -897,8 +974,8 @@ // Disable unsupported browsers than report false positive // Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1033144 - if ((plyr.browser.name === 'IE' && plyr.browser.version >= 10) || - (plyr.browser.name === 'Firefox' && plyr.browser.version >= 31)) { + if ((plyr.browser.isIE && plyr.browser.version >= 10) || + (plyr.browser.isFirefox && plyr.browser.version >= 31)) { // Debugging _log('Detected browser with known TextTrack issues - using manual fallback'); @@ -971,7 +1048,7 @@ _log('Successfully loaded the caption file via AJAX'); } else { - _log('There was a problem loading the caption file via AJAX', true); + _warn('There was a problem loading the caption file via AJAX'); } } }; @@ -994,12 +1071,12 @@ container.innerHTML = ''; // Default to empty - if (typeof caption === 'undefined') { + if (_is.undefined(caption)) { caption = ''; } // Set the span content - if (typeof caption === 'string') { + if (_is.undefined(caption)) { content.innerHTML = caption.trim(); } else { @@ -1059,7 +1136,7 @@ // Check time is a number, if not use currentTime // IE has a bug where currentTime doesn't go to 0 // https://twitter.com/Sam_Potts/status/573715746506731521 - time = typeof time === 'number' ? time : plyr.media.currentTime; + time = _is.number(time) ? time : plyr.media.currentTime; // If there's no subs available, bail if (!plyr.captions[plyr.subcount]) { @@ -1096,7 +1173,15 @@ _toggleClass(plyr.container, config.classes.captions.enabled, true); - if (config.captions.defaultActive) { + // Try to load the value from storage + var active = plyr.storage.captionsEnabled; + + // Otherwise fall back to the default config + if (!_is.boolean(active)) { + active = config.captions.defaultActive; + } + + if (active) { _toggleClass(plyr.container, config.classes.captions.active, true); _toggleState(plyr.buttons.captions, true); } @@ -1150,7 +1235,7 @@ // Add elements to HTML5 media (source, tracks, etc) function _insertChildElements(type, attributes) { - if (typeof attributes === 'string') { + if (_is.string(attributes)) { _insertElement(type, plyr.media, { src: attributes }); } else if (attributes.constructor === Array) { @@ -1162,6 +1247,20 @@ // Insert controls function _injectControls() { + // Sprite + if (config.loadSprite) { + var iconUrl = _getIconUrl(); + + // Only load external sprite using AJAX + if (iconUrl.absolute) { + _log('AJAX loading absolute SVG sprite' + (plyr.browser.isIE ? ' (due to IE)' : '')); + loadSprite(iconUrl.url, "sprite-plyr"); + } + else { + _log('Sprite will be used as external resource directly'); + } + } + // Make a copy of the html var html = config.html; @@ -1186,13 +1285,13 @@ if (config.selectors.controls.container !== null) { container = config.selectors.controls.container; - if (typeof selector === 'string') { + if (_is.string(container)) { container = document.querySelector(container); } } // Inject into the container by default - if (!(container instanceof HTMLElement)) { + if (!_is.htmlElement(container)) { container = plyr.container } @@ -1259,7 +1358,7 @@ return true; } catch(e) { - _log('It looks like there is a problem with your controls html', true); + _warn('It looks like there is a problem with your controls HTML'); // Restore native video controls _toggleNativeControls(true); @@ -1275,7 +1374,7 @@ // Toggle native controls function _toggleNativeControls(toggle) { - if (toggle) { + if (toggle && _inArray(config.types.html5, plyr.type)) { plyr.media.setAttribute('controls', ''); } else { @@ -1289,7 +1388,7 @@ var label = config.i18n.play; // If there's a media title set, use that for the label - if (typeof(config.title) !== 'undefined' && config.title.length) { + if (!_is.undefined(config.title) && config.title.length) { label += ', ' + config.title; } @@ -1302,17 +1401,64 @@ // Set iframe title // https://github.com/Selz/plyr/issues/124 - if (iframe instanceof HTMLElement) { + if (_is.htmlElement(iframe)) { iframe.setAttribute('title', config.i18n.frameTitle.replace('{title}', config.title)); } } + // Setup localStorage + function _setupStorage() { + var value = null; + plyr.storage = {}; + + // Bail if we don't have localStorage support or it's disabled + if (!_storage().supported || !config.storage.enabled) { + return; + } + + // Clean up old volume + // https://github.com/Selz/plyr/issues/171 + window.localStorage.removeItem('plyr-volume'); + + // load value from the current key + value = window.localStorage.getItem(config.storage.key); + + if (!value) { + // Key wasn't set (or had been cleared), move along + return; + } + else if (/^\d+(\.\d+)?$/.test(value)) { + // If value is a number, it's probably volume from an older + // version of plyr. See: https://github.com/Selz/plyr/pull/313 + // Update the key to be JSON + _updateStorage({volume: parseFloat(value)}); + } + else { + // Assume it's JSON from this or a later version of plyr + plyr.storage = JSON.parse(value); + } + } + + // Save a value back to local storage + function _updateStorage(value) { + // Bail if we don't have localStorage support or it's disabled + if (!_storage().supported || !config.storage.enabled) { + return; + } + + // Update the working copy of the values + _extend(plyr.storage, value); + + window.localStorage.setItem( + config.storage.key, JSON.stringify(plyr.storage)); + } + // Setup media function _setupMedia() { // If there's no media, bail if (!plyr.media) { - _log('No audio or video element found', true); - return false; + _warn('No media element found!'); + return; } if (plyr.supported.full) { @@ -1329,10 +1475,10 @@ _toggleClass(plyr.container, config.classes.stopped, config.autoplay); // Add iOS class - _toggleClass(plyr.container, config.classes.isIos, plyr.browser.ios); + _toggleClass(plyr.container, config.classes.isIos, plyr.browser.isIos); // Add touch class - _toggleClass(plyr.container, config.classes.isTouch, plyr.browser.touch); + _toggleClass(plyr.container, config.classes.isTouch, plyr.browser.isTouch); // Inject the player wrapper if (plyr.type === 'video') { @@ -1382,7 +1528,7 @@ container.setAttribute('id', id); // Setup API - if (typeof YT === 'object' && YT.loaded !== 0) { + if (_is.object(window.YT)) { _youTubeReady(mediaId, container); } else { @@ -1403,45 +1549,35 @@ } // Vimeo else if (plyr.type === 'vimeo') { - // Inject the iframe - var vimeo = document.createElement('iframe'); - - // Watch for iframe load - vimeo.loaded = false; - _on(vimeo, 'load', function() { vimeo.loaded = true; }); - - _setAttributes(vimeo, { - 'src': 'https://player.vimeo.com/video/' + mediaId + '?player_id=' + id + '&api=1&badge=0&byline=0&portrait=0&title=0', - 'id': id, - 'webkitallowfullscreen': '', - 'mozallowfullscreen': '', - 'allowfullscreen': '', - 'frameborder': 0 - }); - - // If full support, we can use custom controls (hiding Vimeos), if not, use Vimeo + // Vimeo needs an extra div to hide controls on desktop (which has full support) if (plyr.supported.full) { - container.appendChild(vimeo); plyr.media.appendChild(container); } else { - plyr.media.appendChild(vimeo); + container = plyr.media; } + // Set ID + container.setAttribute('id', id); + // Load the API if not already - if (!('$f' in window)) { + if (!_is.object(window.Vimeo)) { _injectScript(config.urls.vimeo.api); - } - // Wait for fragaloop load - var vimeoTimer = window.setInterval(function() { - if ('$f' in window && vimeo.loaded) { - window.clearInterval(vimeoTimer); - _vimeoReady.call(vimeo); - } - }, 50); + // Wait for fragaloop load + var vimeoTimer = window.setInterval(function() { + if (_is.object(window.Vimeo)) { + window.clearInterval(vimeoTimer); + _vimeoReady(mediaId, container); + } + }, 50); + } + else { + _vimeoReady(mediaId, container); + } } // Soundcloud + // TODO: Currently unsupported and undocumented else if (plyr.type === 'soundcloud') { // Inject the iframe var soundCloud = document.createElement('iframe'); @@ -1478,8 +1614,10 @@ // Store reference to API plyr.container.plyr.embed = plyr.embed; - // Setup the UI - _setupInterface(); + // Setup the UI if full support + if (plyr.supported.full) { + _setupInterface(); + } // Set title _setTitle(_getElement('iframe')); @@ -1495,7 +1633,7 @@ // Setup instance // https://developers.google.com/youtube/iframe_api_reference - plyr.embed = new YT.Player(container.id, { + plyr.embed = new window.YT.Player(container.id, { videoId: videoId, playerVars: { autoplay: (config.autoplay ? 1 : 0), @@ -1623,86 +1761,95 @@ } // Vimeo ready - function _vimeoReady() { - /* jshint validthis: true */ - plyr.embed = $f(this); - - // Setup on ready - plyr.embed.addEvent('ready', function() { + function _vimeoReady(mediaId, container) { + // Setup instance + // https://github.com/vimeo/player.js + plyr.embed = new window.Vimeo.Player(container.id, { + id: mediaId, + loop: config.loop, + autoplay: config.autoplay, + byline: false, + portrait: false, + title: false + }); - // Create a faux HTML5 API using the Vimeo API - plyr.media.play = function() { - plyr.embed.api('play'); - plyr.media.paused = false; - }; - plyr.media.pause = function() { - plyr.embed.api('pause'); - plyr.media.paused = true; - }; - plyr.media.stop = function() { - plyr.embed.api('stop'); - plyr.media.paused = true; - }; + // Create a faux HTML5 API using the Vimeo API + plyr.media.play = function() { + plyr.embed.play(); + plyr.media.paused = false; + }; + plyr.media.pause = function() { + plyr.embed.pause(); plyr.media.paused = true; - plyr.media.currentTime = 0; - - // Update UI - _embedReady(); + }; + plyr.media.stop = function() { + plyr.embed.stop(); + plyr.media.paused = true; + }; + plyr.media.paused = true; + plyr.media.currentTime = 0; - plyr.embed.api('getCurrentTime', function (value) { - plyr.media.currentTime = value; + // Update UI + _embedReady(); - // Trigger timeupdate - _triggerEvent(plyr.media, 'timeupdate'); - }); + plyr.embed.getCurrentTime().then(function (value) { + plyr.media.currentTime = value; - plyr.embed.api('getDuration', function(value) { - plyr.media.duration = value; + // Trigger timeupdate + _triggerEvent(plyr.media, 'timeupdate'); + }); - // Display duration if available - _displayDuration(); - }); + plyr.embed.getDuration().then(function(value) { + plyr.media.duration = value; - plyr.embed.addEvent('play', function() { - plyr.media.paused = false; - _triggerEvent(plyr.media, 'play'); - _triggerEvent(plyr.media, 'playing'); - }); + // Display duration if available + _displayDuration(); + }); - plyr.embed.addEvent('pause', function() { - plyr.media.paused = true; - _triggerEvent(plyr.media, 'pause'); - }); + // TODO: Captions + /*if (config.captions.defaultActive) { + plyr.embed.enableTextTrack('en'); + }*/ - plyr.embed.addEvent('playProgress', function(data) { - plyr.media.seeking = false; - plyr.media.currentTime = data.seconds; - _triggerEvent(plyr.media, 'timeupdate'); - }); + // Fix keyboard focus issues + // https://github.com/Selz/plyr/issues/317 + plyr.embed.on('loaded', function() { + if(_is.htmlElement(plyr.embed.element) && plyr.supported.full) { + plyr.embed.element.setAttribute('tabindex', '-1'); + } + }); - plyr.embed.addEvent('loadProgress', function(data) { - plyr.media.buffered = data.percent; - _triggerEvent(plyr.media, 'progress'); + plyr.embed.on('play', function() { + plyr.media.paused = false; + _triggerEvent(plyr.media, 'play'); + _triggerEvent(plyr.media, 'playing'); + }); - if (parseInt(data.percent) === 1) { - // Trigger event - _triggerEvent(plyr.media, 'canplaythrough'); - } - }); + plyr.embed.on('pause', function() { + plyr.media.paused = true; + _triggerEvent(plyr.media, 'pause'); + }); - plyr.embed.addEvent('finish', function() { - plyr.media.paused = true; - _triggerEvent(plyr.media, 'ended'); - }); + plyr.embed.on('timeupdate', function(data) { + plyr.media.seeking = false; + plyr.media.currentTime = data.seconds; + _triggerEvent(plyr.media, 'timeupdate'); + }); - // Always seek to 0 - // plyr.embed.api('seekTo', 0); + plyr.embed.on('progress', function(data) { + plyr.media.buffered = data.percent; + _triggerEvent(plyr.media, 'progress'); - // Autoplay - if (config.autoplay) { - plyr.embed.api('play'); + if (parseInt(data.percent) === 1) { + // Trigger event + _triggerEvent(plyr.media, 'canplaythrough'); } }); + + plyr.embed.on('ended', function() { + plyr.media.paused = true; + _triggerEvent(plyr.media, 'ended'); + }); } // Soundcloud ready @@ -1817,7 +1964,7 @@ // Rewind function _rewind(seekTime) { // Use default if needed - if (typeof seekTime !== 'number') { + if (!_is.number(seekTime)) { seekTime = config.seekTime; } _seek(plyr.media.currentTime - seekTime); @@ -1826,7 +1973,7 @@ // Fast forward function _forward(seekTime) { // Use default if needed - if (typeof seekTime !== 'number') { + if (!_is.number(seekTime)) { seekTime = config.seekTime; } _seek(plyr.media.currentTime + seekTime); @@ -1840,11 +1987,11 @@ duration = _getDuration(); // Explicit position - if (typeof input === 'number') { + if (_is.number(input)) { targetTime = input; } // Event - else if (typeof input === 'object' && (input.type === 'input' || input.type === 'change')) { + else if (_is.object(input) && _inArray(['input', 'change'], input.type)) { // It's the seek slider // Seek to the selected time targetTime = ((input.target.value / input.target.max) * duration); @@ -1864,7 +2011,7 @@ // Set the current time // Try/catch incase the media isn't set and we're calling seek() from source() and IE moans try { - plyr.media.currentTime = targetTime.toFixed(1); + plyr.media.currentTime = targetTime.toFixed(4); } catch(e) {} @@ -1878,7 +2025,7 @@ case 'vimeo': // Round to nearest second for vimeo - plyr.embed.api('seekTo', targetTime.toFixed(0)); + plyr.embed.setCurrentTime(targetTime.toFixed(0)); break; case 'soundcloud': @@ -1929,6 +2076,19 @@ _toggleControls(plyr.media.paused); } + // Save scroll position + function _saveScrollPosition() { + scroll = { + x: window.pageXOffset || 0, + y: window.pageYOffset || 0 + }; + } + + // Restore scroll position + function _restoreScrollPosition() { + window.scrollTo(scroll.x, scroll.y); + } + // Toggle fullscreen function _toggleFullscreen(event) { // Check for native support @@ -1942,6 +2102,10 @@ else if (nativeSupport) { // Request fullscreen if (!fullscreen.isFullScreen(plyr.container)) { + // Save scroll position + _saveScrollPosition(); + + // Request full screen fullscreen.requestFullScreen(plyr.container); } // Bail from fullscreen @@ -1982,10 +2146,17 @@ _focusTrap(plyr.isFullscreen); // Set button state - _toggleState(plyr.buttons.fullscreen, plyr.isFullscreen); + if (plyr.buttons && plyr.buttons.fullscreen) { + _toggleState(plyr.buttons.fullscreen, plyr.isFullscreen); + } // Trigger an event - _triggerEvent(plyr.container, plyr.isFullscreen ? 'enterfullscreen' : 'exitfullscreen'); + _triggerEvent(plyr.container, plyr.isFullscreen ? 'enterfullscreen' : 'exitfullscreen', true); + + // Restore scroll position + if (!plyr.isFullscreen && nativeSupport) { + _restoreScrollPosition(); + } } // Bail from faux-fullscreen @@ -1999,7 +2170,7 @@ // Mute function _toggleMute(muted) { // If the method is called without parameter, toggle based on current value - if (typeof muted !== 'boolean') { + if (!_is.boolean(muted)) { muted = !plyr.media.muted; } @@ -2023,9 +2194,6 @@ break; case 'vimeo': - plyr.embed.api('setVolume', plyr.media.muted ? 0 : parseFloat(config.volume / config.volumeMax)); - break; - case 'soundcloud': plyr.embed.setVolume(plyr.media.muted ? 0 : parseFloat(config.volume / config.volumeMax)); break; @@ -2041,17 +2209,9 @@ var max = config.volumeMax, min = config.volumeMin; - // Use default if no value specified - if (typeof volume === 'undefined') { - volume = config.volume; - - if (config.storage.enabled && _storage().supported) { - volume = window.localStorage.getItem(config.storage.key); - - // Clean up old volume - // https://github.com/Selz/plyr/issues/171 - window.localStorage.removeItem('plyr-volume'); - } + // Load volume from storage if no value specified + if (_is.undefined(volume)) { + volume = plyr.storage.volume; } // Use config if all else fails @@ -2078,16 +2238,12 @@ // Embeds if (_inArray(config.types.embed, plyr.type)) { - // YouTube switch(plyr.type) { case 'youtube': plyr.embed.setVolume(plyr.media.volume * 100); break; case 'vimeo': - plyr.embed.api('setVolume', plyr.media.volume); - break; - case 'soundcloud': plyr.embed.setVolume(plyr.media.volume); break; @@ -2107,14 +2263,14 @@ function _increaseVolume() { var volume = plyr.media.muted ? 0 : (plyr.media.volume * config.volumeMax); - _setVolume(volume + config.volumeStep); + _setVolume(volume + (config.volumeStep / 5)); } // Decrease volume function _decreaseVolume() { var volume = plyr.media.muted ? 0 : (plyr.media.volume * config.volumeMax); - _setVolume(volume - config.volumeStep); + _setVolume(volume - (config.volumeStep / 5)); } // Update volume UI and storage @@ -2132,10 +2288,8 @@ } } - // Store the volume in storage - if (config.storage.enabled && _storage().supported && !isNaN(volume)) { - window.localStorage.setItem(config.storage.key, volume); - } + // Update the volume in storage + _updateStorage({volume: volume}); // Toggle class if muted _toggleClass(plyr.container, config.classes.muted, (volume === 0)); @@ -2154,7 +2308,7 @@ } // If the method is called without parameter, toggle based on current value - if (typeof show !== 'boolean') { + if (!_is.boolean(show)) { show = (plyr.container.className.indexOf(config.classes.captions.active) === -1); } @@ -2168,7 +2322,10 @@ _toggleClass(plyr.container, config.classes.captions.active, plyr.captionsEnabled); // Trigger an event - _triggerEvent(plyr.container, plyr.captionsEnabled ? 'captionsenabled' : 'captionsdisabled'); + _triggerEvent(plyr.container, plyr.captionsEnabled ? 'captionsenabled' : 'captionsdisabled', true); + + // Save captions state to localStorage + _updateStorage({captionsEnabled: plyr.captionsEnabled}); } // Check if media is loading @@ -2199,6 +2356,10 @@ // Video playing case 'timeupdate': case 'seeking': + if (plyr.controls.pressed) { + return; + } + value = _getPercentage(plyr.media.currentTime, duration); // Set seek range value only if it's a 'natural' time event @@ -2220,7 +2381,7 @@ return _getPercentage(buffered.end(0), duration); } // YouTube returns between 0 and 1 - else if (typeof buffered === 'number') { + else if (_is.number(buffered)) { return (buffered * 100); } @@ -2242,11 +2403,11 @@ } // Default to 0 - if (typeof value === 'undefined') { + if (_is.undefined(value)) { value = 0; } // Default to buffer or bail - if (typeof progress === 'undefined') { + if (_is.undefined(progress)) { if (plyr.progress && plyr.progress.buffer) { progress = plyr.progress.buffer; } @@ -2256,7 +2417,7 @@ } // One progress element passed - if (progress instanceof HTMLElement) { + if (_is.htmlElement(progress)) { progress.value = value; } // Object of progress + text element @@ -2337,7 +2498,7 @@ // Update seek range and progress function _updateSeekDisplay(time) { // Default to 0 - if (typeof time !== 'number') { + if (!_is.number(time)) { time = 0; } @@ -2408,21 +2569,22 @@ if (!config.hideControls || plyr.type === 'audio') { return; } + var delay = 0, isEnterFullscreen = false, show = toggle; // Default to false if no boolean - if (typeof toggle !== "boolean") { + if (!_is.boolean(toggle)) { if (toggle && toggle.type) { // Is the enter fullscreen event isEnterFullscreen = (toggle.type === 'enterfullscreen'); // Whether to show controls - show = _inArray(['mousemove', 'mouseenter', 'focus'], toggle.type); + show = _inArray(['mousemove', 'touchstart', 'mouseenter', 'focus'], toggle.type); - // Delay hiding on mousemove events - if (toggle.type === 'mousemove') { + // Delay hiding on move events + if (_inArray(['mousemove', 'touchmove'], toggle.type)) { delay = 2000; } @@ -2432,7 +2594,7 @@ } } else { - show = false; + show = _hasClass(plyr.container, config.classes.hideControls); } } @@ -2443,21 +2605,26 @@ if (show || plyr.media.paused) { _toggleClass(plyr.container, config.classes.hideControls, false); - // Always show controls when paused + // Always show controls when paused or if touch if (plyr.media.paused) { return; } + + // Delay for hiding on touch + if (plyr.browser.isTouch) { + delay = 3000; + } } - // If toggle is false or if we're playing (regardless of toggle), then - // set the timer to hide the controls + // If toggle is false or if we're playing (regardless of toggle), + // then set the timer to hide the controls if (!show || !plyr.media.paused) { plyr.timers.hover = window.setTimeout(function() { // If the mouse is over the controls (and not entering fullscreen), bail - if (plyr.controls.active && !isEnterFullscreen) { + if ((plyr.controls.pressed || plyr.controls.hover) && !isEnterFullscreen) { return; } - + _toggleClass(plyr.container, config.classes.hideControls, true); }, delay); } @@ -2466,7 +2633,7 @@ // Add common function to retrieve media source function _source(source) { // If not null or undefined, parse it - if (typeof source !== 'undefined') { + if (!_is.undefined(source)) { _updateSource(source); return; } @@ -2479,7 +2646,7 @@ break; case 'vimeo': - plyr.embed.api('getVideoUrl', function (value) { + plyr.embed.getVideoUrl.then(function (value) { url = value; }); break; @@ -2501,8 +2668,8 @@ // Update source // Sources are not checked for support so be careful function _updateSource(source) { - if (typeof source === 'undefined' || !('sources' in source) || !source.sources.length) { - _log('Invalid source format', true); + if (!_is.object(source) || !('sources' in source) || !source.sources.length) { + _warn('Invalid source format'); return; } @@ -2554,7 +2721,7 @@ } // Check for support - plyr.supported = api.supported(plyr.type); + plyr.supported = supported(plyr.type); // Create new markup switch(plyr.type) { @@ -2578,7 +2745,7 @@ _prependChild(plyr.container, plyr.media); // Autoplay the new source? - if (typeof source.autoplay !== 'undefined') { + if (_is.boolean(source.autoplay)) { config.autoplay = source.autoplay; } @@ -2630,6 +2797,10 @@ // Display duration if available _displayDuration(); } + // If embed but not fully supported, setupInterface now + else if (_inArray(config.types.embed, plyr.type) && !plyr.supported.full) { + _setupInterface(); + } // Set aria title and iframe title config.title = source.title; @@ -2649,7 +2820,7 @@ // Listen for control events function _controlListeners() { // IE doesn't support input event, so we fallback to change - var inputEvent = (plyr.browser.name == 'IE' ? 'change' : 'input'); + var inputEvent = (plyr.browser.isIE ? 'change' : 'input'); // Click play/pause helper function _togglePlay() { @@ -2703,7 +2874,7 @@ for (var button in plyr.buttons) { var element = plyr.buttons[button]; - if (element instanceof NodeList) { + if (_is.nodeList(element)) { for (var i = 0; i < element.length; i++) { _toggleClass(element[i], config.classes.tabFocus, (element[i] === focused)); } @@ -2775,26 +2946,48 @@ // Toggle controls visibility based on mouse movement if (config.hideControls) { // Toggle controls on mouse events and entering fullscreen - _on(plyr.container, 'mouseenter mouseleave mousemove enterfullscreen', _toggleControls); + _on(plyr.container, 'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen', _toggleControls); // Watch for cursor over controls so they don't hide when trying to interact _on(plyr.controls, 'mouseenter mouseleave', function(event) { - plyr.controls.active = (event.type === 'mouseenter'); + plyr.controls.hover = event.type === 'mouseenter'; + }); + + // Watch for cursor over controls so they don't hide when trying to interact + _on(plyr.controls, 'mousedown mouseup touchstart touchend touchcancel', function(event) { + plyr.controls.pressed = _inArray(['mousedown', 'touchstart'], event.type); }); // Focus in/out on controls _on(plyr.controls, 'focus blur', _toggleControls, true); } + // Adjust volume on scroll _on(plyr.volume.input, 'wheel', function(event) { event.preventDefault(); + // Detect "natural" scroll - suppored on OS X Safari only + // Other browsers on OS X will be inverted until support improves + var inverted = event.webkitDirectionInvertedFromDevice; + + // Scroll down (or up on natural) to decrease if (event.deltaY < 0 || event.deltaX > 0) { - _increaseVolume(); + if (inverted) { + _decreaseVolume(); + } + else { + _increaseVolume(); + } } + // Scroll up (or down on natural) to increase if (event.deltaY > 0 || event.deltaX < 0) { - _decreaseVolume(); + if (inverted) { + _increaseVolume(); + } + else { + _decreaseVolume(); + } } }); } @@ -2860,6 +3053,11 @@ // On click play, pause ore restart _on(wrapper, 'click', function() { + // Touch devices will just show controls (if we're hiding controls) + if (config.hideControls && plyr.browser.isTouch && !plyr.media.paused) { + return; + } + if (plyr.media.paused) { _play(); } @@ -2873,6 +3071,11 @@ }); } + // Disable right click + if (config.disableContextMenu) { + _on(plyr.media, 'contextmenu', function(event) { event.preventDefault(); }); + } + // Proxy events to container _on(plyr.media, config.events.join(' '), function(event) { _triggerEvent(plyr.container, event.type, true); @@ -2894,9 +3097,8 @@ // Set blank video src attribute // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error - // Small mp4: https://github.com/mathiasbynens/small/blob/master/mp4.mp4 // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection - plyr.media.setAttribute('src', 'data:video/mp4;base64,AAAAHGZ0eXBpc29tAAACAGlzb21pc28ybXA0MQAAAAhmcmVlAAAAGm1kYXQAAAGzABAHAAABthBgUYI9t+8AAAMNbW9vdgAAAGxtdmhkAAAAAMXMvvrFzL76AAAD6AAAACoAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAABhpb2RzAAAAABCAgIAHAE/////+/wAAAiF0cmFrAAAAXHRraGQAAAAPxcy++sXMvvoAAAABAAAAAAAAACoAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAgAAAAIAAAAAAG9bWRpYQAAACBtZGhkAAAAAMXMvvrFzL76AAAAGAAAAAEVxwAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABaG1pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAShzdGJsAAAAxHN0c2QAAAAAAAAAAQAAALRtcDR2AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAgACABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAAXmVzZHMAAAAAA4CAgE0AAQAEgICAPyARAAAAAAMNQAAAAAAFgICALQAAAbABAAABtYkTAAABAAAAASAAxI2IAMUARAEUQwAAAbJMYXZjNTMuMzUuMAaAgIABAgAAABhzdHRzAAAAAAAAAAEAAAABAAAAAQAAABxzdHNjAAAAAAAAAAEAAAABAAAAAQAAAAEAAAAUc3RzegAAAAAAAAASAAAAAQAAABRzdGNvAAAAAAAAAAEAAAAsAAAAYHVkdGEAAABYbWV0YQAAAAAAAAAhaGRscgAAAAAAAAAAbWRpcmFwcGwAAAAAAAAAAAAAAAAraWxzdAAAACOpdG9vAAAAG2RhdGEAAAABAAAAAExhdmY1My4yMS4x'); + plyr.media.setAttribute('src', 'https://cdn.selz.com/plyr/blank.mp4'); // Load the new empty source // This will cancel existing requests @@ -2967,7 +3169,7 @@ // Get the div placeholder for YouTube and Vimeo if (!plyr.media) { - plyr.media = plyr.container.querySelectorAll('div')[0]; + plyr.media = plyr.container.querySelectorAll('[data-type]')[0]; } // Bail if nothing to setup @@ -2975,6 +3177,9 @@ return; } + // Load saved settings from localStorage + _setupStorage(); + // Get original classname plyr.originalClassName = plyr.container.className; @@ -2997,7 +3202,7 @@ } // Check for support - plyr.supported = api.supported(plyr.type); + plyr.supported = supported(plyr.type); // Add style hook _toggleStyleHook(); @@ -3035,6 +3240,10 @@ _play(); } } + // If embed but not fully supported, setupInterface now (to avoid flash of controls) + else if (_inArray(config.types.embed, plyr.type) && !plyr.supported.full) { + _setupInterface(); + } // Successful setup plyr.init = true; @@ -3043,7 +3252,7 @@ function _setupInterface() { // Don't setup interface if no support if (!plyr.supported.full) { - _log('No full support for this media type (' + plyr.type + ')', true); + _warn('No full support for this media type (' + plyr.type + ')'); // Remove controls _remove(_getElement(config.selectors.controls.wrapper)); @@ -3101,7 +3310,7 @@ _displayDuration(); // Ready event - _triggerEvent(plyr.container, 'ready'); + _triggerEvent(plyr.container, 'ready', true); } // Initialize instance @@ -3127,95 +3336,166 @@ toggleMute: _toggleMute, toggleCaptions: _toggleCaptions, toggleFullscreen: _toggleFullscreen, + toggleControls: _toggleControls, isFullscreen: function() { return plyr.isFullscreen || false; }, support: function(mimeType) { return _supportMime(plyr, mimeType); }, destroy: _destroy, - restore: _init + restore: _init, + getCurrentTime: function() { return plyr.media.currentTime; } }; } + // Load a sprite + function loadSprite(url, id) { + var x = new XMLHttpRequest(); + + // If the id is set and sprite exists, bail + if (_is.string(id) && document.querySelector('#' + id) !== null) { + return; + } + + // Check for CORS support + if ('withCredentials' in x) { + x.open('GET', url, true); + } + else { + return; + } + + // Inject hidden div with sprite on load + x.onload = function() { + var c = document.createElement('div'); + c.setAttribute('hidden', ''); + if (_is.string(id)) { + c.setAttribute('id', id); + } + c.innerHTML = x.responseText; + document.body.insertBefore(c, document.body.childNodes[0]); + } + + x.send(); + } + // Check for support - api.supported = function(type) { - var browser = _browserSniff(), - oldIE = (browser.name === 'IE' && browser.version <= 9), - iPhone = /iPhone|iPod/i.test(navigator.userAgent), - audio = !!document.createElement('audio').canPlayType, - video = !!document.createElement('video').canPlayType, + function supported(type) { + var browser = _browserSniff(), + isOldIE = (browser.isIE && browser.version <= 9), + isIos = browser.isIos, + isIphone = /iPhone|iPod/i.test(navigator.userAgent), + audio = !!document.createElement('audio').canPlayType, + video = !!document.createElement('video').canPlayType, basic, full; switch (type) { case 'video': basic = video; - full = (basic && (!oldIE && !iPhone)); + full = (basic && (!isOldIE && !isIphone)); break; case 'audio': basic = audio; - full = (basic && !oldIE); + full = (basic && !isOldIE); break; case 'vimeo': case 'youtube': case 'soundcloud': basic = true; - full = (!oldIE && !iPhone); + full = (!isOldIE && !isIos); break; default: basic = (audio && video); - full = (basic && !oldIE); + full = (basic && !isOldIE); } return { basic: basic, full: full }; - }; + } - // Expose setup function - api.setup = function(elements, options) { + // Setup function + function setup(targets, options) { // Get the players - var instances = []; + var elements = [], + containers = [], + selector = [defaults.selectors.html5, defaults.selectors.embed].join(','); // Select the elements // Assume elements is a NodeList by default - if (typeof elements === 'string') { - elements = document.querySelectorAll(elements); + if (_is.string(targets)) { + targets = document.querySelectorAll(targets); } // Single HTMLElement passed - else if (elements instanceof HTMLElement) { - elements = [elements]; + else if (_is.htmlElement(targets)) { + targets = [targets]; } // No selector passed, possibly options as first argument - else if (!(elements instanceof NodeList) && typeof elements !== 'string') { + else if (!_is.nodeList(targets) && !_is.array(targets) && !_is.string(targets)) { // If options are the first argument - if (typeof options === 'undefined' && typeof elements === 'object') { - options = elements; + if (_is.undefined(options) && _is.object(targets)) { + options = targets; } // Use default selector - elements = document.querySelectorAll(defaults.selectors.container); + targets = document.querySelectorAll(selector); + } + + // Convert NodeList to array + if (_is.nodeList(targets)) { + targets = Array.prototype.slice.call(targets); } // Bail if disabled or no basic support // You may want to disable certain UAs etc - if (!api.supported().basic || !elements.length) { + if (!supported().basic || !targets.length) { return false; } + // Check if the targets have multiple media elements + for (var i = 0; i < targets.length; i++) { + var target = targets[i]; + + // Get children + var children = target.querySelectorAll(selector); + + // If there's more than one media element, wrap them + if (children.length > 1) { + for (var x = 0; x < children.length; x++) { + containers.push({ + element: _wrap(children[x], document.createElement('div')), + original: target + }); + } + } + else { + containers.push({ + element: target + }); + } + } + // Create a player instance for each element - for (var i = 0; i < elements.length; i++) { - // Get the current element - var element = elements[i]; + for (var key in containers) { + var element = containers[key].element, + original = containers[key].original || element; + + // Wrap each media element if is target is media element + // as opposed to a wrapper + if (_matches(element, selector)) { + // Wrap in a <div> + element = _wrap(element, document.createElement('div')); + } // Setup a player instance and add to the element - if (typeof element.plyr === 'undefined') { + if (!('plyr' in element)) { // Create instance-specific config - var config = _extend(defaults, options, JSON.parse(element.getAttribute("data-plyr"))); + var config = _extend({}, defaults, options, JSON.parse(original.getAttribute('data-plyr'))); // Bail if not enabled if (!config.enabled) { - return; + return null; } // Create new instance @@ -3225,27 +3505,33 @@ element.plyr = (Object.keys(instance).length ? instance : false); // Callback - _triggerEvent(element, 'setup', { plyr: element.plyr }); + _triggerEvent(original, 'setup', true, { + plyr: element.plyr + }); } // Add to return array even if it's already setup - instances.push(element.plyr); + elements.push(element); } - return instances; - }; + return elements; + } - return api; + return { + setup: setup, + supported: supported, + loadSprite: loadSprite + }; })); // Custom event polyfill // https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent (function () { if (typeof window.CustomEvent === 'function') { - return false; + return; } - function CustomEvent (event, params) { + function CustomEvent(event, params) { params = params || { bubbles: false, cancelable: false, detail: undefined }; var evt = document.createEvent('CustomEvent'); evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); |