diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/js/plyr.js | 2869 | ||||
-rw-r--r-- | src/less/plyr.less | 298 | ||||
-rw-r--r-- | src/less/variables.less | 13 | ||||
-rw-r--r-- | src/scss/plyr.scss | 26 | ||||
-rw-r--r-- | src/sprite/plyr-airplay.svg | 7 | ||||
-rw-r--r-- | src/sprite/plyr-pip.svg | 7 | ||||
-rw-r--r-- | src/sprite/plyr-settings.svg | 6 |
7 files changed, 2111 insertions, 1115 deletions
diff --git a/src/js/plyr.js b/src/js/plyr.js index d86a2ecb..d797f66d 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,22 +1,24 @@ // ========================================================================== // Plyr -// plyr.js v2.0.12 +// plyr.js v2.0.10 // https://github.com/selz/plyr // License: The MIT License (MIT) // ========================================================================== // Credits: http://paypal.github.io/accessible-html5-video-player/ // ========================================================================== -;(function(root, factory) { +(function(root, factory) { 'use strict'; - /*global define,module*/ + /* global define,module */ if (typeof module === 'object' && typeof module.exports === 'object') { // Node, CommonJS-like module.exports = factory(root, document); } else if (typeof define === 'function' && define.amd) { // AMD - define([], function () { return factory(root, document); }); + define([], function() { + return factory(root, document); + }); } else { // Browser globals (root is window) root.plyr = factory(root, document); @@ -25,176 +27,213 @@ 'use strict'; // Globals - var fullscreen, - scroll = { x: 0, y: 0 }, - - // Default config - defaults = { - enabled: true, - debug: false, - autoplay: false, - loop: false, - seekTime: 10, - volume: 10, - volumeMin: 0, - volumeMax: 10, - volumeStep: 1, - duration: null, - displayDuration: true, - loadSprite: true, - iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/2.0.12/plyr.svg', - blankUrl: 'https://cdn.selz.com/plyr/blank.mp4', - clickToPlay: true, - hideControls: true, - showPosterOnEnd: false, - disableContextMenu: true, - keyboardShorcuts: { - focused: true, - global: false - }, - tooltips: { - controls: false, - seek: true + var scroll = { + x: 0, + y: 0 }, - selectors: { - html5: 'video, audio', - embed: '[data-type]', - editable: 'input, textarea, select, [contenteditable]', - container: '.plyr', - controls: { - container: null, - wrapper: '.plyr__controls' + + // Default config + defaults = { + enabled: true, + debug: false, + autoplay: false, + loop: { + active: false, + start: 0, + end: null, + indicator: { + start: 0, + end: 0 + } }, - labels: '[data-plyr]', - buttons: { - seek: '[data-plyr="seek"]', - play: '[data-plyr="play"]', - pause: '[data-plyr="pause"]', - restart: '[data-plyr="restart"]', - rewind: '[data-plyr="rewind"]', - forward: '[data-plyr="fast-forward"]', - mute: '[data-plyr="mute"]', - captions: '[data-plyr="captions"]', - fullscreen: '[data-plyr="fullscreen"]' + seekTime: 10, + volume: 10, + volumeMin: 0, + volumeMax: 10, + volumeStep: 1, + defaultSpeed: 1.0, + currentSpeed: 1, + speeds: [0.5, 1.0, 1.5, 2.0], + duration: null, + displayDuration: true, + loadSprite: true, + iconPrefix: 'plyr', + iconUrl: 'https://cdn.plyr.io/2.0.10/plyr.svg', + clickToPlay: true, + hideControls: true, + showPosterOnEnd: false, + disableContextMenu: true, + keyboardShortcuts: { + focused: true, + global: false }, - volume: { - input: '[data-plyr="volume"]', - display: '.plyr__volume--display' + tooltips: { + controls: false, + seek: true }, - progress: { - container: '.plyr__progress', - buffer: '.plyr__progress--buffer', - played: '.plyr__progress--played' + selectors: { + html5: 'video, audio', + embed: '[data-type]', + editable: 'input, textarea, select, [contenteditable]', + container: '.plyr', + controls: { + container: null, + wrapper: '.plyr__controls' + }, + labels: '[data-plyr]', + buttons: { + seek: '[data-plyr="seek"]', + play: '[data-plyr="play"]', + pause: '[data-plyr="pause"]', + restart: '[data-plyr="restart"]', + rewind: '[data-plyr="rewind"]', + forward: '[data-plyr="fast-forward"]', + mute: '[data-plyr="mute"]', + captions: '[data-plyr="captions"]', + fullscreen: '[data-plyr="fullscreen"]', + settings: '[data-plyr="settings"]', + pip: '[data-plyr="pip"]', + airplay: '[data-plyr="airplay"]', + speed: '[data-plyr="speed"]', + loop: '[data-plyr="loop"]' + }, + volume: { + input: '[data-plyr="volume"]', + display: '.plyr__volume--display' + }, + progress: { + container: '.plyr__progress', + buffer: '.plyr__progress--buffer', + played: '.plyr__progress--played', + looped: '.plyr__progress-loop' + }, + captions: '.plyr__captions', + currentTime: '.plyr__time--current', + duration: '.plyr__time--duration', + menu: { + quality: '.js-plyr__menu__list--quality' + } + }, + classes: { + setup: 'plyr--setup', + ready: 'plyr--ready', + videoWrapper: 'plyr__video-wrapper', + embedWrapper: 'plyr__video-embed', + type: 'plyr--{0}', + stopped: 'plyr--stopped', + playing: 'plyr--playing', + muted: 'plyr--muted', + loading: 'plyr--loading', + hover: 'plyr--hover', + tooltip: 'plyr__tooltip', + hidden: 'plyr__sr-only', + hideControls: 'plyr--hide-controls', + isIos: 'plyr--is-ios', + isTouch: 'plyr--is-touch', + captions: { + enabled: 'plyr--captions-enabled', + active: 'plyr--captions-active' + }, + fullscreen: { + enabled: 'plyr--fullscreen-enabled', + active: 'plyr--fullscreen-active' + }, + pip: { + enabled: 'plyr--pip-enabled', + active: 'plyr--pip-active' + }, + tabFocus: 'tab-focus' }, - captions: '.plyr__captions', - currentTime: '.plyr__time--current', - duration: '.plyr__time--duration' - }, - classes: { - setup: 'plyr--setup', - ready: 'plyr--ready', - videoWrapper: 'plyr__video-wrapper', - embedWrapper: 'plyr__video-embed', - type: 'plyr--{0}', - stopped: 'plyr--stopped', - playing: 'plyr--playing', - muted: 'plyr--muted', - loading: 'plyr--loading', - hover: 'plyr--hover', - tooltip: 'plyr__tooltip', - hidden: 'plyr__sr-only', - hideControls: 'plyr--hide-controls', - isIos: 'plyr--is-ios', - isTouch: 'plyr--is-touch', captions: { - enabled: 'plyr--captions-enabled', - active: 'plyr--captions-active' + defaultActive: false, + selectedIndex: 0 }, fullscreen: { - enabled: 'plyr--fullscreen-enabled', - active: 'plyr--fullscreen-active' + enabled: true, + fallback: true, + allowAudio: false }, - tabFocus: 'tab-focus' - }, - captions: { - defaultActive: false - }, - fullscreen: { - enabled: true, - fallback: true, - allowAudio: false - }, - storage: { - enabled: true, - key: 'plyr' - }, - controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'fullscreen'], - i18n: { - restart: 'Restart', - rewind: 'Rewind {seektime} secs', - play: 'Play', - pause: 'Pause', - forward: 'Forward {seektime} secs', - played: 'played', - buffered: 'buffered', - currentTime: 'Current time', - duration: 'Duration', - volume: 'Volume', - toggleMute: 'Toggle Mute', - toggleCaptions: 'Toggle Captions', - toggleFullscreen: 'Toggle Fullscreen', - frameTitle: 'Player for {title}' - }, - types: { - embed: ['youtube', 'vimeo', 'soundcloud'], - html5: ['video', 'audio'] - }, - // URLs - urls: { - vimeo: { - api: 'https://player.vimeo.com/api/player.js', + storage: { + enabled: true, + key: 'plyr' }, - youtube: { - api: 'https://www.youtube.com/iframe_api' + controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'], + i18n: { + restart: 'Restart', + rewind: 'Rewind {seektime} secs', + play: 'Play', + pause: 'Pause', + forward: 'Forward {seektime} secs', + played: 'played', + buffered: 'buffered', + currentTime: 'Current time', + duration: 'Duration', + volume: 'Volume', + toggleMute: 'Toggle Mute', + toggleCaptions: 'Toggle Captions', + toggleFullscreen: 'Toggle Fullscreen', + frameTitle: 'Player for {title}', + captions: 'Captions', + settings: 'Settings', + speed: 'Speed', + quality: 'Quality', + loop: 'Loop', + loopStart: 'Loop start', + loopEnd: 'Loop end', + loopAll: 'Loop all', + loopNone: 'No Loop', }, - soundcloud: { - api: 'https://w.soundcloud.com/player/api.js' - } - }, - // Custom control listeners - listeners: { - seek: null, - play: null, - pause: null, - restart: null, - rewind: null, - forward: null, - mute: null, - volume: null, - captions: null, - fullscreen: null - }, - // Events to watch on HTML5 media elements - events: ['ready', 'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied'], - // Logging - logPrefix: '[Plyr]' - }; + types: { + embed: ['youtube', 'vimeo', 'soundcloud'], + html5: ['video', 'audio'] + }, + // URLs + urls: { + vimeo: { + api: 'https://player.vimeo.com/api/player.js', + }, + youtube: { + api: 'https://www.youtube.com/iframe_api' + }, + soundcloud: { + api: 'https://w.soundcloud.com/player/api.js' + } + }, + // Custom control listeners + listeners: { + seek: null, + play: null, + pause: null, + restart: null, + rewind: null, + forward: null, + mute: null, + volume: null, + captions: null, + fullscreen: null, + speed: null, + loop: null + }, + // Events to watch on HTML5 media elements + events: ['ready', 'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied'], + // Logging + logPrefix: '' + }; // Credits: http://paypal.github.io/accessible-html5-video-player/ // Unfortunately, due to mixed support, UA sniffing is required - function _browserSniff() { - var ua = navigator.userAgent, - name = navigator.appName, - fullVersion = '' + parseFloat(navigator.appVersion), - majorVersion = parseInt(navigator.appVersion, 10), - nameOffset, - verOffset, - ix, - isIE = false, - isFirefox = false, - isChrome = false, - isSafari = false; + function getBrowser() { + var ua = navigator.userAgent; + var name = navigator.appName; + var fullVersion = '' + parseFloat(navigator.appVersion); + var majorVersion = parseInt(navigator.appVersion, 10); + var nameOffset; + var verOffset; + var ix; + var isIE = false; + var isFirefox = false; + var isChrome = false; + var isSafari = false; if ((navigator.appVersion.indexOf('Windows NT') !== -1) && (navigator.appVersion.indexOf('rv:11') !== -1)) { // MSIE 11 @@ -216,6 +255,7 @@ isSafari = true; name = 'Safari'; fullVersion = ua.substring(verOffset + 7); + if ((verOffset = ua.indexOf('Version')) !== -1) { fullVersion = ua.substring(verOffset + 8); } @@ -226,7 +266,7 @@ fullVersion = ua.substring(verOffset + 8); } else if ((nameOffset = ua.lastIndexOf(' ') + 1) < (verOffset = ua.lastIndexOf('/'))) { // In most other browsers, 'name/version' is at the end of userAgent - name = ua.substring(nameOffset,verOffset); + name = ua.substring(nameOffset, verOffset); fullVersion = ua.substring(verOffset + 1); if (name.toLowerCase() === name.toUpperCase()) { @@ -251,68 +291,42 @@ // Return data return { - name: name, - version: majorVersion, - isIE: isIE, - isFirefox: isFirefox, - isChrome: isChrome, - isSafari: isSafari, - isIos: /(iPad|iPhone|iPod)/g.test(navigator.platform), - isIphone: /(iPhone|iPod)/g.test(navigator.userAgent), - isTouch: 'ontouchstart' in document.documentElement + name: name, + version: majorVersion, + isIE: isIE, + isFirefox: isFirefox, + isChrome: isChrome, + isSafari: isSafari, + isIos: /(iPad|iPhone|iPod)/g.test(navigator.platform), + isTouch: 'ontouchstart' in document.documentElement }; } - // Check for mime type support against a player instance - // Credits: http://diveintohtml5.info/everything.html - // Related: http://www.leanbackplyr.com/test/h5mt.html - function _supportMime(plyr, mimeType) { - var media = plyr.media; - - if (plyr.type === 'video') { - // Check type - switch (mimeType) { - case 'video/webm': return !!(media.canPlayType && media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, '')); - case 'video/mp4': return !!(media.canPlayType && media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, '')); - case 'video/ogg': return !!(media.canPlayType && media.canPlayType('video/ogg; codecs="theora"').replace(/no/, '')); - } - } else if (plyr.type === 'audio') { - // Check type - switch (mimeType) { - case 'audio/mpeg': return !!(media.canPlayType && media.canPlayType('audio/mpeg;').replace(/no/, '')); - case 'audio/ogg': return !!(media.canPlayType && media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, '')); - case 'audio/wav': return !!(media.canPlayType && media.canPlayType('audio/wav; codecs="1"').replace(/no/, '')); - } - } - - // If we got this far, we're stuffed - return false; - } - // Inject a script - function _injectScript(source) { + function injectScript(source) { if (document.querySelectorAll('script[src="' + source + '"]').length) { return; } var tag = document.createElement('script'); tag.src = source; + var firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); } // Element exists in an array - function _inArray(haystack, needle) { + function inArray(haystack, needle) { return Array.prototype.indexOf && (haystack.indexOf(needle) !== -1); } // Replace all - function _replaceAll(string, find, replace) { + function replaceAll(string, find, replace) { return string.replace(new RegExp(find.replace(/([.*+?\^=!:${}()|\[\]\/\\])/g, '\\$1'), 'g'), replace); } // Wrap an element - function _wrap(elements, wrapper) { + function wrap(elements, wrapper) { // Convert `elements` to an array, if necessary. if (!elements.length) { elements = [elements]; @@ -321,11 +335,11 @@ // Loops backwards to prevent having to clone the wrapper on the // first element (see `child` below). for (var i = elements.length - 1; i >= 0; i--) { - var child = (i > 0) ? wrapper.cloneNode(true) : wrapper; + var child = (i > 0) ? wrapper.cloneNode(true) : wrapper; var element = elements[i]; // Cache the current parent and sibling. - var parent = element.parentNode; + var parent = element.parentNode; var sibling = element.nextSibling; // Wrap the element (is automatically removed from its current @@ -345,23 +359,8 @@ } } - // Unwrap an element - // http://plainjs.com/javascript/manipulation/unwrap-a-dom-element-35/ - /*function _unwrap(wrapper) { - // Get the element's parent node - var parent = wrapper.parentNode; - - // Move all children out of the element - while (wrapper.firstChild) { - parent.insertBefore(wrapper.firstChild, wrapper); - } - - // Remove the empty element - parent.removeChild(wrapper); - }*/ - // Remove an element - function _remove(element) { + function remove(element) { if (!element) { return; } @@ -369,36 +368,36 @@ } // Prepend child - function _prependChild(parent, element) { + function prependChild(parent, element) { parent.insertBefore(element, parent.firstChild); } // Set attributes - function _setAttributes(element, attributes) { + function setAttributes(element, attributes) { for (var key in attributes) { - element.setAttribute(key, (_is.boolean(attributes[key]) && attributes[key]) ? '' : attributes[key]); + element.setAttribute(key, (is.boolean(attributes[key]) && attributes[key]) ? '' : attributes[key]); } } // Insert a HTML element - function _insertElement(type, parent, attributes) { + function insertElement(type, parent, attributes) { // Create a new <element> var element = document.createElement(type); // Set all passed attributes - _setAttributes(element, attributes); + setAttributes(element, attributes); // Inject the new element - _prependChild(parent, element); + prependChild(parent, element); } // Get a classname from selector - function _getClassname(selector) { + function getClassname(selector) { return selector.replace('.', ''); } // Toggle class on an element - function _toggleClass(element, className, state) { + function toggleClass(element, className, state) { if (element) { if (element.classList) { element.classList[state ? 'add' : 'remove'](className); @@ -410,7 +409,7 @@ } // Has class name - function _hasClass(element, className) { + function hasClass(element, className) { if (element) { if (element.classList) { return element.classList.contains(className); @@ -422,19 +421,23 @@ } // 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; - }; + function matches(element, selector) { + var prototype = Element.prototype; + + var matches = prototype.matches || + prototype.webkitMatchesSelector || + prototype.mozMatchesSelector || + prototype.msMatchesSelector || + function(s) { + return [].indexOf.call(document.querySelectorAll(s), this) !== -1; + }; - return f.call(element, selector); + return matches.call(element, selector); } // Bind along with custom handler - function _proxyListener(element, eventName, userListener, defaultListener, useCapture) { - _on(element, eventName, function(event) { + function proxy(element, eventName, userListener, defaultListener, useCapture) { + on(element, eventName, function(event) { if (userListener) { userListener.apply(element, [event]); } @@ -443,20 +446,20 @@ } // Toggle event listener - function _toggleListener(element, events, callback, toggle, useCapture) { + function toggleListener(elements, events, callback, toggle, useCapture) { var eventList = events.split(' '); // Whether the listener is a capturing listener or not // Default to false - if (!_is.boolean(useCapture)) { + if (!is.boolean(useCapture)) { useCapture = false; } // If a nodelist is passed, call itself on each node - if (element instanceof NodeList) { - for (var x = 0; x < element.length; x++) { - if (element[x] instanceof Node) { - _toggleListener(element[x], arguments[1], arguments[2], arguments[3]); + if (elements instanceof NodeList) { + for (var x = 0; x < elements.length; x++) { + if (elements[x] instanceof Node) { + toggleListener(elements[x], arguments[1], arguments[2], arguments[3]); } } return; @@ -464,40 +467,40 @@ // If a single node is passed, bind the event listener for (var i = 0; i < eventList.length; i++) { - element[toggle ? 'addEventListener' : 'removeEventListener'](eventList[i], callback, useCapture); + elements[toggle ? 'addEventListener' : 'removeEventListener'](eventList[i], callback, useCapture); } } - // Bind event - function _on(element, events, callback, useCapture) { - if (element) { - _toggleListener(element, events, callback, true, useCapture); + // Bind event handler + function on(element, events, callback, useCapture) { + if (!is.undefined(element)) { + toggleListener(element, events, callback, true, useCapture); } } - // Unbind event - /*function _off(element, events, callback, useCapture) { - if (element) { - _toggleListener(element, events, callback, false, useCapture); + // Unbind event handler + function off(element, events, callback, useCapture) { + if (!is.undefined(element)) { + toggleListener(element, events, callback, false, useCapture); } - }*/ + } // Trigger event - function _event(element, type, bubbles, properties) { + function event(element, type, bubbles, properties) { // Bail if no element if (!element || !type) { return; } // Default bubbles to false - if (!_is.boolean(bubbles)) { + if (!is.boolean(bubbles)) { bubbles = false; } // Create and dispatch the event var event = new CustomEvent(type, { - bubbles: bubbles, - detail: properties + bubbles: bubbles, + detail: properties }); // Dispatch the event @@ -506,14 +509,14 @@ // Toggle aria-pressed state on a toggle button // http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles - function _toggleState(target, state) { + function toggleState(target, state) { // Bail if no target if (!target) { return; } // Get state - state = (_is.boolean(state) ? state : !target.getAttribute('aria-pressed')); + state = (is.boolean(state) ? state : !target.getAttribute('aria-pressed')); // Set the attribute on target target.setAttribute('aria-pressed', state); @@ -522,7 +525,7 @@ } // Get percentage - function _getPercentage(current, max) { + function getPercentage(current, max) { if (current === 0 || max === 0 || isNaN(current) || isNaN(max)) { return 0; } @@ -532,7 +535,7 @@ // Deep extend/merge destination object with N more objects // http://andrewdupont.net/2009/08/28/deep-extending-objects-in-javascript/ // Removed call to arguments.callee (used explicit function name instead) - function _extend() { + function extend() { // Get arguments var objects = arguments; @@ -548,7 +551,7 @@ // First object is the destination var destination = Array.prototype.shift.call(objects), - length = objects.length; + length = objects.length; // Loop through all objects to merge for (var i = 0; i < length; i++) { @@ -557,7 +560,7 @@ for (var property in source) { if (source[property] && source[property].constructor && source[property].constructor === Object) { destination[property] = destination[property] || {}; - _extend(destination[property], source[property]); + extend(destination[property], source[property]); } else { destination[property] = source[property]; } @@ -568,12 +571,12 @@ } // Check variable types - var _is = { + var is = { object: function(input) { - return input !== null && typeof(input) === 'object'; + return input !== null && typeof(input) === 'object' && input.constructor === Object; }, array: function(input) { - return input !== null && (typeof(input) === 'object' && input.constructor === Array); + return input !== null && typeof(input) === 'object' && input.constructor === Array; }, number: function(input) { return input !== null && (typeof(input) === 'number' && !isNaN(input - 0) || (typeof input === 'object' && input.constructor === Number)); @@ -593,64 +596,67 @@ function: function(input) { return input !== null && typeof input === 'function'; }, + event: function(input) { + return input !== null && typeof input === 'object' && (input.constructor === Event || input.constructor === CustomEvent); + }, undefined: function(input) { return input !== null && typeof input === 'undefined'; + }, + empty: function(input) { + return input === null || this.undefined(input) || ((this.string(input) || this.array(input) || this.nodeList(input)) && input.length === 0) || (this.object(input) && Object.keys(input).length === 0); } }; // Parse YouTube ID from url - function _parseYouTubeId(url) { + function parseYouTubeId(url) { var regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; return (url.match(regex)) ? RegExp.$2 : url; } // Parse Vimeo ID from url - function _parseVimeoId(url) { + function parseVimeoId(url) { var regex = /^.*(vimeo.com\/|video\/)(\d+).*/; return (url.match(regex)) ? RegExp.$2 : url; } // Fullscreen API - function _fullscreen() { - var fullscreen = { - supportsFullScreen: false, - isFullScreen: function() { return false; }, - requestFullScreen: function() {}, - cancelFullScreen: function() {}, - fullScreenEventName: '', - element: null, - prefix: '' - }, - browserPrefixes = 'webkit o moz ms khtml'.split(' '); - - // Check for native support - if (!_is.undefined(document.cancelFullScreen)) { - fullscreen.supportsFullScreen = true; - } else { - // Check for fullscreen support by vendor prefix - for (var i = 0, il = browserPrefixes.length; i < il; i++ ) { - fullscreen.prefix = browserPrefixes[i]; - - if (!_is.undefined(document[fullscreen.prefix + 'CancelFullScreen'])) { - fullscreen.supportsFullScreen = true; - break; - } else if (!_is.undefined(document.msExitFullscreen) && document.msFullscreenEnabled) { - // Special case for MS (when isn't it?) - fullscreen.prefix = 'ms'; - fullscreen.supportsFullScreen = true; - break; - } + var fullscreen; + (function() { + // Determine the prefix + var prefix = (function() { + var value = false; + + if (is.function(document.cancelFullScreen)) { + value = ''; + } else { + // Check for fullscreen support by vendor prefix + ['webkit', 'o', 'moz', 'ms', 'khtml'].some(function(prefix) { + if (is.function(document[prefix + 'CancelFullScreen'])) { + value = prefix; + return true; + } else if (is.function(document.msExitFullscreen) && document.msFullscreenEnabled) { + // Special case for MS (when isn't it?) + value = 'ms'; + return true; + } + }); } - } - // Update methods to do something useful - if (fullscreen.supportsFullScreen) { + return value; + })(); + + fullscreen = { + prefix: prefix, // Yet again Microsoft awesomeness, // Sometimes the prefix is 'ms', sometimes 'MS' to keep you on your toes - fullscreen.fullScreenEventName = (fullscreen.prefix === 'ms' ? 'MSFullscreenChange' : fullscreen.prefix + 'fullscreenchange'); + eventType: (prefix === 'ms' ? 'MSFullscreenChange' : prefix + 'fullscreenchange'), - fullscreen.isFullScreen = function(element) { - if (_is.undefined(element)) { + // Is an element fullscreen + isFullScreen: function(element) { + if (!support.fullscreen) { + return false; + } + if (is.undefined(element)) { element = document.body; } switch (this.prefix) { @@ -659,29 +665,43 @@ case 'moz': return document.mozFullScreenElement === element; default: - return document[this.prefix + 'FullscreenElement'] === element; + return document[prefix + 'FullscreenElement'] === element; } - }; - fullscreen.requestFullScreen = function(element) { - if (_is.undefined(element)) { + }, + requestFullScreen: function(element) { + if (!support.fullscreen) { + return false; + } + if (!is.htmlElement(element)) { element = document.body; } - return (this.prefix === '') ? element.requestFullScreen() : element[this.prefix + (this.prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')](); - }; - fullscreen.cancelFullScreen = function() { - return (this.prefix === '') ? document.cancelFullScreen() : document[this.prefix + (this.prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen')](); - }; - fullscreen.element = function() { - return (this.prefix === '') ? document.fullscreenElement : document[this.prefix + 'FullscreenElement']; - }; - } - return fullscreen; - } + console.log(prefix); + + return (prefix === '') ? element.requestFullScreen() : element[prefix + (prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')](); + }, + cancelFullScreen: function() { + if (!support.fullscreen) { + return false; + } + return (prefix === '') ? document.cancelFullScreen() : document[prefix + (prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen')](); + }, + element: function() { + if (!support.fullscreen) { + return null; + } + return (prefix === '') ? document.fullscreenElement : document[prefix + 'FullscreenElement']; + } + }; + })(); - // Local storage - var _storage = { - supported: (function() { + // Check for support + var support = { + // Fullscreen support and set prefix + fullscreen: fullscreen.prefix !== false, + // Local storage mode + // We can't assume if local storage is present that we can use it + storage: (function() { if (!('localStorage' in window)) { return false; } @@ -700,132 +720,199 @@ // Check if value matches return (result === 'OK'); + } catch (e) { + return false; } - catch (e) { + + return false; + })(), + // Picture-in-picture support + // Safari only currently + pip: (function() { + return is.function(document.createElement('video').webkitSetPresentationMode); + })(), + // Airplay support + // Safari only currently + airplay: (function() { + return is.function(window.WebKitPlaybackTargetAvailabilityEvent); + })(), + // Check for mime type support against a player instance + // Credits: http://diveintohtml5.info/everything.html + // Related: http://www.leanbackplyr.com/test/h5mt.html + mime: function(plyr, type) { + var media = plyr.media; + + try { + // Bail if no checking function + if (!is.function(media.canPlayType)) { + return false; + } + + // Type specific checks + if (plyr.type === 'video') { + switch (type) { + case 'video/webm': + return media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, ''); + case 'video/mp4': + return media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''); + case 'video/ogg': + return media.canPlayType('video/ogg; codecs="theora"').replace(/no/, ''); + } + } else if (plyr.type === 'audio') { + switch (type) { + case 'audio/mpeg': + return media.canPlayType('audio/mpeg;').replace(/no/, ''); + case 'audio/ogg': + return media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, ''); + case 'audio/wav': + return media.canPlayType('audio/wav; codecs="1"').replace(/no/, ''); + } + } + } catch (e) { return false; } + // If we got this far, we're stuffed return false; - })() + } }; // Player instance function Plyr(media, config) { - var plyr = this, - timers = {}, - api; + var plyr = this; + var timers = {}; + var api; // Set media plyr.media = media; var original = media.cloneNode(true); // Trigger events, with plyr instance passed - function _triggerEvent(element, type, bubbles, properties) { - _event(element, type, bubbles, _extend({}, properties, { + function trigger(element, type, bubbles, properties) { + event(element, type, bubbles, extend({}, properties, { plyr: api })); } // Debugging - function _console(type, args) { + function logger(type, args) { if (config.debug && window.console) { args = Array.prototype.slice.call(args); - if (_is.string(config.logPrefix) && config.logPrefix.length) { + if (is.string(config.logPrefix) && config.logPrefix.length) { args.unshift(config.logPrefix); } console[type].apply(console, args); } } - var _log = function() { _console('log', arguments) }, - _warn = function() { _console('warn', arguments) }; + var log = function() { + logger('log', arguments) + }; + var warn = function() { + logger('warn', arguments) + }; // Log config options - _log('Config', config); + log('Config', config); // Get icon URL - function _getIconUrl() { + function getIconUrl() { return { - url: config.iconUrl, - absolute: (config.iconUrl.indexOf("http") === 0) || plyr.browser.isIE + url: config.iconUrl, + absolute: (config.iconUrl.indexOf("http") === 0) || plyr.browser.isIE }; } // Build the default HTML - function _buildControls() { + function buildControls() { // Create html array - var html = [], - iconUrl = _getIconUrl(), - iconPath = (!iconUrl.absolute ? iconUrl.url : '') + '#' + config.iconPrefix; + var html = []; + var iconUrl = getIconUrl(); + var iconPath = (!iconUrl.absolute ? iconUrl.url : '') + '#' + config.iconPrefix; // Larger overlaid play button - if (_inArray(config.controls, 'play-large')) { + if (inArray(config.controls, 'play-large')) { + /* beautify ignore:start */ html.push( '<button type="button" data-plyr="play" class="plyr__play-large">', '<svg><use xlink:href="' + iconPath + '-play" /></svg>', '<span class="plyr__sr-only">' + config.i18n.play + '</span>', '</button>' ); + /* beautify ignore:end */ } html.push('<div class="plyr__controls">'); // Restart button - if (_inArray(config.controls, 'restart')) { + if (inArray(config.controls, 'restart')) { + /* beautify ignore:start */ html.push( - '<button type="button" data-plyr="restart">', + '<button type="button" class="plyr__control" data-plyr="restart">', '<svg><use xlink:href="' + iconPath + '-restart" /></svg>', '<span class="plyr__sr-only">' + config.i18n.restart + '</span>', '</button>' ); + /* beautify ignore:end */ } // Rewind button - if (_inArray(config.controls, 'rewind')) { + if (inArray(config.controls, 'rewind')) { + /* beautify ignore:start */ html.push( - '<button type="button" data-plyr="rewind">', + '<button type="button" class="plyr__control" data-plyr="rewind">', '<svg><use xlink:href="' + iconPath + '-rewind" /></svg>', '<span class="plyr__sr-only">' + config.i18n.rewind + '</span>', '</button>' ); + /* beautify ignore:end */ } // Play Pause button // TODO: This should be a toggle button really? - if (_inArray(config.controls, 'play')) { + if (inArray(config.controls, 'play')) { + /* beautify ignore:start */ html.push( - '<button type="button" data-plyr="play">', + '<button type="button" class="plyr__control" data-plyr="play">', '<svg><use xlink:href="' + iconPath + '-play" /></svg>', '<span class="plyr__sr-only">' + config.i18n.play + '</span>', '</button>', - '<button type="button" data-plyr="pause">', + '<button type="button" class="plyr__control" data-plyr="pause">', '<svg><use xlink:href="' + iconPath + '-pause" /></svg>', '<span class="plyr__sr-only">' + config.i18n.pause + '</span>', '</button>' ); + /* beautify ignore:end */ } // Fast forward button - if (_inArray(config.controls, 'fast-forward')) { + if (inArray(config.controls, 'fast-forward')) { + /* beautify ignore:start */ html.push( - '<button type="button" data-plyr="fast-forward">', + '<button type="button" class="plyr__control" data-plyr="fast-forward">', '<svg><use xlink:href="' + iconPath + '-fast-forward" /></svg>', '<span class="plyr__sr-only">' + config.i18n.forward + '</span>', '</button>' ); + /* beautify ignore:end */ } // Progress - if (_inArray(config.controls, 'progress')) { + if (inArray(config.controls, 'progress')) { // Create progress - html.push('<span class="plyr__progress">', - '<label for="seek{id}" class="plyr__sr-only">Seek</label>', - '<input id="seek{id}" class="plyr__progress--seek" type="range" min="0" max="100" step="0.1" value="0" data-plyr="seek">', - '<progress class="plyr__progress--played" max="100" value="0" role="presentation"></progress>', - '<progress class="plyr__progress--buffer" max="100" value="0">', + /* beautify ignore:start */ + html.push( + '<span class="plyr__progress">', + '<div class="plyr__progress-loop"></div>', + '<label for="seek-{id}" class="plyr__sr-only">Seek</label>', + '<input id="seek-{id}" class="plyr__progress--seek" type="range" min="0" max="100" step="0.1" value="0" data-plyr="seek">', + '<progress class="plyr__progress--played" max="100" value="0" role="presentation"></progress>', + '<progress class="plyr__progress--buffer" max="100" value="0">', '<span>0</span>% ' + config.i18n.buffered, - '</progress>'); + '</progress>' + ); + /* beautify ignore:end */ // Seek tooltip if (config.tooltips.seek) { @@ -837,67 +924,286 @@ } // Media current time display - if (_inArray(config.controls, 'current-time')) { + if (inArray(config.controls, 'current-time')) { + /* beautify ignore:start */ html.push( '<span class="plyr__time">', '<span class="plyr__sr-only">' + config.i18n.currentTime + '</span>', '<span class="plyr__time--current">00:00</span>', '</span>' ); + /* beautify ignore:end */ } // Media duration display - if (_inArray(config.controls, 'duration')) { + if (inArray(config.controls, 'duration')) { + /* beautify ignore:start */ html.push( '<span class="plyr__time">', '<span class="plyr__sr-only">' + config.i18n.duration + '</span>', '<span class="plyr__time--duration">00:00</span>', '</span>' ); + /* beautify ignore:end */ } // Toggle mute button - if (_inArray(config.controls, 'mute')) { + if (inArray(config.controls, 'mute')) { + /* beautify ignore:start */ html.push( - '<button type="button" data-plyr="mute">', + '<button type="button" class="plyr__control" data-plyr="mute">', '<svg class="icon--muted"><use xlink:href="' + iconPath + '-muted" /></svg>', '<svg><use xlink:href="' + iconPath + '-volume" /></svg>', '<span class="plyr__sr-only">' + config.i18n.toggleMute + '</span>', '</button>' ); + /* beautify ignore:end */ } // Volume range control - if (_inArray(config.controls, 'volume')) { + if (inArray(config.controls, 'volume')) { + /* beautify ignore:start */ html.push( '<span class="plyr__volume">', - '<label for="volume{id}" class="plyr__sr-only">' + config.i18n.volume + '</label>', - '<input id="volume{id}" class="plyr__volume--input" type="range" min="' + config.volumeMin + '" max="' + config.volumeMax + '" value="' + config.volume + '" data-plyr="volume">', + '<label for="volume-{id}" class="plyr__sr-only">' + config.i18n.volume + '</label>', + '<input id="volume-{id}" class="plyr__volume--input" type="range" min="' + config.volumeMin + '" max="' + config.volumeMax + '" value="' + config.volume + '" data-plyr="volume">', '<progress class="plyr__volume--display" max="' + config.volumeMax + '" value="' + config.volumeMin + '" role="presentation"></progress>', '</span>' ); + /* beautify ignore:end */ } // Toggle captions button - if (_inArray(config.controls, 'captions')) { + if (inArray(config.controls, 'captions')) { + /* beautify ignore:start */ html.push( - '<button type="button" data-plyr="captions">', + '<button type="button" class="plyr__control" data-plyr="captions">', '<svg class="icon--captions-on"><use xlink:href="' + iconPath + '-captions-on" /></svg>', - '<svg><use xlink:href="' + iconPath+ '-captions-off" /></svg>', + '<svg><use xlink:href="' + iconPath + '-captions-off" /></svg>', '<span class="plyr__sr-only">' + config.i18n.toggleCaptions + '</span>', '</button>' ); + /* beautify ignore:end */ + } + + // Settings button / menu + if (inArray(config.controls, 'settings')) { + /* beautify ignore:start */ + html.push( + '<div class="plyr__menu" data-plyr="settings">', + '<button type="button" id="plyr-settings-toggle-{id}" class="plyr__control" aria-haspopup="true" aria-controls="plyr-settings-{id}" aria-expanded="false">', + '<svg><use xlink:href="' + iconPath + '-settings" /></svg>', + '<span class="plyr__sr-only">' + config.i18n.settings + '</span>', + '</button>', + '<form class="plyr__menu__container" id="plyr-settings-{id}" aria-hidden="true" aria-labelled-by="plyr-settings-toggle-{id}" role="tablist" tabindex="-1">', + '<div>', + '<div id="plyr-settings-{id}-primary" aria-hidden="false" aria-labelled-by="plyr-settings-toggle-{id}" role="tabpanel" tabindex="-1">', + '<ul>', + '<li role="tab">', + '<button type="button" class="plyr__control plyr__control--forward" id="plyr-settings-{id}-captions-toggle" aria-haspopup="true" aria-controls="plyr-settings-{id}-captions" aria-expanded="false">', + config.i18n.captions + + '<span class="plyr__menu__value">{lang}</span>', + '</button>', + '</li>', + '<li role="tab">', + '<button type="button" class="plyr__control plyr__control--forward" id="plyr-settings-{id}-speed-toggle" aria-haspopup="true" aria-controls="plyr-settings-{id}-speed" aria-expanded="false">', + config.i18n.speed + + '<span class="plyr__menu__value" data-menu="speed">{speed}</span>', + '</button>', + '</li>', + '<li role="tab">', + '<button type="button" class="plyr__control plyr__control--forward" id="plyr-settings-{id}-quality-toggle" aria-haspopup="true" aria-controls="plyr-settings-{id}-quality" aria-expanded="false">', + config.i18n.quality + + '<span class="plyr__menu__value">{quality}</span>', + '</button>', + '</li>', + '<li role="tab">', + '<button type="button" class="plyr__control plyr__control--forward" id="plyr-settings-{id}-loop-toggle" aria-haspopup="true" aria-controls="plyr-settings-{id}-loop" aria-expanded="false">', + config.i18n.loop + + '<span class="plyr__menu__value" data-menu="loop">{loop}</span>', + '</button>', + '</li>', + '</ul>', + '</div>', + '<div id="plyr-settings-{id}-captions" aria-hidden="true" aria-labelled-by="plyr-settings-{id}-captions-toggle" role="tabpanel" tabindex="-1">', + '<ul>', + '<li role="tab">', + '<button type="button" class="plyr__control plyr__control--back" aria-haspopup="true" aria-controls="plyr-settings-{id}-primary" aria-expanded="false">', + config.i18n.captions, + '</button>', + '</li>', + '<li>', + '<button type="button" class="plyr__control">English</button>', + '</li>', + '<li>', + '<button type="button" class="plyr__control">Off</button>', + '</li>', + '</ul>', + '</div>', + '<div id="plyr-settings-{id}-speed" aria-hidden="true" aria-labelled-by="plyr-settings-{id}-speed-toggle" role="tabpanel" tabindex="-1">', + '<ul>', + '<li role="tab">', + '<button type="button" class="plyr__control plyr__control--back" aria-haspopup="true" aria-controls="plyr-settings-{id}-primary" aria-expanded="false">', + config.i18n.speed, + '</button>', + '</li>', + '<li>', + '<label class="plyr__control">', + '<input type="radio" name="speed" data-plyr="speed" value="2.0" '+ (config.currentSpeed === 2 ? 'checked' : '') +'>', + '2.0×', + '</label>', + '</li>', + '<li>', + '<label class="plyr__control">', + '<input type="radio" name="speed" data-plyr="speed" value="1.5" '+ (config.currentSpeed === 1.5 ? 'checked' : '') +'>', + '1.5×', + '</label>', + '</li>', + '<li>', + '<label class="plyr__control">', + '<input type="radio" name="speed" data-plyr="speed" value="1.0" '+ (config.currentSpeed === 1 ? 'checked' : '') +'>', + '1.0×', + '</label>', + '</li>', + '<li>', + '<label class="plyr__control">', + '<input type="radio" name="speed" data-plyr="speed" value="0.5" '+ (config.currentSpeed === 0.5 ? 'checked' : '') +'>', + '0.5×', + '</label>', + '</li>', + '</ul>', + '</div>', + '<div id="plyr-settings-{id}-quality" aria-hidden="true" aria-labelled-by="plyr-settings-{id}-quality-toggle" role="tabpanel" tabindex="-1">', + '<ul>', + '<li role="tab">', + '<button type="button" class="plyr__control plyr__control--back" aria-haspopup="true" aria-controls="plyr-settings-{id}-primary" aria-expanded="false">', + config.i18n.quality, + '</button>', + '</li>', + '<li>', + '<label class="plyr__control">', + '<input type="radio" name="quality">', + '2160P', + '<span class="plyr__menu__value">', + '<span class="plyr__badge">4K</span>', + '</span>', + '</label>', + '</li>', + '<li>', + '<label class="plyr__control">', + '<input type="radio" name="quality">', + '1440P', + '<span class="plyr__menu__value">', + '<span class="plyr__badge">WQHD</span>', + '</span>', + '</label>', + '</li>', + '<li>', + '<label class="plyr__control">', + '<input type="radio" name="quality">', + '1080P', + '<span class="plyr__menu__value">', + '<span class="plyr__badge">HD</span>', + '</span>', + '</label>', + '</li>', + '<li>', + '<label class="plyr__control">', + '<input type="radio" name="quality">', + '720P', + '<span class="plyr__menu__value">', + '<span class="plyr__badge">HD</span>', + '</span>', + '</label>', + '</li>', + '<li>', + '<label class="plyr__control">', + '<input type="radio" name="quality">', + '480P', + '</label>', + '</li>', + '<li>', + '<label class="plyr__control">', + '<input type="radio" name="quality">', + '360P', + '</label>', + '</li>', + '</ul>', + '</div>', + '<div id="plyr-settings-{id}-loop" aria-hidden="true" aria-labelled-by="plyr-settings-{id}-loop-toggle" role="tabpanel" tabindex="-1">', + '<ul>', + '<li role="tab">', + '<button type="button" class="plyr__control plyr__control--back" aria-haspopup="true" aria-controls="plyr-settings-{id}-primary" aria-expanded="false">', + config.i18n.loop, + '</button>', + '</li>', + '<li>', + '<button type="button" class="plyr__control" data-plyr="loop" data-plyr-loop="all">', + config.i18n.loopAll, + '<span></span>', + '</button>', + '</li>', + '<li>', + '<button type="button" class="plyr__control" data-plyr="loop" data-plyr-loop="start">', + config.i18n.loopStart, + '<span></span>', + '</button>', + '</li>', + '<li>', + '<button type="button" class="plyr__control" data-plyr="loop" data-plyr-loop="end">', + config.i18n.loopEnd, + '<span></span>', + '</button>', + '</li>', + '<li>', + '<button type="button" class="plyr__control" data-plyr="loop" data-plyr-loop="none">', + config.i18n.loopNone, + '</button>', + '</li>', + '</ul>', + '</div>', + '</div>', + '</form>', + '</div>' + ); + /* beautify ignore:end */ + } + + // Picture in picture button + if (inArray(config.controls, 'pip') && support.pip) { + /* beautify ignore:start */ + html.push( + '<button type="button" class="plyr__control" data-plyr="pip">', + '<svg><use xlink:href="' + iconPath + '-pip" /></svg>', + '<span class="plyr__sr-only">PIP</span>', + '</button>' + ); + /* beautify ignore:end */ + } + + // Airplay button + if (inArray(config.controls, 'airplay') && support.airplay) { + /* beautify ignore:start */ + html.push( + '<button type="button" class="plyr__control" data-plyr="airplay">', + '<svg><use xlink:href="' + iconPath + '-airplay" /></svg>', + '<span class="plyr__sr-only">AirPlay</span>', + '</button>' + ); + /* beautify ignore:end */ } // Toggle fullscreen button - if (_inArray(config.controls, 'fullscreen')) { + if (inArray(config.controls, 'fullscreen')) { + /* beautify ignore:start */ html.push( - '<button type="button" data-plyr="fullscreen">', + '<button type="button" class="plyr__control" data-plyr="fullscreen">', '<svg class="icon--exit-fullscreen"><use xlink:href="' + iconPath + '-exit-fullscreen" /></svg>', '<svg><use xlink:href="' + iconPath + '-enter-fullscreen" /></svg>', '<span class="plyr__sr-only">' + config.i18n.toggleFullscreen + '</span>', '</button>' ); + /* beautify ignore:end */ } // Close everything @@ -906,45 +1212,155 @@ return html.join(''); } + // Set the YouTube quality menu + // Later this will work for HTML5 also + // YouTube: "hd2160", "hd1440", "hd1080", "hd720", "large", "medium", "small", "tiny", "auto" + function setQualityMenu(available, current) { + if (is.object(plyr.quality)) { + return; + } + + plyr.quality = { + available: available, + current: current + }; + + // Get the badge HTML for HD, 4K etc + function getBadge(quality) { + var label = ""; + + switch (quality) { + case 'hd2160': + label = '4K'; + break; + case 'hd1440': + label = 'WQHD'; + break; + case 'hd1080': + label = 'HD'; + break; + case 'hd720': + label = 'HD'; + break; + } + + if (!label.length) { + return ""; + } + + return [ + '<span class="plyr__menu__value">', + '<span class="plyr__badge">' + label + '</span>', + '</span>' + ].join(''); + } + + // Translate the quality key into a nice label + function getLabel(quality) { + switch (quality) { + case 'hd2160': + return '2160P'; + case 'hd1440': + return '1440P'; + case 'hd1080': + return '1080P'; + case 'hd720': + return '720P'; + case 'large': + return '480P'; + case 'medium': + return '360P'; + case 'small': + return '240P'; + default: + return 'Auto'; + } + } + + if (is.array(available) && available.length) { + // Remove any unwanted quality levels + var filtered = available.filter(function(quality) { + return ['tiny', 'small'].indexOf(quality) === -1; + }); + + var list = filtered.map(function(quality) { + return [ + '<li>', + '<label class="plyr__control">', + '<input type="radio" name="quality" value="' + quality + '"' + (quality === plyr.quality.current ? ' checked' : '') + '>', + getLabel(quality), + getBadge(quality), + '</label>', + '</li>' + ].join(''); + }); + + list.unshift([ + '<li role="tab">', + '<button type="button" class="plyr__control plyr__control--back" aria-haspopup="true" aria-controls="plyr-settings-' + plyr.id + '-primary" aria-expanded="false">', + config.i18n.quality, + '</button>', + '</li>' + ].join('')); + + getElement(config.selectors.menu.quality).innerHTML = list.join(''); + } + } + // Setup fullscreen - function _setupFullscreen() { + function setupFullscreen() { if (!plyr.supported.full) { return; } if ((plyr.type !== 'audio' || config.fullscreen.allowAudio) && config.fullscreen.enabled) { // Check for native support - var nativeSupport = fullscreen.supportsFullScreen; + var nativeSupport = support.fullscreen; - if (nativeSupport || (config.fullscreen.fallback && !_inFrame())) { - _log((nativeSupport ? 'Native' : 'Fallback') + ' fullscreen enabled'); + if (nativeSupport || (config.fullscreen.fallback && !inFrame())) { + log((nativeSupport ? 'Native' : 'Fallback') + ' fullscreen enabled'); // Add styling hook - _toggleClass(plyr.container, config.classes.fullscreen.enabled, true); + toggleClass(plyr.container, config.classes.fullscreen.enabled, true); } else { - _log('Fullscreen not supported and fallback disabled'); + log('Fullscreen not supported and fallback disabled'); } // Toggle state if (plyr.buttons && plyr.buttons.fullscreen) { - _toggleState(plyr.buttons.fullscreen, false); + toggleState(plyr.buttons.fullscreen, false); } // Setup focus trap - _focusTrap(); + focusTrap(); + } + } + + // Display active caption if it contains text + function setActiveCue(track) { + // Get the track from the event if needed + if (is.event(track)) { + track = track.target; + } + + // Display a cue, if there is one + if (track.activeCues[0] && 'text' in track.activeCues[0]) { + setCaption(track.activeCues[0].getCueAsHTML()); + } else { + setCaption(); } } // Setup captions - function _setupCaptions() { + function setupCaptions() { // Bail if not HTML5 video if (plyr.type !== 'video') { return; } // Inject the container - if (!_getElement(config.selectors.captions)) { - plyr.videoContainer.insertAdjacentHTML('afterbegin', '<div class="' + _getClassname(config.selectors.captions) + '"></div>'); + if (!getElement(config.selectors.captions)) { + plyr.videoContainer.insertAdjacentHTML('afterbegin', '<div class="' + getClassname(config.selectors.captions) + '"></div>'); } // Determine if HTML5 textTracks is supported @@ -954,41 +1370,48 @@ } // Get URL of caption file if exists - var captionSrc = '', - kind, - children = plyr.media.childNodes; - - for (var i = 0; i < children.length; i++) { - if (children[i].nodeName.toLowerCase() === 'track') { - kind = children[i].kind; - if (kind === 'captions' || kind === 'subtitles') { - captionSrc = children[i].getAttribute('src'); + var captionSources = [], + captionSrc = ''; + + plyr.media.childNodes.forEach(function(child) { + if (child.nodeName.toLowerCase() === 'track') { + if (child.kind === 'captions' || child.kind === 'subtitles') { + captionSources.push(child.getAttribute('src')); } } - } + }); // Record if caption file exists or not plyr.captionExists = true; - if (captionSrc === '') { + if (captionSources.length === 0) { + plyr.captionExists = false; + log('No caption track found'); + } else if ((config.captions.selectedIndex + 1) > captionSources.length) { plyr.captionExists = false; - _log('No caption track found'); + log('Caption index out of bound'); } else { - _log('Caption track found; URI: ' + captionSrc); + captionSrc = captionSources[config.captions.selectedIndex]; + log('Caption track found; URI: ' + captionSrc); } // If no caption file exists, hide container for caption text if (!plyr.captionExists) { - _toggleClass(plyr.container, config.classes.captions.enabled); + toggleClass(plyr.container, config.classes.captions.enabled); } else { + var tracks = plyr.media.textTracks; + // Turn off native caption rendering to avoid double captions // This doesn't seem to work in Safari 7+, so the <track> elements are removed from the dom below - var tracks = plyr.media.textTracks; - for (var x = 0; x < tracks.length; x++) { - tracks[x].mode = 'hidden'; - } + [].forEach.call(tracks, function(track) { + // Remove the listener to prevent event overlapping + off(track, 'cuechange', setActiveCue); + + // Hide captions + track.mode = 'hidden'; + }); // Enable UI - _showCaptions(plyr); + showCaptions(plyr); // Disable unsupported browsers than report false positive // Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1033144 @@ -996,7 +1419,7 @@ (plyr.browser.isFirefox && plyr.browser.version >= 31)) { // Debugging - _log('Detected browser with known TextTrack issues - using manual fallback'); + log('Detected browser with known TextTrack issues - using manual fallback'); // Set to false so skips to 'manual' captioning plyr.usingTextTracks = false; @@ -1005,25 +1428,21 @@ // Rendering caption tracks // Native support required - http://caniuse.com/webvtt if (plyr.usingTextTracks) { - _log('TextTracks supported'); + log('TextTracks supported'); - for (var y = 0; y < tracks.length; y++) { - var track = tracks[y]; + var track = tracks[config.captions.selectedIndex]; - if (track.kind === 'captions' || track.kind === 'subtitles') { - _on(track, 'cuechange', function() { - // Display a cue, if there is one - if (this.activeCues[0] && 'text' in this.activeCues[0]) { - _setCaption(this.activeCues[0].getCueAsHTML()); - } else { - _setCaption(); - } - }); + if (track.kind === 'captions' || track.kind === 'subtitles') { + on(track, 'cuechange', setActiveCue); + + // If we change the active track while a cue is already displayed we need to update it + if (track.activeCues && track.activeCues.length > 0) { + setActiveCue(track); } } } else { // Caption tracks not natively supported - _log('TextTracks not supported so rendering captions manually'); + log('TextTracks not supported so rendering captions manually'); // Render captions from array at appropriate time plyr.currentCaption = ''; @@ -1036,45 +1455,38 @@ xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { - var captions = [], - caption, - req = xhr.responseText; + var response = xhr.responseText; - //According to webvtt spec, line terminator consists of one of the following + // According to webvtt spec, line terminator consists of one of the following // CRLF (U+000D U+000A), LF (U+000A) or CR (U+000D) var lineSeparator = '\r\n'; - if(req.indexOf(lineSeparator+lineSeparator) === -1) { - if(req.indexOf('\r\r') !== -1){ + if (response.indexOf(lineSeparator + lineSeparator) === -1) { + if (response.indexOf('\r\r') !== -1) { lineSeparator = '\r'; } else { lineSeparator = '\n'; } } - captions = req.split(lineSeparator+lineSeparator); - - for (var r = 0; r < captions.length; r++) { - caption = captions[r]; - plyr.captions[r] = []; + var captions = response.split(lineSeparator + lineSeparator); - // Get the parts of the captions - var parts = caption.split(lineSeparator), - index = 0; + plyr.captions = captions.map(function(caption) { + var parts = caption.split(lineSeparator); + var index = 0; // Incase caption numbers are added - if (parts[index].indexOf(":") === -1) { + if (parts[index].indexOf(":") !== -1) { index = 1; } - plyr.captions[r] = [parts[index], parts[index + 1]]; - } + return [parts[index], parts[index + 1]]; + }); - // Remove first element ('VTT') plyr.captions.shift(); - _log('Successfully loaded the caption file via AJAX'); + log('Successfully loaded the caption file via AJAX'); } else { - _warn(config.logPrefix + 'There was a problem loading the caption file via AJAX'); + warn(config.logPrefix + 'There was a problem loading the caption file via AJAX'); } } }; @@ -1088,63 +1500,69 @@ } // Set the current caption - function _setCaption(caption) { - /* jshint unused:false */ - var container = _getElement(config.selectors.captions), - content = document.createElement('span'); + function setCaption(caption) { + var captions = getElement(config.selectors.captions); + var content = document.createElement('span'); // Empty the container - container.innerHTML = ''; + captions.innerHTML = ''; // Default to empty - if (_is.undefined(caption)) { + if (is.undefined(caption)) { caption = ''; } // Set the span content - if (_is.string(caption)) { + if (is.string(caption)) { content.innerHTML = caption.trim(); } else { content.appendChild(caption); } // Set new caption text - container.appendChild(content); + captions.appendChild(content); // Force redraw (for Safari) - var redraw = container.offsetHeight; + var redraw = captions.offsetHeight; } // Captions functions // Seek the manual caption time and update UI - function _seekManualCaptions(time) { + function seekManualCaptions(time) { // Utilities for caption time codes - function _timecodeCommon(tc, pos) { + function timecodeCommon(tc, pos) { var tcpair = []; tcpair = tc.split(' --> '); - for(var i = 0; i < tcpair.length; i++) { + for (var i = 0; i < tcpair.length; i++) { // WebVTT allows for extra meta data after the timestamp line // So get rid of this if it exists tcpair[i] = tcpair[i].replace(/(\d+:\d+:\d+\.\d+).*/, "$1"); } - return _subTcSecs(tcpair[pos]); + return subTcSecs(tcpair[pos]); } - function _timecodeMin(tc) { - return _timecodeCommon(tc, 0); + + function timecodeMin(tc) { + return timecodeCommon(tc, 0); } - function _timecodeMax(tc) { - return _timecodeCommon(tc, 1); + + function timecodeMax(tc) { + return timecodeCommon(tc, 1); } - function _subTcSecs(tc) { + + function subTcSecs(tc) { if (tc === null || tc === undefined) { return 0; } else { var tc1 = [], tc2 = [], - seconds; + seconds = 0; tc1 = tc.split(','); tc2 = tc1[0].split(':'); - seconds = Math.floor(tc2[0]*60*60) + Math.floor(tc2[1]*60) + Math.floor(tc2[2]); + + for (var i = 0, len = tc2.length; i < len; i++) { + seconds += Math.floor(tc2[i] * (Math.pow(60, len - (i + 1)))); + } + return seconds; } } @@ -1160,14 +1578,14 @@ // 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 = _is.number(time) ? time : plyr.media.currentTime; + time = is.number(time) ? time : plyr.media.currentTime; // If there's no subs available, bail if (!plyr.captions[plyr.subcount]) { return; } - while (_timecodeMax(plyr.captions[plyr.subcount][0]) < time.toFixed(1)) { + while (timecodeMax(plyr.captions[plyr.subcount][0]) < time.toFixed(1)) { plyr.subcount++; if (plyr.subcount > plyr.captions.length - 1) { plyr.subcount = plyr.captions.length - 1; @@ -1176,67 +1594,66 @@ } // Check if the next caption is in the current time range - if (plyr.media.currentTime.toFixed(1) >= _timecodeMin(plyr.captions[plyr.subcount][0]) && - plyr.media.currentTime.toFixed(1) <= _timecodeMax(plyr.captions[plyr.subcount][0])) { - plyr.currentCaption = plyr.captions[plyr.subcount][1]; + if (plyr.media.currentTime.toFixed(1) >= timecodeMin(plyr.captions[plyr.subcount][0]) && + plyr.media.currentTime.toFixed(1) <= timecodeMax(plyr.captions[plyr.subcount][0])) { + plyr.currentCaption = plyr.captions[plyr.subcount][1]; // Render the caption - _setCaption(plyr.currentCaption); + setCaption(plyr.currentCaption); } else { - _setCaption(); + setCaption(); } } // Display captions container and button (for initialization) - function _showCaptions() { + function showCaptions() { // If there's no caption toggle, bail if (!plyr.buttons.captions) { return; } - _toggleClass(plyr.container, config.classes.captions.enabled, true); + toggleClass(plyr.container, config.classes.captions.enabled, true); // Try to load the value from storage var active = plyr.storage.captionsEnabled; // Otherwise fall back to the default config - if (!_is.boolean(active)) { + if (!is.boolean(active)) { active = config.captions.defaultActive; } if (active) { - _toggleClass(plyr.container, config.classes.captions.active, true); - _toggleState(plyr.buttons.captions, true); + toggleClass(plyr.container, config.classes.captions.active, true); + toggleState(plyr.buttons.captions, true); } } // Find all elements - function _getElements(selector) { + function getElements(selector) { return plyr.container.querySelectorAll(selector); } // Find a single element - function _getElement(selector) { - return _getElements(selector)[0]; + function getElement(selector) { + return getElements(selector)[0]; } // Determine if we're in an iframe - function _inFrame() { + function inFrame() { try { return window.self !== window.top; - } - catch (e) { + } catch (e) { return true; } } // Trap focus inside container - function _focusTrap() { - var tabbables = _getElements('input:not([disabled]), button:not([disabled])'), - first = tabbables[0], - last = tabbables[tabbables.length - 1]; + function focusTrap() { + var tabbables = getElements('input:not([disabled]), button:not([disabled])'), + first = tabbables[0], + last = tabbables[tabbables.length - 1]; - function _checkFocus(event) { + function checkFocus(event) { // If it is TAB if (event.which === 9 && plyr.isFullscreen) { if (event.target === last && !event.shiftKey) { @@ -1252,32 +1669,34 @@ } // Bind the handler - _on(plyr.container, 'keydown', _checkFocus); + on(plyr.container, 'keydown', checkFocus); } // Add elements to HTML5 media (source, tracks, etc) - function _insertChildElements(type, attributes) { - if (_is.string(attributes)) { - _insertElement(type, plyr.media, { src: attributes }); + function insertChildElements(type, attributes) { + if (is.string(attributes)) { + insertElement(type, plyr.media, { + src: attributes + }); } else if (attributes.constructor === Array) { for (var i = attributes.length - 1; i >= 0; i--) { - _insertElement(type, plyr.media, attributes[i]); + insertElement(type, plyr.media, attributes[i]); } } } // Insert controls - function _injectControls() { + function injectControls() { // Sprite if (config.loadSprite) { - var iconUrl = _getIconUrl(); + var iconUrl = getIconUrl(); // Only load external sprite using AJAX if (iconUrl.absolute) { - _log('AJAX loading absolute SVG sprite' + (plyr.browser.isIE ? ' (due to IE)' : '')); + 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'); + log('Sprite will be used as external resource directly'); } } @@ -1285,29 +1704,36 @@ var html = config.html; // Insert custom video controls - _log('Injecting custom controls'); + log('Injecting custom controls'); // If no controls are specified, create default if (!html) { - html = _buildControls(); + html = buildControls(); } // Replace seek time instances - html = _replaceAll(html, '{seektime}', config.seekTime); + html = replaceAll(html, '{seektime}', config.seekTime); + + // Replace speed time instances + html = replaceAll(html, '{speed}', getSpeedDisplayValue()); + + // Replace current captions language + html = replaceAll(html, '{lang}', 'English'); // Replace all id references with random numbers - html = _replaceAll(html, '{id}', Math.floor(Math.random() * (10000))); + plyr.id = Math.floor(Math.random() * (10000)); + html = replaceAll(html, '{id}', plyr.id); // Controls container var target; // Inject to custom location - if (_is.string(config.selectors.controls.container)) { + if (is.string(config.selectors.controls.container)) { target = document.querySelector(config.selectors.controls.container); } // Inject into the container by default - if (!_is.htmlElement(target)) { + if (!is.htmlElement(target)) { target = plyr.container } @@ -1316,81 +1742,96 @@ // Setup tooltips if (config.tooltips.controls) { - var labels = _getElements([config.selectors.controls.wrapper, ' ', config.selectors.labels, ' .', config.classes.hidden].join('')); + var labels = getElements([config.selectors.controls.wrapper, ' ', config.selectors.labels, ' .', config.classes.hidden].join('')); for (var i = labels.length - 1; i >= 0; i--) { var label = labels[i]; - _toggleClass(label, config.classes.hidden, false); - _toggleClass(label, config.classes.tooltip, true); + toggleClass(label, config.classes.hidden, false); + toggleClass(label, config.classes.tooltip, true); } } } + function getSpeedDisplayValue() { + return config.currentSpeed.toFixed(1).toString().replace('.0', '') + '×' + } + // Find the UI controls and store references - function _findElements() { + function findElements() { try { - plyr.controls = _getElement(config.selectors.controls.wrapper); + plyr.controls = getElement(config.selectors.controls.wrapper); // Buttons - plyr.buttons = {}; - plyr.buttons.seek = _getElement(config.selectors.buttons.seek); - plyr.buttons.play = _getElements(config.selectors.buttons.play); - plyr.buttons.pause = _getElement(config.selectors.buttons.pause); - plyr.buttons.restart = _getElement(config.selectors.buttons.restart); - plyr.buttons.rewind = _getElement(config.selectors.buttons.rewind); - plyr.buttons.forward = _getElement(config.selectors.buttons.forward); - plyr.buttons.fullscreen = _getElement(config.selectors.buttons.fullscreen); + plyr.buttons = { + seek: getElement(config.selectors.buttons.seek), + play: getElements(config.selectors.buttons.play), + pause: getElement(config.selectors.buttons.pause), + restart: getElement(config.selectors.buttons.restart), + rewind: getElement(config.selectors.buttons.rewind), + forward: getElement(config.selectors.buttons.forward), + fullscreen: getElement(config.selectors.buttons.fullscreen), + settings: getElement(config.selectors.buttons.settings), + pip: getElement(config.selectors.buttons.pip), + speed: getElement(config.selectors.buttons.speed), + loop: getElement(config.selectors.buttons.loop) + }; // Inputs - plyr.buttons.mute = _getElement(config.selectors.buttons.mute); - plyr.buttons.captions = _getElement(config.selectors.buttons.captions); + plyr.buttons.mute = getElement(config.selectors.buttons.mute); + plyr.buttons.captions = getElement(config.selectors.buttons.captions); // Progress - plyr.progress = {}; - plyr.progress.container = _getElement(config.selectors.progress.container); + plyr.progress = { + container: getElement(config.selectors.progress.container) + }; // Progress - Buffering - plyr.progress.buffer = {}; - plyr.progress.buffer.bar = _getElement(config.selectors.progress.buffer); - plyr.progress.buffer.text = plyr.progress.buffer.bar && plyr.progress.buffer.bar.getElementsByTagName('span')[0]; + plyr.progress.buffer = (function() { + var bar = getElement(config.selectors.progress.buffer); + + return { + bar: bar, + text: is.htmlElement(bar) && bar.getElementsByTagName('span')[0] + }; + })(); // Progress - Played - plyr.progress.played = _getElement(config.selectors.progress.played); + plyr.progress.played = getElement(config.selectors.progress.played); // Seek tooltip - plyr.progress.tooltip = plyr.progress.container && plyr.progress.container.querySelector('.' + config.classes.tooltip); + plyr.progress.tooltip = plyr.progress.container && plyr.progress.container.querySelector('.' + config.classes.tooltip); // Volume - plyr.volume = {}; - plyr.volume.input = _getElement(config.selectors.volume.input); - plyr.volume.display = _getElement(config.selectors.volume.display); + plyr.volume = { + input: getElement(config.selectors.volume.input), + display: getElement(config.selectors.volume.display) + }; // Timing - plyr.duration = _getElement(config.selectors.duration); - plyr.currentTime = _getElement(config.selectors.currentTime); - plyr.seekTime = _getElements(config.selectors.seekTime); + plyr.duration = getElement(config.selectors.duration); + plyr.currentTime = getElement(config.selectors.currentTime); + plyr.seekTime = getElements(config.selectors.seekTime); return true; - } - catch(e) { - _warn('It looks like there is a problem with your controls HTML'); + } catch (e) { + warn('It looks like there is a problem with your controls HTML', e); // Restore native video controls - _toggleNativeControls(true); + toggleNativeControls(true); return false; } } // Toggle style hook - function _toggleStyleHook() { - _toggleClass(plyr.container, config.selectors.container.replace('.', ''), plyr.supported.full); + function toggleStyleHook() { + toggleClass(plyr.container, config.selectors.container.replace('.', ''), plyr.supported.full); } // Toggle native controls - function _toggleNativeControls(toggle) { - if (toggle && _inArray(config.types.html5, plyr.type)) { + function toggleNativeControls(toggle) { + if (toggle && inArray(config.types.html5, plyr.type)) { plyr.media.setAttribute('controls', ''); } else { plyr.media.removeAttribute('controls'); @@ -1398,12 +1839,12 @@ } // Setup aria attribute for play and iframe title - function _setTitle(iframe) { + function setTitle(iframe) { // Find the current text var label = config.i18n.play; // If there's a media title set, use that for the label - if (_is.string(config.title) && config.title.length) { + if (is.string(config.title) && config.title.length) { label += ', ' + config.title; // Set container label @@ -1419,18 +1860,18 @@ // Set iframe title // https://github.com/Selz/plyr/issues/124 - if (_is.htmlElement(iframe)) { + if (is.htmlElement(iframe)) { iframe.setAttribute('title', config.i18n.frameTitle.replace('{title}', config.title)); } } // Setup localStorage - function _setupStorage() { + 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) { + if (!support.storage || !config.storage.enabled) { return; } @@ -1448,7 +1889,9 @@ // 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)}); + updateStorage({ + volume: parseFloat(value) + }); } else { // Assume it's JSON from this or a later version of plyr plyr.storage = JSON.parse(value); @@ -1456,45 +1899,48 @@ } // Save a value back to local storage - function _updateStorage(value) { + function updateStorage(value) { // Bail if we don't have localStorage support or it's disabled - if (!_storage.supported || !config.storage.enabled) { + if (!support.storage || !config.storage.enabled) { return; } // Update the working copy of the values - _extend(plyr.storage, value); + extend(plyr.storage, value); // Update storage window.localStorage.setItem(config.storage.key, JSON.stringify(plyr.storage)); } // Setup media - function _setupMedia() { + function setupMedia() { // If there's no media, bail if (!plyr.media) { - _warn('No media element found!'); + warn('No media element found!'); return; } if (plyr.supported.full) { // Add type class - _toggleClass(plyr.container, config.classes.type.replace('{0}', plyr.type), true); + toggleClass(plyr.container, config.classes.type.replace('{0}', plyr.type), true); // Add video class for embeds // This will require changes if audio embeds are added - if (_inArray(config.types.embed, plyr.type)) { - _toggleClass(plyr.container, config.classes.type.replace('{0}', 'video'), true); + if (inArray(config.types.embed, plyr.type)) { + toggleClass(plyr.container, config.classes.type.replace('{0}', 'video'), true); } + // Check for picture-in-picture support + toggleClass(plyr.container, config.classes.pip.enabled, support.pip); + // If there's no autoplay attribute, assume the video is stopped and add state class - _toggleClass(plyr.container, config.classes.stopped, config.autoplay); + toggleClass(plyr.container, config.classes.stopped, config.autoplay); // Add iOS class - _toggleClass(plyr.container, config.classes.isIos, plyr.browser.isIos); + toggleClass(plyr.container, config.classes.isIos, plyr.browser.isIos); // Add touch class - _toggleClass(plyr.container, config.classes.isTouch, plyr.browser.isTouch); + toggleClass(plyr.container, config.classes.isTouch, plyr.browser.isTouch); // Inject the player wrapper if (plyr.type === 'video') { @@ -1503,7 +1949,7 @@ wrapper.setAttribute('class', config.classes.videoWrapper); // Wrap the video in a container - _wrap(plyr.media, wrapper); + wrap(plyr.media, wrapper); // Cache the container plyr.videoContainer = wrapper; @@ -1511,13 +1957,13 @@ } // Embeds - if (_inArray(config.types.embed, plyr.type)) { - _setupEmbed(); + if (inArray(config.types.embed, plyr.type)) { + setupEmbed(); } } // Setup YouTube/Vimeo - function _setupEmbed() { + function setupEmbed() { var container = document.createElement('div'), mediaId, id = plyr.type + '-' + Math.floor(Math.random() * (10000)); @@ -1525,11 +1971,11 @@ // Parse IDs from URLs if supplied switch (plyr.type) { case 'youtube': - mediaId = _parseYouTubeId(plyr.embedId); + mediaId = parseYouTubeId(plyr.embedId); break; case 'vimeo': - mediaId = _parseVimeoId(plyr.embedId); + mediaId = parseVimeoId(plyr.embedId); break; default: @@ -1537,14 +1983,14 @@ } // Remove old containers - var containers = _getElements('[id^="' + plyr.type + '-"]'); + var containers = getElements('[id^="' + plyr.type + '-"]'); for (var i = containers.length - 1; i >= 0; i--) { - _remove(containers[i]); + remove(containers[i]); } // Add embed class for responsive - _toggleClass(plyr.media, config.classes.videoWrapper, true); - _toggleClass(plyr.media, config.classes.embedWrapper, true); + toggleClass(plyr.media, config.classes.videoWrapper, true); + toggleClass(plyr.media, config.classes.embedWrapper, true); if (plyr.type === 'youtube') { // Create the YouTube container @@ -1554,21 +2000,25 @@ container.setAttribute('id', id); // Setup API - if (_is.object(window.YT)) { - _youTubeReady(mediaId, container); + if (is.object(window.YT)) { + youTubeReady(mediaId, container); } else { // Load the API - _injectScript(config.urls.youtube.api); + injectScript(config.urls.youtube.api); // Setup callback for the API window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || []; // Add to queue - window.onYouTubeReadyCallbacks.push(function() { _youTubeReady(mediaId, container); }); + window.onYouTubeReadyCallbacks.push(function() { + youTubeReady(mediaId, container); + }); // Set callback to process queue - window.onYouTubeIframeAPIReady = function () { - window.onYouTubeReadyCallbacks.forEach(function(callback) { callback(); }); + window.onYouTubeIframeAPIReady = function() { + window.onYouTubeReadyCallbacks.forEach(function(callback) { + callback(); + }); }; } } else if (plyr.type === 'vimeo') { @@ -1583,18 +2033,18 @@ container.setAttribute('id', id); // Load the API if not already - if (!_is.object(window.Vimeo)) { - _injectScript(config.urls.vimeo.api); + if (!is.object(window.Vimeo)) { + injectScript(config.urls.vimeo.api); // Wait for fragaloop load var vimeoTimer = window.setInterval(function() { - if (_is.object(window.Vimeo)) { + if (is.object(window.Vimeo)) { window.clearInterval(vimeoTimer); - _vimeoReady(mediaId, container); + vimeoReady(mediaId, container); } }, 50); } else { - _vimeoReady(mediaId, container); + vimeoReady(mediaId, container); } } else if (plyr.type === 'soundcloud') { // TODO: Currently unsupported and undocumented @@ -1603,11 +2053,13 @@ // Watch for iframe load soundCloud.loaded = false; - _on(soundCloud, 'load', function() { soundCloud.loaded = true; }); + on(soundCloud, 'load', function() { + soundCloud.loaded = true; + }); - _setAttributes(soundCloud, { - 'src': 'https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/' + mediaId, - 'id': id + setAttributes(soundCloud, { + 'src': 'https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/' + mediaId, + 'id': id }); container.appendChild(soundCloud); @@ -1615,57 +2067,66 @@ // Load the API if not already if (!window.SC) { - _injectScript(config.urls.soundcloud.api); + injectScript(config.urls.soundcloud.api); } // Wait for SC load var soundCloudTimer = window.setInterval(function() { if (window.SC && soundCloud.loaded) { window.clearInterval(soundCloudTimer); - _soundcloudReady.call(soundCloud); + soundcloudReady.call(soundCloud); } }, 50); } } // When embeds are ready - function _embedReady() { + function embedReady() { // Setup the UI and call ready if full support if (plyr.supported.full) { - _setupInterface(); - _ready(); + setupInterface(); + ready(); } // Set title - _setTitle(_getElement('iframe')); + setTitle(getElement('iframe')); } // Handle YouTube API ready - function _youTubeReady(videoId, container) { + function youTubeReady(videoId, container) { // Setup instance // https://developers.google.com/youtube/iframe_api_reference plyr.embed = new window.YT.Player(container.id, { videoId: videoId, playerVars: { - autoplay: (config.autoplay ? 1 : 0), - controls: (plyr.supported.full ? 0 : 1), - rel: 0, - showinfo: 0, + autoplay: (config.autoplay ? 1 : 0), + controls: (plyr.supported.full ? 0 : 1), + rel: 0, + showinfo: 0, iv_load_policy: 3, cc_load_policy: (config.captions.defaultActive ? 1 : 0), - cc_lang_pref: 'en', - wmode: 'transparent', + cc_lang_pref: 'en', + wmode: 'transparent', modestbranding: 1, - disablekb: 1, - origin: '*' // https://code.google.com/p/gdata-issues/issues/detail?id=5788#c45 + disablekb: 1, + origin: '*' // https://code.google.com/p/gdata-issues/issues/detail?id=5788#c45 }, events: { 'onError': function(event) { - _triggerEvent(plyr.container, 'error', true, { - code: event.data, - embed: event.target + trigger(plyr.container, 'error', true, { + code: event.data, + embed: event.target }); }, + 'onPlaybackQualityChange': function(event) { + // Get the instance + var instance = event.target; + + var quality = instance.getPlaybackQuality(); + + // var set = instance.setPlaybackQuality(); + console.warn(quality); + }, 'onReady': function(event) { // Get the instance var instance = event.target; @@ -1688,6 +2149,12 @@ plyr.media.currentTime = 0; plyr.media.muted = instance.isMuted(); + // Get available speeds + var speed = instance.getPlaybackRate(); + var speedOptions = instance.getAvailablePlaybackRates(); + //var set = instance.setPlaybackRate(); + console.warn(speed, speedOptions); + // Set title config.title = instance.getVideoData().title; @@ -1697,13 +2164,13 @@ } // Update UI - _embedReady(); + embedReady(); // Trigger timeupdate - _triggerEvent(plyr.media, 'timeupdate'); + trigger(plyr.media, 'timeupdate'); // Trigger timeupdate - _triggerEvent(plyr.media, 'durationchange'); + trigger(plyr.media, 'durationchange'); // Reset timer window.clearInterval(timers.buffering); @@ -1715,7 +2182,7 @@ // Trigger progress only when we actually buffer something if (plyr.media.lastBuffered === null || plyr.media.lastBuffered < plyr.media.buffered) { - _triggerEvent(plyr.media, 'progress'); + trigger(plyr.media, 'progress'); } // Set last buffer point @@ -1726,7 +2193,7 @@ window.clearInterval(timers.buffering); // Trigger event - _triggerEvent(plyr.media, 'canplaythrough'); + trigger(plyr.media, 'canplaythrough'); } }, 200); }, @@ -1747,7 +2214,7 @@ switch (event.data) { case 0: plyr.media.paused = true; - _triggerEvent(plyr.media, 'ended'); + trigger(plyr.media, 'ended'); break; case 1: @@ -1755,12 +2222,12 @@ // If we were seeking, fire seeked event if (plyr.media.seeking) { - _triggerEvent(plyr.media, 'seeked'); + trigger(plyr.media, 'seeked'); } plyr.media.seeking = false; - _triggerEvent(plyr.media, 'play'); - _triggerEvent(plyr.media, 'playing'); + trigger(plyr.media, 'play'); + trigger(plyr.media, 'playing'); // Poll to get playback progress timers.playing = window.setInterval(function() { @@ -1768,7 +2235,7 @@ plyr.media.currentTime = instance.getCurrentTime(); // Trigger timeupdate - _triggerEvent(plyr.media, 'timeupdate'); + trigger(plyr.media, 'timeupdate'); }, 100); // Check duration again due to YouTube bug @@ -1776,18 +2243,23 @@ // https://code.google.com/p/gdata-issues/issues/detail?id=8690 if (plyr.media.duration !== instance.getDuration()) { plyr.media.duration = instance.getDuration(); - _triggerEvent(plyr.media, 'durationchange'); + trigger(plyr.media, 'durationchange'); } + // Get quality + var qualityOptions = instance.getAvailableQualityLevels(); + var quality = instance.getPlaybackQuality(); + setQualityMenu(qualityOptions, quality); + break; case 2: plyr.media.paused = true; - _triggerEvent(plyr.media, 'pause'); + trigger(plyr.media, 'pause'); break; } - _triggerEvent(plyr.container, 'statechange', false, { + trigger(plyr.container, 'statechange', false, { code: event.data }); } @@ -1796,16 +2268,16 @@ } // Vimeo ready - function _vimeoReady(mediaId, container) { + function vimeoReady(mediaId, container) { // Setup instance // https://github.com/vimeo/player.js plyr.embed = new window.Vimeo.Player(container, { - id: parseInt(mediaId), - loop: config.loop, - autoplay: config.autoplay, - byline: false, - portrait: false, - title: false + id: parseInt(mediaId), + loop: config.loop.active, + autoplay: config.autoplay, + byline: false, + portrait: false, + title: false }); // Create a faux HTML5 API using the Vimeo API @@ -1826,20 +2298,20 @@ plyr.media.currentTime = 0; // Update UI - _embedReady(); + embedReady(); plyr.embed.getCurrentTime().then(function(value) { plyr.media.currentTime = value; // Trigger timeupdate - _triggerEvent(plyr.media, 'timeupdate'); + trigger(plyr.media, 'timeupdate'); }); plyr.embed.getDuration().then(function(value) { plyr.media.duration = value; // Trigger timeupdate - _triggerEvent(plyr.media, 'durationchange'); + trigger(plyr.media, 'durationchange'); }); // TODO: Captions @@ -1850,52 +2322,52 @@ plyr.embed.on('loaded', function() { // Fix keyboard focus issues // https://github.com/Selz/plyr/issues/317 - if (_is.htmlElement(plyr.embed.element) && plyr.supported.full) { + if (is.htmlElement(plyr.embed.element) && plyr.supported.full) { plyr.embed.element.setAttribute('tabindex', '-1'); } }); plyr.embed.on('play', function() { plyr.media.paused = false; - _triggerEvent(plyr.media, 'play'); - _triggerEvent(plyr.media, 'playing'); + trigger(plyr.media, 'play'); + trigger(plyr.media, 'playing'); }); plyr.embed.on('pause', function() { plyr.media.paused = true; - _triggerEvent(plyr.media, 'pause'); + trigger(plyr.media, 'pause'); }); plyr.embed.on('timeupdate', function(data) { plyr.media.seeking = false; plyr.media.currentTime = data.seconds; - _triggerEvent(plyr.media, 'timeupdate'); + trigger(plyr.media, 'timeupdate'); }); plyr.embed.on('progress', function(data) { plyr.media.buffered = data.percent; - _triggerEvent(plyr.media, 'progress'); + trigger(plyr.media, 'progress'); if (parseInt(data.percent) === 1) { // Trigger event - _triggerEvent(plyr.media, 'canplaythrough'); + trigger(plyr.media, 'canplaythrough'); } }); plyr.embed.on('seeked', function() { plyr.media.seeking = false; - _triggerEvent(plyr.media, 'seeked'); - _triggerEvent(plyr.media, 'play'); + trigger(plyr.media, 'seeked'); + trigger(plyr.media, 'play'); }); plyr.embed.on('ended', function() { plyr.media.paused = true; - _triggerEvent(plyr.media, 'ended'); + trigger(plyr.media, 'ended'); }); } // Soundcloud ready - function _soundcloudReady() { + function soundcloudReady() { /* jshint validthis: true */ plyr.embed = window.SC.Widget(this); @@ -1920,111 +2392,231 @@ plyr.media.currentTime = 0; plyr.embed.getDuration(function(value) { - plyr.media.duration = value/1000; + plyr.media.duration = value / 1000; // Update UI - _embedReady(); + embedReady(); }); plyr.embed.getPosition(function(value) { plyr.media.currentTime = value; // Trigger timeupdate - _triggerEvent(plyr.media, 'timeupdate'); + trigger(plyr.media, 'timeupdate'); }); plyr.embed.bind(window.SC.Widget.Events.PLAY, function() { plyr.media.paused = false; - _triggerEvent(plyr.media, 'play'); - _triggerEvent(plyr.media, 'playing'); + trigger(plyr.media, 'play'); + trigger(plyr.media, 'playing'); }); plyr.embed.bind(window.SC.Widget.Events.PAUSE, function() { plyr.media.paused = true; - _triggerEvent(plyr.media, 'pause'); + trigger(plyr.media, 'pause'); }); plyr.embed.bind(window.SC.Widget.Events.PLAY_PROGRESS, function(data) { plyr.media.seeking = false; - plyr.media.currentTime = data.currentPosition/1000; - _triggerEvent(plyr.media, 'timeupdate'); + plyr.media.currentTime = data.currentPosition / 1000; + trigger(plyr.media, 'timeupdate'); }); plyr.embed.bind(window.SC.Widget.Events.LOAD_PROGRESS, function(data) { plyr.media.buffered = data.loadProgress; - _triggerEvent(plyr.media, 'progress'); + trigger(plyr.media, 'progress'); if (parseInt(data.loadProgress) === 1) { // Trigger event - _triggerEvent(plyr.media, 'canplaythrough'); + trigger(plyr.media, 'canplaythrough'); } }); plyr.embed.bind(window.SC.Widget.Events.FINISH, function() { plyr.media.paused = true; - _triggerEvent(plyr.media, 'ended'); + trigger(plyr.media, 'ended'); }); }); } // Play media - function _play() { + function play() { if ('play' in plyr.media) { plyr.media.play(); } } // Pause media - function _pause() { + function pause() { if ('pause' in plyr.media) { plyr.media.pause(); } } // Toggle playback - function _togglePlay(toggle) { + function togglePlay(toggle) { // True toggle - if (!_is.boolean(toggle)) { + if (!is.boolean(toggle)) { toggle = plyr.media.paused; } if (toggle) { - _play(); + play(); } else { - _pause(); + pause(); } return toggle; } + // Toggle loop + // TODO: Set the indicator on load as user may pass loop as config + function toggleLoop(type) { + // Set default to be a true toggle + if (!inArray(['start', 'end', 'all', 'none', 'toggle'], type)) { + type = 'toggle'; + } + + var currentTime = Number(plyr.media.currentTime); + + switch (type) { + case 'start': + if (config.loop.end && config.loop.end <= currentTime) { + config.loop.end = null; + } + config.loop.start = currentTime; + config.loop.indicator.start = plyr.progress.played.value; + break; + + case 'end': + if (config.loop.start >= currentTime) { + return; + } + config.loop.end = currentTime; + config.loop.indicator.end = plyr.progress.played.value; + break; + + case 'all': + config.loop.start = 0; + config.loop.end = plyr.media.duration - 2; + config.loop.indicator.start = 0; + config.loop.indicator.end = 100; + break; + + case 'toggle': + if (config.loop.active) { + config.loop.start = 0; + config.loop.end = null; + } else { + config.loop.start = 0; + config.loop.end = plyr.media.duration - 2; + } + break; + + default: + config.loop.start = 0; + config.loop.end = null; + break; + } + + // Check if can loop + config.loop.active = is.number(config.loop.start) && is.number(config.loop.end); + var start = updateTimeDisplay(config.loop.start, getElement('[data-plyr-loop="start"]')); + var end = null; + + if (is.number(config.loop.end)) { + // Find the <span> inside button + end = updateTimeDisplay(config.loop.end, document.querySelector('[data-loop__value="loopout"]')); + } else { + // Find the <span> inside button + //end = document.querySelector('[data-loop__value="loopout"]').innerHTML = ''; + } + + if (config.loop.active) { + // TODO: Improve the design of the loop indicator and put styling in CSS where it's meant to be + //getElement('[data-menu="loop"]').innerHTML = start + ' - ' + end; + //getElement(config.selectors.progress.looped).style.position = 'absolute'; + //getElement(config.selectors.progress.looped).style.left = config.loopinPositionPercentage + '%'; + //getElement(config.selectors.progress.looped).style.width = (config.loopoutPositionPercentage - config.loopinPositionPercentage) + '%'; + //getElement(config.selectors.progress.looped).style.background = '#ffbb00'; + //getElement(config.selectors.progress.looped).style.height = '3px'; + //getElement(config.selectors.progress.looped).style.top = '3px'; + //getElement(config.selectors.progress.looped).style['border-radius'] = '100px'; + } else { + //getElement('[data-menu="loop"]').innerHTML = config.i18n.loopNone; + //getElement(config.selectors.progress.looped).style.width = '0px'; + } + } + + // Speed-up + function setSpeed(speed) { + // Load speed from storage or default value + if (is.undefined(speed)) { + speed = plyr.storage.speed || config.defaultSpeed; + } + + if (!is.array(config.speeds)) { + warn('Invalid speeds format'); + return; + } + + if (!is.number(speed)) { + var index = config.speeds.indexOf(config.currentSpeed); + + if (index !== -1) { + var nextIndex = index + 1; + if (nextIndex >= config.speeds.length) { + nextIndex = 0; + } + speed = config.speeds[nextIndex]; + } else { + speed = config.defaultSpeed; + } + } + + // Store current speed + config.currentSpeed = speed; + + // Set HTML5 speed + plyr.media.playbackRate = speed; + + // Save speed to localStorage + updateStorage({ + speed: speed + }); + + //Update current value of menu + document.querySelector('[data-menu="speed"]').innerHTML = getSpeedDisplayValue(); + } + // Rewind - function _rewind(seekTime) { + function rewind(seekTime) { // Use default if needed - if (!_is.number(seekTime)) { + if (!is.number(seekTime)) { seekTime = config.seekTime; } - _seek(plyr.media.currentTime - seekTime); + seek(plyr.media.currentTime - seekTime); } // Fast forward - function _forward(seekTime) { + function forward(seekTime) { // Use default if needed - if (!_is.number(seekTime)) { + if (!is.number(seekTime)) { seekTime = config.seekTime; } - _seek(plyr.media.currentTime + seekTime); + seek(plyr.media.currentTime + seekTime); } // Seek to time // The input parameter can be an event or a number - function _seek(input) { - var targetTime = 0, - paused = plyr.media.paused, - duration = _getDuration(); + function seek(input) { + var targetTime = 0, + paused = plyr.media.paused, + duration = getDuration(); - if (_is.number(input)) { + if (is.number(input)) { targetTime = input; - } else if (_is.object(input) && _inArray(['input', 'change'], input.type)) { + } else if (is.event(input) && inArray(['input', 'change'], input.type)) { // It's the seek slider // Seek to the selected time targetTime = ((input.target.value / input.target.max) * duration); @@ -2038,18 +2630,17 @@ } // Update seek range and progress - _updateSeekDisplay(targetTime); + updateSeekDisplay(targetTime); // 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(4); - } - catch(e) {} + } catch (e) {} // Embeds - if (_inArray(config.types.embed, plyr.type)) { - switch(plyr.type) { + if (inArray(config.types.embed, plyr.type)) { + switch (plyr.type) { case 'youtube': plyr.embed.seekTo(targetTime); break; @@ -2065,33 +2656,33 @@ } if (paused) { - _pause(); + pause(); } // Trigger timeupdate - _triggerEvent(plyr.media, 'timeupdate'); + trigger(plyr.media, 'timeupdate'); // Set seeking flag plyr.media.seeking = true; // Trigger seeking - _triggerEvent(plyr.media, 'seeking'); + trigger(plyr.media, 'seeking'); } // Logging - _log('Seeking to ' + plyr.media.currentTime + ' seconds'); + log('Seeking to ' + plyr.media.currentTime + ' seconds'); // Special handling for 'manual' captions - _seekManualCaptions(targetTime); + seekManualCaptions(targetTime); } // Get the duration (or custom if set) - function _getDuration() { + function getDuration() { // It should be a number, but parse it just incase var duration = parseInt(config.duration), - // True duration - mediaDuration = 0; + // True duration + mediaDuration = 0; // Only if duration available if (plyr.media.duration !== null && !isNaN(plyr.media.duration)) { @@ -2103,16 +2694,16 @@ } // Check playing state - function _checkPlaying() { - _toggleClass(plyr.container, config.classes.playing, !plyr.media.paused); + function checkPlaying() { + toggleClass(plyr.container, config.classes.playing, !plyr.media.paused); - _toggleClass(plyr.container, config.classes.stopped, plyr.media.paused); + toggleClass(plyr.container, config.classes.stopped, plyr.media.paused); - _toggleControls(plyr.media.paused); + toggleControls(plyr.media.paused); } // Save scroll position - function _saveScrollPosition() { + function saveScrollPosition() { scroll = { x: window.pageXOffset || 0, y: window.pageYOffset || 0 @@ -2120,24 +2711,24 @@ } // Restore scroll position - function _restoreScrollPosition() { + function restoreScrollPosition() { window.scrollTo(scroll.x, scroll.y); } // Toggle fullscreen - function _toggleFullscreen(event) { + function toggleFullscreen(event) { // Check for native support - var nativeSupport = fullscreen.supportsFullScreen; + var nativeSupport = support.fullscreen; if (nativeSupport) { // If it's a fullscreen change event, update the UI - if (event && event.type === fullscreen.fullScreenEventName) { + if (event && event.type === fullscreen.eventType) { plyr.isFullscreen = fullscreen.isFullScreen(plyr.container); } else { // Else it's a user request to enter or exit if (!fullscreen.isFullScreen(plyr.container)) { // Save scroll position - _saveScrollPosition(); + saveScrollPosition(); // Request full screen fullscreen.requestFullScreen(plyr.container); @@ -2160,47 +2751,47 @@ } // Set class hook - _toggleClass(plyr.container, config.classes.fullscreen.active, plyr.isFullscreen); + toggleClass(plyr.container, config.classes.fullscreen.active, plyr.isFullscreen); // Trap focus - _focusTrap(plyr.isFullscreen); + focusTrap(plyr.isFullscreen); // Set button state if (plyr.buttons && plyr.buttons.fullscreen) { - _toggleState(plyr.buttons.fullscreen, plyr.isFullscreen); + toggleState(plyr.buttons.fullscreen, plyr.isFullscreen); } // Trigger an event - _triggerEvent(plyr.container, plyr.isFullscreen ? 'enterfullscreen' : 'exitfullscreen', true); + trigger(plyr.container, plyr.isFullscreen ? 'enterfullscreen' : 'exitfullscreen', true); // Restore scroll position if (!plyr.isFullscreen && nativeSupport) { - _restoreScrollPosition(); + restoreScrollPosition(); } } // Mute - function _toggleMute(muted) { + function toggleMute(muted) { // If the method is called without parameter, toggle based on current value - if (!_is.boolean(muted)) { + if (!is.boolean(muted)) { muted = !plyr.media.muted; } // Set button state - _toggleState(plyr.buttons.mute, muted); + toggleState(plyr.buttons.mute, muted); // Set mute on the player plyr.media.muted = muted; // If volume is 0 after unmuting, set to default if (plyr.media.volume === 0) { - _setVolume(config.volume); + setVolume(config.volume); } // Embeds - if (_inArray(config.types.embed, plyr.type)) { + if (inArray(config.types.embed, plyr.type)) { // YouTube - switch(plyr.type) { + switch (plyr.type) { case 'youtube': plyr.embed[plyr.media.muted ? 'mute' : 'unMute'](); break; @@ -2212,17 +2803,17 @@ } // Trigger volumechange for embeds - _triggerEvent(plyr.media, 'volumechange'); + trigger(plyr.media, 'volumechange'); } } // Set volume - function _setVolume(volume) { + function setVolume(volume) { var max = config.volumeMax, min = config.volumeMin; // Load volume from storage if no value specified - if (_is.undefined(volume)) { + if (is.undefined(volume)) { volume = plyr.storage.volume; } @@ -2249,8 +2840,8 @@ } // Embeds - if (_inArray(config.types.embed, plyr.type)) { - switch(plyr.type) { + if (inArray(config.types.embed, plyr.type)) { + switch (plyr.type) { case 'youtube': plyr.embed.setVolume(plyr.media.volume * 100); break; @@ -2262,41 +2853,41 @@ } // Trigger volumechange for embeds - _triggerEvent(plyr.media, 'volumechange'); + trigger(plyr.media, 'volumechange'); } // Toggle muted state if (volume === 0) { plyr.media.muted = true; } else if (plyr.media.muted && volume > 0) { - _toggleMute(); + toggleMute(); } } // Increase volume - function _increaseVolume(step) { + function increaseVolume(step) { var volume = plyr.media.muted ? 0 : (plyr.media.volume * config.volumeMax); - if (!_is.number(step)) { + if (!is.number(step)) { step = config.volumeStep; } - _setVolume(volume + step); + setVolume(volume + step); } // Decrease volume - function _decreaseVolume(step) { + function decreaseVolume(step) { var volume = plyr.media.muted ? 0 : (plyr.media.volume * config.volumeMax); - if (!_is.number(step)) { + if (!is.number(step)) { step = config.volumeStep; } - _setVolume(volume - step); + setVolume(volume - step); } // Update volume UI and storage - function _updateVolume() { + function updateVolume() { // Get the current volume var volume = plyr.media.muted ? 0 : (plyr.media.volume * config.volumeMax); @@ -2311,26 +2902,28 @@ } // Update the volume in storage - _updateStorage({volume: volume}); + updateStorage({ + volume: volume + }); // Toggle class if muted - _toggleClass(plyr.container, config.classes.muted, (volume === 0)); + toggleClass(plyr.container, config.classes.muted, (volume === 0)); // Update checkbox for mute state if (plyr.supported.full && plyr.buttons.mute) { - _toggleState(plyr.buttons.mute, (volume === 0)); + toggleState(plyr.buttons.mute, (volume === 0)); } } // Toggle captions - function _toggleCaptions(show) { + function toggleCaptions(show) { // If there's no full support, or there's no caption toggle if (!plyr.supported.full || !plyr.buttons.captions) { return; } // If the method is called without parameter, toggle based on current value - if (!_is.boolean(show)) { + if (!is.boolean(show)) { show = (plyr.container.className.indexOf(config.classes.captions.active) === -1); } @@ -2338,20 +2931,34 @@ plyr.captionsEnabled = show; // Toggle state - _toggleState(plyr.buttons.captions, plyr.captionsEnabled); + toggleState(plyr.buttons.captions, plyr.captionsEnabled); // Add class hook - _toggleClass(plyr.container, config.classes.captions.active, plyr.captionsEnabled); + toggleClass(plyr.container, config.classes.captions.active, plyr.captionsEnabled); // Trigger an event - _triggerEvent(plyr.container, plyr.captionsEnabled ? 'captionsenabled' : 'captionsdisabled', true); + trigger(plyr.container, plyr.captionsEnabled ? 'captionsenabled' : 'captionsdisabled', true); // Save captions state to localStorage - _updateStorage({captionsEnabled: plyr.captionsEnabled}); + updateStorage({ + captionsEnabled: plyr.captionsEnabled + }); + } + + // Select active caption + function setCaptionIndex(index) { + // Save active caption + config.captions.selectedIndex = index; + + // Clear caption + setCaption(); + + // Re-run setup + setupCaptions(); } // Check if media is loading - function _checkLoading(event) { + function checkLoading(event) { var loading = (event.type === 'waiting'); // Clear timer @@ -2360,22 +2967,22 @@ // Timer to prevent flicker when seeking timers.loading = setTimeout(function() { // Toggle container class hook - _toggleClass(plyr.container, config.classes.loading, loading); + toggleClass(plyr.container, config.classes.loading, loading); // Show controls if loading, hide if done - _toggleControls(loading); + toggleControls(loading); }, (loading ? 250 : 0)); } // Update <progress> elements - function _updateProgress(event) { + function updateProgress(event) { if (!plyr.supported.full) { return; } - var progress = plyr.progress.played, - value = 0, - duration = _getDuration(); + var progress = plyr.progress.played, + value = 0, + duration = getDuration(); if (event) { switch (event.type) { @@ -2386,7 +2993,7 @@ return; } - value = _getPercentage(plyr.media.currentTime, duration); + value = getPercentage(plyr.media.currentTime, duration); // Set seek range value only if it's a 'natural' time event if (event.type === 'timeupdate' && plyr.buttons.seek) { @@ -2395,17 +3002,17 @@ break; - // Check buffer status + // Check buffer status case 'playing': case 'progress': - progress = plyr.progress.buffer; - value = (function() { + progress = plyr.progress.buffer; + value = (function() { var buffered = plyr.media.buffered; if (buffered && buffered.length) { // HTML5 - return _getPercentage(buffered.end(0), duration); - } else if (_is.number(buffered)) { + return getPercentage(buffered.end(0), duration); + } else if (is.number(buffered)) { // YouTube returns between 0 and 1 return (buffered * 100); } @@ -2417,22 +3024,25 @@ } } - // Set values - _setProgress(progress, value); + if (is.number(config.loop.start) && is.number(config.loop.end) && plyr.media.currentTime >= config.loop.end) { + seek(config.loop.start); + } + + setProgress(progress, value); } // Set <progress> value - function _setProgress(progress, value) { + function setProgress(progress, value) { if (!plyr.supported.full) { return; } // Default to 0 - if (_is.undefined(value)) { + if (is.undefined(value)) { value = 0; } // Default to buffer or bail - if (_is.undefined(progress)) { + if (is.undefined(progress)) { if (plyr.progress && plyr.progress.buffer) { progress = plyr.progress.buffer; } else { @@ -2441,7 +3051,7 @@ } // One progress element passed - if (_is.htmlElement(progress)) { + if (is.htmlElement(progress)) { progress.value = value; } else if (progress) { // Object of progress + text element @@ -2455,7 +3065,7 @@ } // Update the displayed time - function _updateTimeDisplay(time, element) { + function updateTimeDisplay(time, element) { // Bail if there's no duration display if (!element) { return; @@ -2471,43 +3081,49 @@ plyr.hours = parseInt(((time / 60) / 60) % 60); // Do we need to display hours? - var displayHours = (parseInt(((_getDuration() / 60) / 60) % 60) > 0); + var displayHours = (parseInt(((getDuration() / 60) / 60) % 60) > 0); // Ensure it's two digits. For example, 03 rather than 3. plyr.secs = ('0' + plyr.secs).slice(-2); plyr.mins = ('0' + plyr.mins).slice(-2); + // Generate display + var display = (displayHours ? plyr.hours + ':' : '') + plyr.mins + ':' + plyr.secs; + // Render - element.innerHTML = (displayHours ? plyr.hours + ':' : '') + plyr.mins + ':' + plyr.secs; + element.innerHTML = display; + + // Return for looping + return display; } // Show the duration on metadataloaded - function _displayDuration() { + function displayDuration() { if (!plyr.supported.full) { return; } // Determine duration - var duration = _getDuration() || 0; + var duration = getDuration() || 0; // If there's only one time display, display duration there if (!plyr.duration && config.displayDuration && plyr.media.paused) { - _updateTimeDisplay(duration, plyr.currentTime); + updateTimeDisplay(duration, plyr.currentTime); } // If there's a duration element, update content if (plyr.duration) { - _updateTimeDisplay(duration, plyr.duration); + updateTimeDisplay(duration, plyr.duration); } // Update the tooltip (if visible) - _updateSeekTooltip(); + updateSeekTooltip(); } // Handle time change event - function _timeUpdate(event) { + function timeUpdate(event) { // Duration - _updateTimeDisplay(plyr.media.currentTime, plyr.currentTime); + updateTimeDisplay(plyr.media.currentTime, plyr.currentTime); // Ignore updates while seeking if (event && event.type === 'timeupdate' && plyr.media.seeking) { @@ -2515,18 +3131,18 @@ } // Playing progress - _updateProgress(event); + updateProgress(event); } // Update seek range and progress - function _updateSeekDisplay(time) { + function updateSeekDisplay(time) { // Default to 0 - if (!_is.number(time)) { + if (!is.number(time)) { time = 0; } - var duration = _getDuration(), - value = _getPercentage(time, duration); + var duration = getDuration(), + value = getPercentage(time, duration); // Update progress if (plyr.progress && plyr.progress.played) { @@ -2540,8 +3156,8 @@ } // Update hover tooltip for seeking - function _updateSeekTooltip(event) { - var duration = _getDuration(); + function updateSeekTooltip(event) { + var duration = getDuration(); // Bail if setting not true if (!config.tooltips.seek || !plyr.progress.container || duration === 0) { @@ -2549,13 +3165,13 @@ } // Calculate percentage - var clientRect = plyr.progress.container.getBoundingClientRect(), - percent = 0, - visible = config.classes.tooltip + '--visible'; + var clientRect = plyr.progress.container.getBoundingClientRect(), + percent = 0, + visible = config.classes.tooltip + '--visible'; // Determine percentage, if already visible if (!event) { - if (_hasClass(plyr.progress.tooltip, visible)) { + if (hasClass(plyr.progress.tooltip, visible)) { percent = plyr.progress.tooltip.style.left.replace('%', ''); } else { return; @@ -2572,20 +3188,20 @@ } // Display the time a click would seek to - _updateTimeDisplay(((duration / 100) * percent), plyr.progress.tooltip); + updateTimeDisplay(((duration / 100) * percent), plyr.progress.tooltip); // Set position plyr.progress.tooltip.style.left = percent + "%"; // Show/hide the tooltip // If the event is a moues in/out and percentage is inside bounds - if (event && _inArray(['mouseenter', 'mouseleave'], event.type)) { - _toggleClass(plyr.progress.tooltip, visible, (event.type === 'mouseenter')); + if (event && inArray(['mouseenter', 'mouseleave'], event.type)) { + toggleClass(plyr.progress.tooltip, visible, (event.type === 'mouseenter')); } } // Show the player controls in fullscreen mode - function _toggleControls(toggle) { + function toggleControls(toggle) { // Don't hide if config says not to, it's audio, or not ready or loading if (!config.hideControls || plyr.type === 'audio') { return; @@ -2594,19 +3210,19 @@ var delay = 0, isEnterFullscreen = false, show = toggle, - loading = _hasClass(plyr.container, config.classes.loading); + loading = hasClass(plyr.container, config.classes.loading); // Default to false if no boolean - if (!_is.boolean(toggle)) { + if (!is.boolean(toggle)) { if (toggle && toggle.type) { // Is the enter fullscreen event isEnterFullscreen = (toggle.type === 'enterfullscreen'); // Whether to show controls - show = _inArray(['mousemove', 'touchstart', 'mouseenter', 'focus'], toggle.type); + show = inArray(['mousemove', 'touchstart', 'mouseenter', 'focus'], toggle.type); // Delay hiding on move events - if (_inArray(['mousemove', 'touchmove'], toggle.type)) { + if (inArray(['mousemove', 'touchmove'], toggle.type)) { delay = 2000; } @@ -2615,7 +3231,7 @@ delay = 3000; } } else { - show = _hasClass(plyr.container, config.classes.hideControls); + show = hasClass(plyr.container, config.classes.hideControls); } } @@ -2624,7 +3240,7 @@ // If the mouse is not over the controls, set a timeout to hide them if (show || plyr.media.paused || loading) { - _toggleClass(plyr.container, config.classes.hideControls, false); + toggleClass(plyr.container, config.classes.hideControls, false); // Always show controls when paused or if touch if (plyr.media.paused || loading) { @@ -2646,28 +3262,28 @@ return; } - _toggleClass(plyr.container, config.classes.hideControls, true); + toggleClass(plyr.container, config.classes.hideControls, true); }, delay); } } // Add common function to retrieve media source - function _source(source) { + function source(source) { // If not null or undefined, parse it - if (!_is.undefined(source)) { - _updateSource(source); + if (!is.undefined(source)) { + updateSource(source); return; } // Return the current source var url; - switch(plyr.type) { + switch (plyr.type) { case 'youtube': url = plyr.embed.getVideoUrl(); break; case 'vimeo': - plyr.embed.getVideoUrl.then(function (value) { + plyr.embed.getVideoUrl.then(function(value) { url = value; }); break; @@ -2688,26 +3304,26 @@ // Update source // Sources are not checked for support so be careful - function _updateSource(source) { - if (!_is.object(source) || !('sources' in source) || !source.sources.length) { - _warn('Invalid source format'); + function updateSource(source) { + if (!is.object(source) || !('sources' in source) || !source.sources.length) { + warn('Invalid source format'); return; } // Remove ready class hook - _toggleClass(plyr.container, config.classes.ready, false); + toggleClass(plyr.container, config.classes.ready, false); // Pause playback - _pause(); + pause(); // Update seek range and progress - _updateSeekDisplay(); + updateSeekDisplay(); // Reset buffer progress - _setProgress(); + setProgress(); // Cancel current network requests - _cancelRequests(); + cancelRequests(); // Setup new source function setup() { @@ -2715,11 +3331,11 @@ plyr.embed = null; // Remove the old media - _remove(plyr.media); + remove(plyr.media); // Remove video container if (plyr.type === 'video' && plyr.videoContainer) { - _remove(plyr.videoContainer); + remove(plyr.videoContainer); } // Reset class name @@ -2735,7 +3351,7 @@ if (plyr.type === 'video') { var firstSource = source.sources[0]; - if ('type' in firstSource && _inArray(config.types.embed, firstSource.type)) { + if ('type' in firstSource && inArray(config.types.embed, firstSource.type)) { plyr.type = firstSource.type; } } @@ -2745,7 +3361,7 @@ plyr.supported = supported(plyr.type); // Create new markup - switch(plyr.type) { + switch (plyr.type) { case 'video': plyr.media = document.createElement('video'); break; @@ -2763,15 +3379,15 @@ } // Inject the new element - _prependChild(plyr.container, plyr.media); + prependChild(plyr.container, plyr.media); // Autoplay the new source? - if (_is.boolean(source.autoplay)) { + if (is.boolean(source.autoplay)) { config.autoplay = source.autoplay; } // Set attributes for audio and video - if (_inArray(config.types.html5, plyr.type)) { + if (inArray(config.types.html5, plyr.type)) { if (config.crossorigin) { plyr.media.setAttribute('crossorigin', ''); } @@ -2781,29 +3397,29 @@ if ('poster' in source) { plyr.media.setAttribute('poster', source.poster); } - if (config.loop) { + if (config.loop.active) { plyr.media.setAttribute('loop', ''); } } // Restore class hooks - _toggleClass(plyr.container, config.classes.fullscreen.active, plyr.isFullscreen); - _toggleClass(plyr.container, config.classes.captions.active, plyr.captionsEnabled); - _toggleStyleHook(); + toggleClass(plyr.container, config.classes.fullscreen.active, plyr.isFullscreen); + toggleClass(plyr.container, config.classes.captions.active, plyr.captionsEnabled); + toggleStyleHook(); // Set new sources for html5 - if (_inArray(config.types.html5, plyr.type)) { - _insertChildElements('source', source.sources); + if (inArray(config.types.html5, plyr.type)) { + insertChildElements('source', source.sources); } // Set up from scratch - _setupMedia(); + setupMedia(); // HTML5 stuff - if (_inArray(config.types.html5, plyr.type)) { + if (inArray(config.types.html5, plyr.type)) { // Setup captions if ('tracks' in source) { - _insertChildElements('track', source.tracks); + insertChildElements('track', source.tracks); } // Load HTML5 sources @@ -2811,39 +3427,39 @@ } // If HTML5 or embed but not fully supported, setupInterface and call ready now - if (_inArray(config.types.html5, plyr.type) || (_inArray(config.types.embed, plyr.type) && !plyr.supported.full)) { + if (inArray(config.types.html5, plyr.type) || (inArray(config.types.embed, plyr.type) && !plyr.supported.full)) { // Setup interface - _setupInterface(); + setupInterface(); // Call ready - _ready(); + ready(); } // Set aria title and iframe title config.title = source.title; - _setTitle(); + setTitle(); } // Destroy instance adn wait for callback // Vimeo throws a wobbly if you don't wait - _destroy(setup, false); + destroy(setup, false); } // Update poster - function _updatePoster(source) { + function updatePoster(source) { if (plyr.type === 'video') { plyr.media.setAttribute('poster', source); } } // Listen for control events - function _controlListeners() { + function controlListeners() { // IE doesn't support input event, so we fallback to change var inputEvent = (plyr.browser.isIE ? 'change' : 'input'); // Click play/pause helper - function togglePlay() { - var play = _togglePlay(); + function _togglePlay() { + var play = togglePlay(); // Determine which buttons var trigger = plyr.buttons[play ? 'play' : 'pause'], @@ -2858,14 +3474,14 @@ // Setup focus and tab focus if (target) { - var hadTabFocus = _hasClass(trigger, config.classes.tabFocus); + var hadTabFocus = hasClass(trigger, config.classes.tabFocus); setTimeout(function() { target.focus(); if (hadTabFocus) { - _toggleClass(trigger, config.classes.tabFocus, false); - _toggleClass(target, config.classes.tabFocus, true); + toggleClass(trigger, config.classes.tabFocus, false); + toggleClass(target, config.classes.tabFocus, true); } }, 100); } @@ -2894,50 +3510,50 @@ for (var button in plyr.buttons) { var element = plyr.buttons[button]; - if (_is.nodeList(element)) { + if (is.nodeList(element)) { for (var i = 0; i < element.length; i++) { - _toggleClass(element[i], config.classes.tabFocus, (element[i] === focused)); + toggleClass(element[i], config.classes.tabFocus, (element[i] === focused)); } } else { - _toggleClass(element, config.classes.tabFocus, (element === focused)); + toggleClass(element, config.classes.tabFocus, (element === focused)); } } } // Keyboard shortcuts - if (config.keyboardShorcuts.focused) { + if (config.keyboardShortcuts.focused) { var last = null; // Handle global presses - if (config.keyboardShorcuts.global) { - _on(window, 'keydown keyup', function(event) { - var code = getKeyCode(event), - focused = getFocusElement(), - allowed = [48,49,50,51,52,53,54,56,57,75,77,70,67], - count = get().length; + if (config.keyboardShortcuts.global) { + on(window, 'keydown keyup', function(event) { + var code = getKeyCode(event); + var focused = getFocusElement(); + var allowed = [48, 49, 50, 51, 52, 53, 54, 56, 57, 75, 77, 70, 67, 73, 76, 79]; + var count = get().length; // Only handle global key press if there's only one player // and the key is in the allowed keys // and if the focused element is not editable (e.g. text input) // and any that accept key input http://webaim.org/techniques/keyboard/ - if (count === 1 && _inArray(allowed, code) && (!_is.htmlElement(focused) || !_matches(focused, config.selectors.editable))) { + if (count === 1 && inArray(allowed, code) && (!is.htmlElement(focused) || !matches(focused, config.selectors.editable))) { handleKey(event); } }); } // Handle presses on focused - _on(plyr.container, 'keydown keyup', handleKey); + on(plyr.container, 'keydown keyup', handleKey); } function handleKey(event) { - var code = getKeyCode(event), - pressed = event.type === 'keydown', - held = pressed && code === last; + var code = getKeyCode(event); + var pressed = event.type === 'keydown'; + var held = pressed && code === last; // If the event is bubbled from the media element // Firefox doesn't get the keycode for whatever reason - if (!_is.number(code)) { + if (!is.number(code)) { return; } @@ -2947,28 +3563,36 @@ var duration = plyr.media.duration; // Bail if we have no duration set - if (!_is.number(duration)) { + if (!is.number(duration)) { return; } // Divide the max duration into 10th's and times by the number value - _seek((duration / 10) * (code - 48)); + seek((duration / 10) * (code - 48)); } // Handle the key on keydown // Reset on keyup if (pressed) { // Which keycodes should we prevent default - var preventDefault = [48,49,50,51,52,53,54,56,57,32,75,38,40,77,39,37,70,67]; + var preventDefault = [48, 49, 50, 51, 52, 53, 54, 56, 57, 32, 75, 38, 40, 77, 39, 37, 70, 67, 73, 76, 79]; + var checkFocus = [38, 40]; + + if (inArray(checkFocus, code)) { + var focused = getFocusElement(); + + if (is.htmlElement(focused) && getFocusElement().type === "radio") { + return; + } + } // If the code is found prevent default (e.g. prevent scrolling for arrows) - if (_inArray(preventDefault, code)) { + if (inArray(preventDefault, code)) { event.preventDefault(); event.stopPropagation(); } - switch(code) { - // 0-9 + switch (code) { case 48: case 49: case 50: @@ -2978,30 +3602,77 @@ case 54: case 55: case 56: - case 57: if (!held) { seekByKey(); } break; - // Space and K key + case 57: + // 0-9 + if (!held) { + seekByKey(); + } + break; + case 32: - case 75: if (!held) { _togglePlay(); } break; - // Arrow up - case 38: _increaseVolume(); break; - // Arrow down - case 40: _decreaseVolume(); break; - // M key - case 77: if (!held) { _toggleMute() } break; - // Arrow forward - case 39: _forward(); break; - // Arrow back - case 37: _rewind(); break; - // F key - case 70: _toggleFullscreen(); break; - // C key - case 67: if (!held) { _toggleCaptions(); } break; + case 75: + // Space and K key + if (!held) { + _togglePlay(); + } + break; + + case 38: + // Arrow up + increaseVolume(); + break; + + case 40: + // Arrow down + decreaseVolume(); + break; + + case 77: + // M key + if (!held) { + toggleMute(); + } + break; + + case 39: + // Arrow forward + forward(); + break; + + case 37: + // Arrow back + rewind(); + break; + + case 70: + // F key + toggleFullscreen(); + break; + + case 67: + // C key + if (!held) { + toggleCaptions(); + } + break; + + case 73: + toggleLoop('start'); + break; + + case 76: + toggleLoop(); + break; + + case 79: + toggleLoop('end'); + break; } // Escape is handle natively when in full screen // So we only need to worry about non native - if (!fullscreen.supportsFullScreen && plyr.isFullscreen && code === 27) { - _toggleFullscreen(); + if (!support.fullscreen && plyr.isFullscreen && code === 27) { + toggleFullscreen(); } // Store last code for next cycle @@ -3012,7 +3683,7 @@ } // Focus/tab management - _on(window, 'keyup', function(event) { + on(window, 'keyup', function(event) { var code = getKeyCode(event), focused = getFocusElement(); @@ -3020,78 +3691,159 @@ checkTabFocus(focused); } }); - _on(document.body, 'click', function() { - _toggleClass(_getElement('.' + config.classes.tabFocus), config.classes.tabFocus, false); + on(document.body, 'click', function() { + toggleClass(getElement('.' + config.classes.tabFocus), config.classes.tabFocus, false); }); for (var button in plyr.buttons) { var element = plyr.buttons[button]; - _on(element, 'blur', function() { - _toggleClass(element, 'tab-focus', false); + on(element, 'blur', function() { + toggleClass(element, 'tab-focus', false); }); } // Play - _proxyListener(plyr.buttons.play, 'click', config.listeners.play, togglePlay); + proxy(plyr.buttons.play, 'click', config.listeners.play, _togglePlay); // Pause - _proxyListener(plyr.buttons.pause, 'click', config.listeners.pause, togglePlay); + proxy(plyr.buttons.pause, 'click', config.listeners.pause, _togglePlay); // Restart - _proxyListener(plyr.buttons.restart, 'click', config.listeners.restart, _seek); + proxy(plyr.buttons.restart, 'click', config.listeners.restart, seek); // Rewind - _proxyListener(plyr.buttons.rewind, 'click', config.listeners.rewind, _rewind); + proxy(plyr.buttons.rewind, 'click', config.listeners.rewind, rewind); // Fast forward - _proxyListener(plyr.buttons.forward, 'click', config.listeners.forward, _forward); + proxy(plyr.buttons.forward, 'click', config.listeners.forward, forward); + + // Speed-up + proxy(plyr.buttons.speed, 'click', config.listeners.speed, function() { + var speedValue = document.querySelector('[data-plyr="speed"]:checked').value; + setSpeed(Number(speedValue)); + }); // Seek - _proxyListener(plyr.buttons.seek, inputEvent, config.listeners.seek, _seek); + proxy(plyr.buttons.seek, inputEvent, config.listeners.seek, seek); // Set volume - _proxyListener(plyr.volume.input, inputEvent, config.listeners.volume, function() { - _setVolume(plyr.volume.input.value); + proxy(plyr.volume.input, inputEvent, config.listeners.volume, function() { + setVolume(plyr.volume.input.value); }); // Mute - _proxyListener(plyr.buttons.mute, 'click', config.listeners.mute, _toggleMute); + proxy(plyr.buttons.mute, 'click', config.listeners.mute, toggleMute); // Fullscreen - _proxyListener(plyr.buttons.fullscreen, 'click', config.listeners.fullscreen, _toggleFullscreen); + proxy(plyr.buttons.fullscreen, 'click', config.listeners.fullscreen, toggleFullscreen); + + // Looping + proxy(plyr.buttons.loop, 'click', config.listeners.loop, function(event) { + var value = event.target.getAttribute('data-loop__value') || event.target.getAttribute('data-loop__type'); + + if (inArray(['start', 'end', 'all', 'none'], value)) { + toggleLoop(value); + } + }); // Handle user exiting fullscreen by escaping etc - if (fullscreen.supportsFullScreen) { - _on(document, fullscreen.fullScreenEventName, _toggleFullscreen); + if (support.fullscreen) { + on(document, fullscreen.eventType, toggleFullscreen); } // Captions - _proxyListener(plyr.buttons.captions, 'click', config.listeners.captions, _toggleCaptions); + proxy(plyr.buttons.captions, 'click', config.listeners.captions, toggleCaptions); + + // Settings + on(plyr.buttons.settings, 'click', function(event) { + var menu = this, + toggle = event.target, + target = document.getElementById(toggle.getAttribute('aria-controls')), + show = (toggle.getAttribute('aria-expanded') === 'false'); + + // Nothing to show, bail + if (!is.htmlElement(target)) { + return; + } + + // Are we targetting a tab? + var isTab = target.getAttribute('role') === 'tabpanel', + targetWidth, + targetHeight, + container; + + // Hide all other tabs + if (isTab) { + // Get other tabs + var current = menu.querySelector('[role="tabpanel"][aria-hidden="false"]'); + container = current.parentNode; + + [].forEach.call(menu.querySelectorAll('[aria-controls="' + current.getAttribute('id') + '"]'), function(toggle) { + toggle.setAttribute('aria-expanded', false); + }); + + container.style.width = current.scrollWidth + 'px'; + container.style.height = current.scrollHeight + 'px'; + + current.setAttribute('aria-hidden', true); + current.setAttribute('tabindex', -1); + + // Get the natural element size + var clone = target.cloneNode(true); + clone.style.position = "absolute"; + clone.style.opacity = 0; + clone.setAttribute('aria-hidden', false); + container.appendChild(clone); + targetWidth = clone.scrollWidth; + targetHeight = clone.scrollHeight; + remove(clone); + } + + target.setAttribute('aria-hidden', !show); + toggle.setAttribute('aria-expanded', show); + target.setAttribute('tabindex', 0); + + if (isTab) { + container.style.width = targetWidth + 'px'; + container.style.height = targetHeight + 'px'; + + window.setTimeout(function() { + container.style.width = ''; + container.style.height = ''; + }, 300); + } + }); + + // Picture in picture + on(plyr.buttons.pip, 'click', function() { + // TODO: Check support here + plyr.media.webkitSetPresentationMode(plyr.media.webkitPresentationMode === 'picture-in-picture' ? 'inline' : 'picture-in-picture'); + }); // Seek tooltip - _on(plyr.progress.container, 'mouseenter mouseleave mousemove', _updateSeekTooltip); + on(plyr.progress.container, 'mouseenter mouseleave mousemove', updateSeekTooltip); // Toggle controls visibility based on mouse movement if (config.hideControls) { // Toggle controls on mouse events and entering fullscreen - _on(plyr.container, 'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove 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) { + on(plyr.controls, 'mouseenter mouseleave', function(event) { 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); + // 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); + on(plyr.controls, 'focus blur', toggleControls, true); } // Adjust volume on scroll - _on(plyr.volume.input, 'wheel', function(event) { + on(plyr.volume.input, 'wheel', function(event) { event.preventDefault(); // Detect "natural" scroll - suppored on OS X Safari only @@ -3102,45 +3854,45 @@ // Scroll down (or up on natural) to decrease if (event.deltaY < 0 || event.deltaX > 0) { if (inverted) { - _decreaseVolume(step); + decreaseVolume(step); } else { - _increaseVolume(step); + increaseVolume(step); } } // Scroll up (or down on natural) to increase if (event.deltaY > 0 || event.deltaX < 0) { if (inverted) { - _increaseVolume(step); + increaseVolume(step); } else { - _decreaseVolume(step); + decreaseVolume(step); } } }); } // Listen for media events - function _mediaListeners() { + function mediaListeners() { // Time change on media - _on(plyr.media, 'timeupdate seeking', _timeUpdate); + on(plyr.media, 'timeupdate seeking', timeUpdate); // Update manual captions - _on(plyr.media, 'timeupdate', _seekManualCaptions); + on(plyr.media, 'timeupdate', seekManualCaptions); // Display duration - _on(plyr.media, 'durationchange loadedmetadata', _displayDuration); + on(plyr.media, 'durationchange loadedmetadata', displayDuration); // Handle the media finishing - _on(plyr.media, 'ended', function() { + on(plyr.media, 'ended', function() { // Show poster on end if (plyr.type === 'video' && config.showPosterOnEnd) { // Clear if (plyr.type === 'video') { - _setCaption(); + setCaption(); } // Restart - _seek(); + seek(); // Re-load media plyr.media.load(); @@ -3148,21 +3900,21 @@ }); // Check for buffer progress - _on(plyr.media, 'progress playing', _updateProgress); + on(plyr.media, 'progress playing', updateProgress); // Handle native mute - _on(plyr.media, 'volumechange', _updateVolume); + on(plyr.media, 'volumechange', updateVolume); // Handle native play/pause - _on(plyr.media, 'play pause ended', _checkPlaying); + on(plyr.media, 'play pause ended', checkPlaying); // Loading - _on(plyr.media, 'waiting canplay seeked', _checkLoading); + on(plyr.media, 'waiting canplay seeked', checkLoading); // Click video if (config.clickToPlay && plyr.type !== 'audio') { // Re-fetch the wrapper - var wrapper = _getElement('.' + config.classes.videoWrapper); + var wrapper = getElement('.' + config.classes.videoWrapper); // Bail if there's no wrapper (this should never happen) if (!wrapper) { @@ -3173,52 +3925,54 @@ wrapper.style.cursor = "pointer"; // On click play, pause ore restart - _on(wrapper, 'click', function() { + 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(); + play(); } else if (plyr.media.ended) { - _seek(); - _play(); + seek(); + play(); } else { - _pause(); + pause(); } }); } // Disable right click if (config.disableContextMenu) { - _on(plyr.media, 'contextmenu', function(event) { event.preventDefault(); }); + on(plyr.media, 'contextmenu', function(event) { + event.preventDefault(); + }); } // Proxy events to container // Bubble up key events for Edge - _on(plyr.media, config.events.concat(['keyup', 'keydown']).join(' '), function(event) { - _triggerEvent(plyr.container, event.type, true); + on(plyr.media, config.events.concat(['keyup', 'keydown']).join(' '), function(event) { + trigger(plyr.container, event.type, true); }); } // Cancel current network requests // See https://github.com/Selz/plyr/issues/174 - function _cancelRequests() { - if (!_inArray(config.types.html5, plyr.type)) { + function cancelRequests() { + if (!inArray(config.types.html5, plyr.type)) { return; } // Remove child sources var sources = plyr.media.querySelectorAll('source'); for (var i = 0; i < sources.length; i++) { - _remove(sources[i]); + remove(sources[i]); } // Set blank video src attribute // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection - plyr.media.setAttribute('src', config.blankUrl); + plyr.media.setAttribute('src', 'https://cdn.selz.com/plyr/blank.mp4'); // Load the new empty source // This will cancel existing requests @@ -3226,13 +3980,13 @@ plyr.media.load(); // Debugging - _log('Cancelled network requests'); + log('Cancelled network requests'); } // Destroy an instance // Event listeners are removed when elements are removed // http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory - function _destroy(callback, restore) { + function destroy(callback, restore) { // Bail if the element is not initialized if (!plyr.init) { return null; @@ -3259,14 +4013,14 @@ plyr.embed.unload().then(cleanUp); // Vimeo does not always return - timers.cleanUp = window.setTimeout(cleanUp, 200); + window.setTimeout(cleanUp, 200); break; case 'video': case 'audio': // Restore native video controls - _toggleNativeControls(true); + toggleNativeControls(true); // Clean up cleanUp(); @@ -3275,15 +4029,13 @@ } function cleanUp() { - clearTimeout(timers.cleanUp); - // Default to restore original element - if (!_is.boolean(restore)) { + if (!is.boolean(restore)) { restore = true; } // Callback - if (_is.function(callback)) { + if (is.function(callback)) { callback.call(original); } @@ -3298,50 +4050,47 @@ // Replace the container with the original element provided plyr.container.parentNode.replaceChild(original, plyr.container); - // Allow overflow (set on fullscreen) + // unbind escape key document.body.style.overflow = ''; // Event - _triggerEvent(original, 'destroyed', true); + trigger(original, 'destroyed', true); } } // Setup a player - function _init() { + function init() { // Bail if the element is initialized if (plyr.init) { return null; } - // Setup the fullscreen api - fullscreen = _fullscreen(); - // Sniff out the browser - plyr.browser = _browserSniff(); + plyr.browser = getBrowser(); // Bail if nothing to setup - if (!_is.htmlElement(plyr.media)) { + if (!is.htmlElement(plyr.media)) { return; } // Load saved settings from localStorage - _setupStorage(); + setupStorage(); // Set media type based on tag or data attribute // Supported: video, audio, vimeo, youtube var tagName = media.tagName.toLowerCase(); if (tagName === 'div') { - plyr.type = media.getAttribute('data-type'); - plyr.embedId = media.getAttribute('data-video-id'); + plyr.type = media.getAttribute('data-type'); + plyr.embedId = media.getAttribute('data-video-id'); // Clean up media.removeAttribute('data-type'); media.removeAttribute('data-video-id'); } else { - plyr.type = tagName; - config.crossorigin = (media.getAttribute('crossorigin') !== null); - config.autoplay = (config.autoplay || (media.getAttribute('autoplay') !== null)); - config.loop = (config.loop || (media.getAttribute('loop') !== null)); + plyr.type = tagName; + config.crossorigin = (media.getAttribute('crossorigin') !== null); + config.autoplay = (config.autoplay || (media.getAttribute('autoplay') !== null)); + config.loop = (config.loop || (media.getAttribute('loop') !== null)); } // Check for support @@ -3353,31 +4102,31 @@ } // Wrap media - plyr.container = _wrap(media, document.createElement('div')); + plyr.container = wrap(media, document.createElement('div')); // Allow focus to be captured plyr.container.setAttribute('tabindex', 0); // Add style hook - _toggleStyleHook(); + toggleStyleHook(); // Debug info - _log('' + plyr.browser.name + ' ' + plyr.browser.version); + log('' + plyr.browser.name + ' ' + plyr.browser.version); // Setup media - _setupMedia(); + setupMedia(); // Setup interface // If embed but not fully supported, setupInterface (to avoid flash of controls) and call ready now - if (_inArray(config.types.html5, plyr.type) || (_inArray(config.types.embed, plyr.type) && !plyr.supported.full)) { + if (inArray(config.types.html5, plyr.type) || (inArray(config.types.embed, plyr.type) && !plyr.supported.full)) { // Setup UI - _setupInterface(); + setupInterface(); // Call ready - _ready(); + ready(); // Set title on button and frame - _setTitle(); + setTitle(); } // Successful setup @@ -3385,122 +4134,166 @@ } // Setup the UI - function _setupInterface() { + function setupInterface() { // Don't setup interface if no support if (!plyr.supported.full) { - _warn('Basic support only', plyr.type); + warn('Basic support only', plyr.type); // Remove controls - _remove(_getElement(config.selectors.controls.wrapper)); + remove(getElement(config.selectors.controls.wrapper)); // Remove large play - _remove(_getElement(config.selectors.buttons.play)); + remove(getElement(config.selectors.buttons.play)); // Restore native controls - _toggleNativeControls(true); + toggleNativeControls(true); // Bail return; } // Inject custom controls if not present - var controlsMissing = !_getElements(config.selectors.controls.wrapper).length; + var controlsMissing = !getElements(config.selectors.controls.wrapper).length; if (controlsMissing) { // Inject custom controls - _injectControls(); + injectControls(); } // Find the elements - if (!_findElements()) { + if (!findElements()) { return; } // If the controls are injected, re-bind listeners for controls if (controlsMissing) { - _controlListeners(); + controlListeners(); } // Media element listeners - _mediaListeners(); + mediaListeners(); // Remove native controls - _toggleNativeControls(); + toggleNativeControls(); // Setup fullscreen - _setupFullscreen(); + setupFullscreen(); // Captions - _setupCaptions(); + setupCaptions(); // Set volume - _setVolume(); - _updateVolume(); + setVolume(); + updateVolume(); + + // Set playback speed + setSpeed(); + + // Set loop + toggleLoop(); // Reset time display - _timeUpdate(); + timeUpdate(); // Update the UI - _checkPlaying(); + checkPlaying(); } api = { - getOriginal: function() { return original; }, - getContainer: function() { return plyr.container }, - getEmbed: function() { return plyr.embed; }, - getMedia: function() { return plyr.media; }, - getType: function() { return plyr.type; }, - getDuration: _getDuration, - getCurrentTime: function() { return plyr.media.currentTime; }, - getVolume: function() { return plyr.media.volume; }, - isMuted: function() { return plyr.media.muted; }, - isReady: function() { return _hasClass(plyr.container, config.classes.ready); }, - isLoading: function() { return _hasClass(plyr.container, config.classes.loading); }, - isPaused: function() { return plyr.media.paused; }, - on: function(event, callback) { _on(plyr.container, event, callback); return this; }, - play: _play, - pause: _pause, - stop: function() { _pause(); _seek(); }, - restart: _seek, - rewind: _rewind, - forward: _forward, - seek: _seek, - source: _source, - poster: _updatePoster, - setVolume: _setVolume, - togglePlay: _togglePlay, - toggleMute: _toggleMute, - toggleCaptions: _toggleCaptions, - toggleFullscreen: _toggleFullscreen, - toggleControls: _toggleControls, - isFullscreen: function() { return plyr.isFullscreen || false; }, - support: function(mimeType) { return _supportMime(plyr, mimeType); }, - destroy: _destroy + getOriginal: function() { + return original; + }, + getContainer: function() { + return plyr.container + }, + getEmbed: function() { + return plyr.embed; + }, + getMedia: function() { + return plyr.media; + }, + getType: function() { + return plyr.type; + }, + getDuration: getDuration, + getCurrentTime: function() { + return plyr.media.currentTime; + }, + getVolume: function() { + return plyr.media.volume; + }, + isMuted: function() { + return plyr.media.muted; + }, + isReady: function() { + return hasClass(plyr.container, config.classes.ready); + }, + isLoading: function() { + return hasClass(plyr.container, config.classes.loading); + }, + isPaused: function() { + return plyr.media.paused; + }, + isLooping: function() { + return config.loop.active; + }, + on: function(event, callback) { + on(plyr.container, event, callback); + return this; + }, + play: play, + pause: pause, + loop: toggleLoop, + stop: function() { + pause(); + seek(); + }, + restart: seek, + rewind: rewind, + forward: forward, + seek: seek, + source: source, + poster: updatePoster, + setVolume: setVolume, + setSpeed: setSpeed, + togglePlay: togglePlay, + toggleMute: toggleMute, + toggleCaptions: toggleCaptions, + toggleFullscreen: toggleFullscreen, + toggleControls: toggleControls, + setCaptionIndex: setCaptionIndex, + isFullscreen: function() { + return plyr.isFullscreen || false; + }, + support: function(mimeType) { + return support.mime(plyr, mimeType); + }, + destroy: destroy }; // Everything done - function _ready() { + function ready() { // Ready event at end of execution stack window.setTimeout(function() { - _triggerEvent(plyr.media, 'ready'); + trigger(plyr.media, 'ready'); }, 0); // Set class hook on media element - _toggleClass(plyr.media, defaults.classes.setup, true); + toggleClass(plyr.media, defaults.classes.setup, true); // Set container class for ready - _toggleClass(plyr.container, config.classes.ready, true); + toggleClass(plyr.container, config.classes.ready, true); // Store a refernce to instance plyr.media.plyr = api; // Autoplay if (config.autoplay) { - _play(); + play(); } } // Initialize instance - _init(); + init(); // If init failed, return null if (!plyr.init) { @@ -3515,14 +4308,14 @@ var x = new XMLHttpRequest(); // If the id is set and sprite exists, bail - if (_is.string(id) && _is.htmlElement(document.querySelector('#' + id))) { + if (is.string(id) && is.htmlElement(document.querySelector('#' + id))) { return; } // Create placeholder (to prevent loading twice) var container = document.createElement('div'); container.setAttribute('hidden', ''); - if (_is.string(id)) { + if (is.string(id)) { container.setAttribute('id', id); } document.body.insertBefore(container, document.body.childNodes[0]); @@ -3544,78 +4337,61 @@ // Check for support function supported(type) { - var browser = _browserSniff(), - isOldIE = (browser.isIE && browser.version <= 9), - isIos = browser.isIos, - isIphone = browser.isIphone, - audioSupport = !!document.createElement('audio').canPlayType, - videoSupport = !!document.createElement('video').canPlayType, - basic = false, - full = false; + var browser = getBrowser(), + 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 = videoSupport; - full = (basic && (!isOldIE && !isIphone)); + basic = video; + full = (basic && (!isOldIE && !isIphone)); break; case 'audio': - basic = audioSupport; - full = (basic && !isOldIE); + basic = audio; + full = (basic && !isOldIE); break; - // Vimeo does not seem to be supported on iOS via API - // Issue raised https://github.com/vimeo/player.js/issues/87 case 'vimeo': - basic = true; - full = (!isOldIE && !isIos); - break; - case 'youtube': - basic = true; - full = (!isOldIE && !isIos); - - // YouTube seems to work on iOS 10+ on iPad - if (isIos && !isIphone && browser.version >= 10) { - full = true; - } - - break; - case 'soundcloud': basic = true; - full = (!isOldIE && !isIphone); + full = (!isOldIE && !isIos); break; default: - basic = (audioSupport && videoSupport); - full = (basic && !isOldIE); + basic = (audio && video); + full = (basic && !isOldIE); } return { - basic: basic, - full: full + basic: basic, + full: full }; } // Setup function function setup(targets, options) { // Get the players - var players = [], - instances = [], - selector = [defaults.selectors.html5, defaults.selectors.embed].join(','); + var players = [], + instances = [], + selector = [defaults.selectors.html5, defaults.selectors.embed].join(','); // Select the elements - if (_is.string(targets)) { + if (is.string(targets)) { // String selector passed targets = document.querySelectorAll(targets); - } else if (_is.htmlElement(targets)) { + } else if (is.htmlElement(targets)) { // Single HTMLElement passed targets = [targets]; - } else if (!_is.nodeList(targets) && !_is.array(targets) && !_is.string(targets)) { + } else if (!is.nodeList(targets) && !is.array(targets) && !is.string(targets)) { // No selector passed, possibly options as first argument // If options are the first argument - if (_is.undefined(options) && _is.object(targets)) { + if (is.undefined(options) && is.object(targets)) { options = targets; } @@ -3624,7 +4400,7 @@ } // Convert NodeList to array - if (_is.nodeList(targets)) { + if (is.nodeList(targets)) { targets = Array.prototype.slice.call(targets); } @@ -3636,14 +4412,14 @@ // Add to container list function add(target, media) { - if (!_hasClass(media, defaults.classes.hook)) { + if (!hasClass(media, defaults.classes.hook)) { players.push({ // Always wrap in a <div> for styling - //container: _wrap(media, document.createElement('div')), + // container: wrap(media, document.createElement('div')), // Could be a container or the media itself - target: target, + target: target, // This should be the <video>, <audio> or <div> (YouTube/Vimeo) - media: media + media: media }); } } @@ -3660,7 +4436,7 @@ for (var x = 0; x < children.length; x++) { add(target, children[x]); } - } else if (_matches(target, selector)) { + } else if (matches(target, selector)) { // Target is media element add(target, target); } @@ -3668,9 +4444,9 @@ // Create a player instance for each element players.forEach(function(player) { - var element = player.target, - media = player.media, - match = false; + var element = player.target, + media = player.media, + match = false; // The target element can also be the media element if (media === element) { @@ -3682,10 +4458,11 @@ var data = {}; // Try parsing data attribute config - try { data = JSON.parse(element.getAttribute('data-plyr')); } - catch(e) { } + try { + data = JSON.parse(element.getAttribute('data-plyr')); + } catch (e) {} - var config = _extend({}, defaults, options, data); + var config = extend({}, defaults, options, data); // Bail if not enabled if (!config.enabled) { @@ -3696,7 +4473,7 @@ var instance = new Plyr(media, config); // Go to next if setup failed - if (!_is.object(instance)) { + if (!is.object(instance)) { return; } @@ -3704,13 +4481,13 @@ if (config.debug) { var events = config.events.concat(['setup', 'statechange', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled']); - _on(instance.getContainer(), events.join(' '), function(event) { + on(instance.getContainer(), events.join(' '), function(event) { console.log([config.logPrefix, 'event:', event.type].join(' '), event.detail.plyr); }); } // Callback - _event(instance.getContainer(), 'setup', true, { + event(instance.getContainer(), 'setup', true, { plyr: instance }); @@ -3723,21 +4500,21 @@ // Get all instances within a provided container function get(container) { - if (_is.string(container)) { + if (is.string(container)) { // Get selector if string passed container = document.querySelector(container); - } else if (_is.undefined(container)) { + } else if (is.undefined(container)) { // Use body by default to get all on page container = document.body; } // If we have a HTML element - if (_is.htmlElement(container)) { + if (is.htmlElement(container)) { var elements = container.querySelectorAll('.' + defaults.classes.setup), instances = []; Array.prototype.slice.call(elements).forEach(function(element) { - if (_is.object(element.plyr)) { + if (is.object(element.plyr)) { instances.push(element.plyr); } }); @@ -3749,22 +4526,26 @@ } return { - setup: setup, - supported: supported, + setup: setup, + supported: supported, loadSprite: loadSprite, - get: get + get: get }; })); // Custom event polyfill // https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent -(function () { +(function() { if (typeof window.CustomEvent === 'function') { return; } function CustomEvent(event, params) { - params = params || { bubbles: false, cancelable: false, detail: undefined }; + params = params || { + bubbles: false, + cancelable: false, + detail: undefined + }; var evt = document.createEvent('CustomEvent'); evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); return evt; diff --git a/src/less/plyr.less b/src/less/plyr.less index c9e3d7c3..f4a0a754 100644 --- a/src/less/plyr.less +++ b/src/less/plyr.less @@ -11,6 +11,16 @@ @keyframes plyr-progress { to { background-position: @plyr-progress-loading-size 0; } } +@keyframes plyr-popup { + from { + transform: translateY(10px); + opacity: .5; + } + to { + transform: translateY(0); + opacity: 1; + } +} // Styles // ------------------------------- @@ -20,6 +30,7 @@ max-width: 100%; min-width: 200px; font-family: @plyr-font-family; + font-weight: 500; direction: ltr; & when (@plyr-border-box = true) { @@ -40,6 +51,11 @@ } } + // ARIA + [aria-hidden='true'] { + display: none; + } + // Focus &:focus { outline: 0; @@ -173,6 +189,9 @@ position: relative; background: #000; border-radius: inherit; + // Require z-index to force border-radius + z-index: 0; + overflow: hidden; } // Container for embeds @@ -180,11 +199,11 @@ padding-bottom: 56.25%; /* 16:9 */ height: 0; border-radius: inherit; - // Require overflow and z-index to force border-radius overflow: hidden; z-index: 0; + iframe { position: absolute; top: 0; @@ -225,7 +244,6 @@ color: @plyr-captions-color; font-size: @plyr-font-size-captions-base; text-align: center; - font-weight: 400; span { border-radius: 2px; @@ -270,50 +288,64 @@ text-align: center; // Spacing - > button, + > .plyr__control, .plyr__progress, - .plyr__time { + .plyr__time, + .plyr__menu { margin-left: (@plyr-control-spacing / 2); - &:first-child { + &:first-child, + &:first-child + [data-plyr="pause"] { margin-left: 0; } } .plyr__volume { margin-left: (@plyr-control-spacing / 2); } - [data-plyr="pause"] { - margin-left: 0; - } - // Buttons - button { - position: relative; - display: inline-block; - flex-shrink: 0; - overflow: visible; // IE11 - vertical-align: middle; - padding: @plyr-control-padding; - border: 0; - background: transparent; - border-radius: 3px; - cursor: pointer; - transition: background .3s ease, color .3s ease, opacity .3s ease; - color: inherit; - - svg { - width: @plyr-control-icon-size; - height: @plyr-control-icon-size; - display: block; - fill: currentColor; + @media (min-width: @plyr-bp-screen-sm) { + > .plyr__control, + .plyr__progress, + .plyr__time, + .plyr__menu { + margin-left: @plyr-control-spacing; } - // Default focus - &:focus { - outline: 0; + > .plyr__control + .plyr__control, + .plyr__menu + .plyr__control, + > .plyr__control + .plyr__menu { + margin-left: (@plyr-control-spacing / 2); } } +} +// Hide controls +.plyr--hide-controls .plyr__controls { + opacity: 0; + pointer-events: none; +} + +// Buttons +.plyr__control { + position: relative; + display: inline-block; + flex-shrink: 0; + overflow: visible; // IE11 + vertical-align: middle; + padding: @plyr-control-padding; + border: 0; + background: transparent; + border-radius: 3px; + cursor: pointer; + transition: background .3s ease, color .3s ease, opacity .3s ease; + color: inherit; + svg { + width: @plyr-control-icon-size; + height: @plyr-control-icon-size; + display: block; + fill: currentColor; + pointer-events: none; + } // Hide toggle icons by default .icon--exit-fullscreen, .icon--muted, @@ -321,19 +353,11 @@ display: none; } - @media (min-width: @plyr-bp-screen-sm) { - > button, - .plyr__progress, - .plyr__time { - margin-left: @plyr-control-spacing; - } + // Default focus + &:focus { + outline: 0; } } -// Hide controls -.plyr--hide-controls .plyr__controls { - opacity: 0; - pointer-events: none; -} // Video controls .plyr--video .plyr__controls { @@ -343,16 +367,17 @@ bottom: 0; z-index: 2; padding: (@plyr-control-spacing * 5) @plyr-control-spacing @plyr-control-spacing; - background: linear-gradient(fade(@plyr-video-controls-bg, 0%), fade(@plyr-video-controls-bg, 50%)); + background: linear-gradient(fade(@plyr-video-controls-bg, 0%), fade(@plyr-video-controls-bg, 70%)); border-bottom-left-radius: inherit; border-bottom-right-radius: inherit; color: @plyr-video-control-color; transition: opacity .3s ease; - button { + .plyr__control { // Hover and tab focus &.tab-focus:focus, - &:hover { + &:hover, + &[aria-expanded='true'] { background: @plyr-video-control-bg-hover; color: @plyr-video-control-color-hover; } @@ -367,10 +392,11 @@ border: @plyr-audio-controls-border; color: @plyr-audio-control-color; - button { + .plyr__control { // Hover and tab focus &.tab-focus:focus, - &:hover { + &:hover, + &[aria-expanded='true'] { background: @plyr-audio-control-bg-hover; color: @plyr-audio-control-color-hover; } @@ -400,6 +426,7 @@ height: 20px; display: block; fill: currentColor; + pointer-events: none; } &:focus { @@ -427,9 +454,9 @@ } // Change icons on state change -.plyr--fullscreen-active .icon--exit-fullscreen, -.plyr--muted .plyr__controls .icon--muted, -.plyr--captions-active .plyr__controls .icon--captions-on { +.plyr--fullscreen-active .plyr__control .icon--exit-fullscreen, +.plyr--muted .plyr__control .icon--muted, +.plyr--captions-active .plyr__control .icon--captions-on { display: block; & + svg { @@ -447,6 +474,160 @@ display: inline-block; } +// Menus +// -------------------------------------------------------------- +.plyr__menu { + position: relative; + + // Hide tooltip + .plyr__control svg { + transition: transform .3s ease; + } + .plyr__control[aria-expanded='true'] { + svg { + transform: rotate(45deg); + } + .plyr__tooltip { + display: none; + } + } + + // The actual menu container + &__container { + position: absolute; + bottom: 100%; + right: -5px; + margin-bottom: 10px; + animation: plyr-popup .2s ease; + + background: @plyr-menu-bg; + box-shadow: 0 1px 0 fade(#000, 20%); + border-radius: 4px; + + white-space: nowrap; + text-align: left; + color: @plyr-menu-color; + font-size: @plyr-font-size-small; + + > div { + overflow: hidden; + transition: height .35s cubic-bezier(.4,0,.2,1), width .35s cubic-bezier(.4,0,.2,1); + } + + // Arrow + &::after { + content: ""; + position: absolute; + top: 100%; + right: 15px; + height: 0; + width: 0; + border: 6px solid transparent; + border-top-color: @plyr-menu-bg; + } + + ul { + margin: 0; + padding: 5px; + list-style: none; + overflow: hidden; + } + + // Options + .plyr__control { + display: flex; + width: 100%; + padding: @plyr-control-padding (@plyr-control-padding * 2); + color: @plyr-menu-color; + font-weight: 600; + user-select: none; + + &::after { + content: ""; + position: absolute; + top: 50%; + transform: translateY(-50%); + border: 5px solid transparent; + } + + &--forward { + padding-right: ceil(@plyr-control-padding * 4); + + &::after { + right: 5px; + border-left-color: fade(@plyr-menu-color, 80%); + } + } + + &--back { + position: relative; + padding-top: floor(@plyr-control-padding * .7); + padding-bottom: floor(@plyr-control-padding * .7); + padding-left: ceil(@plyr-control-padding * 4); + margin-bottom: ceil(@plyr-control-padding * 1.3); + font-weight: 500; + + &::after { + left: 5px; + border-right-color: fade(@plyr-menu-color, 80%); + } + &::before { + content: ""; + position: absolute; + top: 100%; + left: 0; + right: 0; + height: 1px; + overflow: hidden; + margin-top: ceil(@plyr-control-padding / 2); + background: fade(#000, 15%); + box-shadow: 0 1px 0 fade(#fff, 10%); + } + } + } + + label.plyr__control { + padding-left: ceil(@plyr-control-padding * 2.5); + + input[type="radio"] { + position: relative; + left: -@plyr-control-padding; + } + } + + // Option value + .plyr__menu__value { + display: inherit; + margin-left: auto; + padding-left: ceil(@plyr-control-padding * 3.5); + pointer-events: none; + overflow: hidden; + font-weight: 500; + color: fade(@plyr-menu-color, 80%); + + .plyr__badge { + font-weight: 600; + } + } + + // When animating between menus + &.is-resizing { + overflow: hidden; + transition: height .35s cubic-bezier(.4,0,.2,1), width .35s cubic-bezier(.4,0,.2,1); + } + } +} + +// Badge +// -------------------------------------------------------------- +.plyr__badge { + padding: 2px 4px; + border-radius: 2px; + background: @plyr-menu-color; + color: @plyr-menu-bg; + font-size: 10px; +} + // Tooltips // -------------------------------------------------------------- .plyr__tooltip { @@ -463,6 +644,7 @@ color: @plyr-tooltip-color; font-size: @plyr-font-size-small; + font-weight: 500; line-height: 1.3; transform: translate(-50%, 10px) scale(.8); @@ -486,18 +668,19 @@ z-index: 2; } } -.plyr button:hover .plyr__tooltip, -.plyr button.tab-focus:focus .plyr__tooltip, +.plyr .plyr__control:hover .plyr__tooltip, +.plyr .plyr__control.tab-focus:focus .plyr__tooltip, .plyr__tooltip--visible { opacity: 1; transform: translate(-50%, 0) scale(1); } -.plyr button:hover .plyr__tooltip { +.plyr .plyr__control:hover .plyr__tooltip { z-index: 3; } // First tooltip -.plyr__controls button:first-child .plyr__tooltip { +.plyr__controls > .plyr__control:first-child .plyr__tooltip, +.plyr__controls > .plyr__control:first-child + .plyr__control .plyr__tooltip { left: 0; transform: translate(0, 10px) scale(.8); transform-origin: 0 100%; @@ -508,7 +691,7 @@ } // Last tooltip -.plyr__controls button:last-child .plyr__tooltip { +.plyr__controls > .plyr__control:last-child .plyr__tooltip { right: 0; transform: translate(0, 10px) scale(.8); transform-origin: 100% 100%; @@ -520,8 +703,9 @@ } } -.plyr__controls button:first-child, -.plyr__controls button:last-child { +.plyr__controls > .plyr__control:first-child, +.plyr__controls > .plyr__control:first-child + .plyr__control, +.plyr__controls > .plyr__control:last-child { &:hover .plyr__tooltip, &.tab-focus:focus .plyr__tooltip, .plyr__tooltip--visible { diff --git a/src/less/variables.less b/src/less/variables.less index 807d0854..5abe3464 100644 --- a/src/less/variables.less +++ b/src/less/variables.less @@ -17,7 +17,7 @@ @plyr-font-size-base: 16px; // Captions -@plyr-captions-bg: fade(#000, 70%); +@plyr-captions-bg: fade(#000, 85%); @plyr-captions-color: #fff; @plyr-font-size-captions-base: @plyr-font-size-base; @plyr-font-size-captions-medium: ceil(@plyr-font-size-base * 1.5); @@ -38,15 +38,20 @@ @plyr-audio-control-bg-hover: @plyr-color-main; // Tooltips -@plyr-tooltip-bg: fade(#000, 70%); +@plyr-tooltip-bg: fade(#343f4a, 90%); @plyr-tooltip-color: #fff; @plyr-tooltip-padding: (@plyr-control-spacing / 2); @plyr-tooltip-arrow-size: 4px; @plyr-tooltip-radius: 3px; +// Menus +@plyr-menu-bg: @plyr-tooltip-bg; +@plyr-menu-color: @plyr-tooltip-color; +@plyr-menu-arrow-size: 6px; + // Progress @plyr-progress-loading-size: 25px; -@plyr-progress-loading-bg: fade(#000, 15%); +@plyr-progress-loading-bg: fade(#343f4a, 20%); @plyr-video-progress-bg: fade(#fff, 25%); @plyr-video-progress-buffered-bg: @plyr-video-progress-bg; @plyr-audio-progress-bg: fade(#C6D6DB, 66%); @@ -58,7 +63,7 @@ @plyr-range-thumb-width: floor(@plyr-range-track-height * 2); @plyr-range-thumb-bg: #fff; @plyr-range-thumb-border: 2px solid transparent; -@plyr-range-thumb-shadow: 0 1px 1px fade(@plyr-video-controls-bg, 15%), 0 0 0 1px fade(#000, 15%); +@plyr-range-thumb-shadow: 0 1px 1px fade(@plyr-video-controls-bg, 15%), 0 0 0 1px fade(#343f4a, 20%); @plyr-range-thumb-active-border-color: #fff; @plyr-range-thumb-active-bg: @plyr-video-control-bg-hover; @plyr-range-thumb-active-scale: 1.25; diff --git a/src/scss/plyr.scss b/src/scss/plyr.scss index 91a5d1b2..e75a2eda 100644 --- a/src/scss/plyr.scss +++ b/src/scss/plyr.scss @@ -172,6 +172,9 @@ position: relative; background: #000; border-radius: inherit; + // Require z-index to force border-radius + z-index: 0; + overflow: hidden; } // Container for embeds @@ -269,21 +272,20 @@ text-align: center; // Spacing - > button, + > .plyr__control, .plyr__progress, - .plyr__time { + .plyr__time, + .plyr__menu { margin-left: ($plyr-control-spacing / 2); - &:first-child { + &:first-child, + &:first-child + [data-plyr="pause"] { margin-left: 0; } } .plyr__volume { margin-left: ($plyr-control-spacing / 2); } - [data-plyr="pause"] { - margin-left: 0; - } // Buttons button { @@ -305,6 +307,7 @@ height: $plyr-control-icon-size; display: block; fill: currentColor; + pointer-events: none; } // Default focus @@ -398,6 +401,7 @@ height: 20px; display: block; fill: currentColor; + pointer-events: none; } &:focus { @@ -495,7 +499,8 @@ } // First tooltip -.plyr__controls button:first-child .plyr__tooltip { +.plyr__controls > button:first-child .plyr__tooltip, +.plyr__controls > button:first-child + button .plyr__tooltip { left: 0; transform: translate(0, 10px) scale(.8); transform-origin: 0 100%; @@ -506,7 +511,7 @@ } // Last tooltip -.plyr__controls button:last-child .plyr__tooltip { +.plyr__controls > button:last-child .plyr__tooltip { right: 0; transform: translate(0, 10px) scale(.8); transform-origin: 100% 100%; @@ -518,8 +523,9 @@ } } -.plyr__controls button:first-child, -.plyr__controls button:last-child { +.plyr__controls > button:first-child, +.plyr__controls > button:first-child + button, +.plyr__controls > button:last-child { &:hover .plyr__tooltip, &.tab-focus:focus .plyr__tooltip, .plyr__tooltip--visible { diff --git a/src/sprite/plyr-airplay.svg b/src/sprite/plyr-airplay.svg new file mode 100644 index 00000000..45c55414 --- /dev/null +++ b/src/sprite/plyr-airplay.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <g> + <path d="M16,1 L2,1 C1.447,1 1,1.447 1,2 L1,12 C1,12.553 1.447,13 2,13 L5,13 L5,11 L3,11 L3,3 L15,3 L15,11 L13,11 L13,13 L16,13 C16.553,13 17,12.553 17,12 L17,2 C17,1.447 16.553,1 16,1 L16,1 Z"></path> + <polygon points="4 17 14 17 9 11"></polygon> + </g> +</svg>
\ No newline at end of file diff --git a/src/sprite/plyr-pip.svg b/src/sprite/plyr-pip.svg new file mode 100644 index 00000000..d841fce5 --- /dev/null +++ b/src/sprite/plyr-pip.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <g> + <polygon points="13.293 3.293 7.022 9.564 8.436 10.978 14.707 4.707 17 7 17 1 11 1"></polygon> + <path d="M13,15 L3,15 L3,5 L8,5 L8,3 L2,3 C1.448,3 1,3.448 1,4 L1,16 C1,16.552 1.448,17 2,17 L14,17 C14.552,17 15,16.552 15,16 L15,10 L13,10 L13,15 L13,15 Z"></path> + </g> +</svg>
\ No newline at end of file diff --git a/src/sprite/plyr-settings.svg b/src/sprite/plyr-settings.svg new file mode 100644 index 00000000..fbf8ecd1 --- /dev/null +++ b/src/sprite/plyr-settings.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <g> + <path d="M16.135,7.784 C14.832,7.458 14.214,5.966 14.905,4.815 C15.227,4.279 15.13,3.817 14.811,3.499 L14.501,3.189 C14.183,2.871 13.721,2.774 13.185,3.095 C12.033,3.786 10.541,3.168 10.216,1.865 C10.065,1.258 9.669,1 9.219,1 L8.781,1 C8.331,1 7.936,1.258 7.784,1.865 C7.458,3.168 5.966,3.786 4.815,3.095 C4.279,2.773 3.816,2.87 3.498,3.188 L3.188,3.498 C2.87,3.816 2.773,4.279 3.095,4.815 C3.786,5.967 3.168,7.459 1.865,7.784 C1.26,7.935 1,8.33 1,8.781 L1,9.219 C1,9.669 1.258,10.064 1.865,10.216 C3.168,10.542 3.786,12.034 3.095,13.185 C2.773,13.721 2.87,14.183 3.189,14.501 L3.499,14.811 C3.818,15.13 4.281,15.226 4.815,14.905 C5.967,14.214 7.459,14.832 7.784,16.135 C7.935,16.742 8.331,17 8.781,17 L9.219,17 C9.669,17 10.064,16.742 10.216,16.135 C10.542,14.832 12.034,14.214 13.185,14.905 C13.72,15.226 14.182,15.13 14.501,14.811 L14.811,14.501 C15.129,14.183 15.226,13.72 14.905,13.185 C14.214,12.033 14.832,10.541 16.135,10.216 C16.742,10.065 17,9.669 17,9.219 L17,8.781 C17,8.33 16.74,7.935 16.135,7.784 L16.135,7.784 Z M9,12 C7.343,12 6,10.657 6,9 C6,7.343 7.343,6 9,6 C10.657,6 12,7.343 12,9 C12,10.657 10.657,12 9,12 L9,12 Z"></path> + </g> +</svg>
\ No newline at end of file |