aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSam Potts <sam@selz.com>2017-04-25 12:37:35 +1000
committerGitHub <noreply@github.com>2017-04-25 12:37:35 +1000
commit0a58cbfd59de14825ef894a1943651a66e8639a6 (patch)
tree238bc4769474f4c9329dd9136fed1ab0050d9473 /src
parent2ecca2cbe3e67aa84c7b10cf1d876f2ba03eac12 (diff)
parentbbe4b7e565f6fd8acc946065934bdeae8d0dfc0e (diff)
downloadplyr-0a58cbfd59de14825ef894a1943651a66e8639a6.tar.lz
plyr-0a58cbfd59de14825ef894a1943651a66e8639a6.tar.xz
plyr-0a58cbfd59de14825ef894a1943651a66e8639a6.zip
Merge branch 'develop' into master
Diffstat (limited to 'src')
-rw-r--r--src/js/plyr.js4383
-rw-r--r--src/less/plyr.less307
-rw-r--r--src/less/variables.less13
-rw-r--r--src/scss/plyr.scss26
-rw-r--r--src/scss/variables.scss2
-rw-r--r--src/sprite/plyr-airplay.svg7
-rw-r--r--src/sprite/plyr-pip.svg7
-rw-r--r--src/sprite/plyr-settings.svg6
8 files changed, 3141 insertions, 1610 deletions
diff --git a/src/js/plyr.js b/src/js/plyr.js
index 7d57a10c..92c5175d 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,299 @@
'use strict';
// Globals
- var fullscreen,
- scroll = { x: 0, y: 0 },
+ var 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
+ var defaults = {
+ enabled: true,
+ debug: false,
+ autoplay: false,
+ loop: {
+ active: false,
+ start: 0,
+ end: null,
+ indicator: {
+ start: 0,
+ end: 0
+ }
+ },
+ seekTime: 10,
+ volume: 10,
+ 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,
+ quality: {
+ options: false
+ },
+ keyboardShortcuts: {
+ focused: true,
+ global: false
},
tooltips: {
- controls: false,
- seek: true
+ controls: false,
+ seek: true
},
+ tracks: [],
selectors: {
- html5: 'video, audio',
- embed: '[data-type]',
- editable: 'input, textarea, select, [contenteditable]',
- container: '.plyr',
+ html5: 'video, audio',
+ embed: '[data-type]',
+ editable: 'input, textarea, select, [contenteditable]',
+ container: '.plyr',
controls: {
- container: null,
- wrapper: '.plyr__controls'
+ container: null,
+ wrapper: '.plyr__controls'
},
- labels: '[data-plyr]',
+ 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"]'
+ 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"]',
+ pip: '[data-plyr="pip"]',
+ airplay: '[data-plyr="airplay"]',
+ settings: '[data-plyr="settings"]',
+ speed: '[data-plyr="speed"]',
+ loop: '[data-plyr="loop"]',
+ language: '[data-plyr="language"]',
+ quality: '[data-plyr="quality"]'
},
- volume: {
- input: '[data-plyr="volume"]',
- display: '.plyr__volume--display'
+ inputs: {
+ seek: '[data-plyr="seek"]',
+ volume: '[data-plyr="volume"]'
},
- progress: {
- container: '.plyr__progress',
- buffer: '.plyr__progress--buffer',
- played: '.plyr__progress--played'
+ display: {
+ currentTime: '.plyr__time--current',
+ duration: '.plyr__time--duration',
+ buffer: '.plyr__progress--buffer',
+ played: '.plyr__progress--played',
+ loop: '.plyr__progress--loop',
+ volume: '.plyr__volume--display',
},
- captions: '.plyr__captions',
- currentTime: '.plyr__time--current',
- duration: '.plyr__time--duration'
+ progress: '.plyr__progress',
+ captions: '.plyr__captions',
+ 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',
+ setup: 'plyr--setup',
+ ready: 'plyr--ready',
+ videoWrapper: 'plyr__video-wrapper',
+ embedWrapper: 'plyr__video-embed',
+ control: 'plyr__control',
+ 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',
+ menu: {
+ value: 'plyr__menu__value',
+ badge: 'plyr__badge'
+ },
captions: {
- enabled: 'plyr--captions-enabled',
- active: 'plyr--captions-active'
+ enabled: 'plyr--captions-enabled',
+ active: 'plyr--captions-active'
},
fullscreen: {
- enabled: 'plyr--fullscreen-enabled',
- active: 'plyr--fullscreen-active'
+ enabled: 'plyr--fullscreen-enabled',
+ active: 'plyr--fullscreen-active'
+ },
+ pip: {
+ enabled: 'plyr--pip-enabled',
+ active: 'plyr--pip-active'
+ },
+ airplay: {
+ enabled: 'plyr--airplay-enabled',
+ active: 'plyr--airplay-active'
},
- tabFocus: 'tab-focus'
+ tabFocus: 'tab-focus'
},
captions: {
- defaultActive: false
+ defaultActive: false,
+ selectedIndex: 0
},
fullscreen: {
- enabled: true,
- fallback: true,
- allowAudio: false
+ enabled: true,
+ fallback: true,
+ allowAudio: false
},
storage: {
- enabled: true,
- key: 'plyr'
+ enabled: true,
+ key: 'plyr'
},
- controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'fullscreen'],
+ 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}'
+ restart: 'Restart',
+ rewind: 'Rewind {seektime} secs',
+ play: 'Play',
+ pause: 'Pause',
+ forward: 'Forward {seektime} secs',
+ seek: 'Seek',
+ 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',
+ start: 'Start',
+ end: 'End',
+ all: 'All',
+ reset: 'Reset',
},
types: {
- embed: ['youtube', 'vimeo', 'soundcloud'],
- html5: ['video', 'audio']
+ embed: ['youtube', 'vimeo', 'soundcloud'],
+ html5: ['video', 'audio']
},
// URLs
urls: {
vimeo: {
- api: 'https://player.vimeo.com/api/player.js',
+ api: 'https://player.vimeo.com/api/player.js',
},
youtube: {
- api: 'https://www.youtube.com/iframe_api'
+ api: 'https://www.youtube.com/iframe_api'
},
soundcloud: {
- api: 'https://w.soundcloud.com/player/api.js'
+ 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
+ seek: null,
+ play: null,
+ pause: null,
+ restart: null,
+ rewind: null,
+ forward: null,
+ mute: null,
+ volume: null,
+ captions: null,
+ fullscreen: null,
+ pip: null,
+ airplay: null,
+ speed: null,
+ quality: null,
+ loop: null,
+ language: 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'],
+ events: [
+ 'ready',
+ 'ended',
+ 'progress',
+ 'stalled',
+ 'playing',
+ 'waiting',
+ 'canplay',
+ 'canplaythrough',
+ 'loadstart',
+ 'loadeddata',
+ 'loadedmetadata',
+ 'timeupdate',
+ 'volumechange',
+ 'play',
+ 'pause',
+ 'error',
+ 'seeking',
+ 'seeked',
+ 'emptied'
+ ],
// Logging
- logPrefix: '[Plyr]'
+ logPrefix: ''
+ };
+
+ // Check variable types
+ var is = {
+ object: function(input) {
+ return input !== null && typeof(input) === 'object' && input.constructor === Object;
+ },
+ array: function(input) {
+ 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));
+ },
+ string: function(input) {
+ return input !== null && (typeof input === 'string' || (typeof input === 'object' && input.constructor === String));
+ },
+ boolean: function(input) {
+ return input !== null && typeof input === 'boolean';
+ },
+ nodeList: function(input) {
+ return input !== null && input instanceof NodeList;
+ },
+ htmlElement: function(input) {
+ return input !== null && input instanceof HTMLElement;
+ },
+ function: function(input) {
+ return input !== null && typeof input === 'function';
+ },
+ event: function(input) {
+ return input !== null && input instanceof Event;
+ },
+ 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);
+ }
};
// 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 +341,7 @@
isSafari = true;
name = 'Safari';
fullVersion = ua.substring(verOffset + 7);
+
if ((verOffset = ua.indexOf('Version')) !== -1) {
fullVersion = ua.substring(verOffset + 8);
}
@@ -226,7 +352,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 +377,51 @@
// 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);
}
+ // Determine if we're in an iframe
+ function inFrame() {
+ try {
+ return window.self !== window.top;
+ } catch (e) {
+ return true;
+ }
+ }
+
// 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 +430,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 +454,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 +463,119 @@
}
// 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, attributes[key]);
+ }
+ }
+
+ // Get an attribute object from a string selector
+ function getAttributesFromSelector(selector, existingAttributes) {
+ // For example:
+ // '.test' to { class: 'test' }
+ // '#test' to { id: 'test' }
+ // '[data-test="test"]' to { 'data-test': 'test' }
+
+ if (!is.string(selector) || is.empty(selector)) {
+ return {};
}
+
+ var attributes = {};
+
+ selector.split(',').forEach(function(selector) {
+ // Remove whitespace
+ selector = selector.trim();
+
+ // Get the first character
+ var start = selector.charAt(0);
+
+ switch (start) {
+ case '.':
+ // Classname selector
+ var className = selector.replace('.', '');
+
+ // Add to existing classname
+ if (is.object(existingAttributes) && is.string(existingAttributes.class)) {
+ existingAttributes.class += ' ' + className;
+ }
+
+ attributes.class = className;
+ break;
+
+ case '#':
+ // ID selector
+ attributes.id = selector.replace('#', '');
+ break;
+
+ case '[':
+ // Strip the []
+ selector = selector.replace(/[\[\]]/g, '');
+
+ // Get the parts if
+ var parts = selector.split('=');
+ var key = parts[0];
+
+ // Get the value if provided
+ var value = parts.length > 1 ? parts[1].replace(/[\"\']/g, '') : '';
+
+ // Attribute selector
+ attributes[key] = value;
+
+ break;
+ }
+ });
+
+ return attributes;
}
- // Insert a HTML element
- function _insertElement(type, parent, attributes) {
+ // Create a DocumentFragment
+ function createElement(type, attributes, text) {
// Create a new <element>
var element = document.createElement(type);
// Set all passed attributes
- _setAttributes(element, attributes);
+ if (is.object(attributes)) {
+ setAttributes(element, attributes);
+ }
+
+ // Add text node
+ if (is.string(text)) {
+ element.textContent = text;
+ }
+
+ // Return built element
+ return element;
+ }
+
+ // Insert a DocumentFragment
+ function insertElement(type, parent, attributes, text) {
+ // Create a new <element>
+ var element = createElement(type, attributes, text);
// Inject the new element
- _prependChild(parent, element);
+ prependChild(parent, element);
+ }
+
+ // Remove all child elements
+ function emptyElement(element) {
+ var length = element.childNodes.length;
+ while (length--) {
+ element.removeChild(element.lastChild);
+ }
}
// 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 +587,7 @@
}
// Has class name
- function _hasClass(element, className) {
+ function hasClass(element, className) {
if (element) {
if (element.classList) {
return element.classList.contains(className);
@@ -422,41 +599,58 @@
}
// Element matches selector
- function _matches(element, selector) {
- var p = Element.prototype;
+ 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;
+ };
- var f = p.matches || p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector || function(s) {
- return [].indexOf.call(document.querySelectorAll(s), this) !== -1;
- };
+ return matches.call(element, selector);
+ }
+
+ // Get the focused element
+ function getFocusElement() {
+ var focused = document.activeElement;
- return f.call(element, selector);
+ if (!focused || focused === document.body) {
+ focused = null;
+ } else {
+ focused = document.querySelector(':focus');
+ }
+
+ return focused;
}
// Bind along with custom handler
- function _proxyListener(element, eventName, userListener, defaultListener, useCapture) {
- _on(element, eventName, function(event) {
- if (userListener) {
- userListener.apply(element, [event]);
+ function proxy(element, eventName, customListener, defaultListener, useCapture) {
+ on(element, eventName, function(event) {
+ if (customListener) {
+ customListener.apply(element, [event]);
}
defaultListener.apply(element, [event]);
}, useCapture);
}
// 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 +658,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 +700,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 +716,7 @@
}
// Get percentage
- function _getPercentage(current, max) {
+ function getPercentage(current, max) {
if (current === 0 || max === 0 || isNaN(current) || isNaN(max)) {
return 0;
}
@@ -532,7 +726,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;
@@ -547,17 +741,25 @@
}
// First object is the destination
- var destination = Array.prototype.shift.call(objects),
- length = objects.length;
+ var destination = Array.prototype.shift.call(objects);
+ if (!is.object(destination)) {
+ destination = {};
+ }
+
+ var length = objects.length;
// Loop through all objects to merge
for (var i = 0; i < length; i++) {
var source = objects[i];
+ if (!is.object(source)) {
+ source = {};
+ }
+
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];
}
@@ -567,84 +769,49 @@
return destination;
}
- // Check variable types
- var _is = {
- object: function(input) {
- return input !== null && typeof(input) === 'object';
- },
- array: function(input) {
- return input !== null && (typeof(input) === 'object' && input.constructor === Array);
- },
- number: function(input) {
- return input !== null && (typeof(input) === 'number' && !isNaN(input - 0) || (typeof input === 'object' && input.constructor === Number));
- },
- string: function(input) {
- return input !== null && (typeof input === 'string' || (typeof input === 'object' && input.constructor === String));
- },
- boolean: function(input) {
- return input !== null && typeof input === 'boolean';
- },
- nodeList: function(input) {
- return input !== null && input instanceof NodeList;
- },
- htmlElement: function(input) {
- return input !== null && input instanceof HTMLElement;
- },
- function: function(input) {
- return input !== null && typeof input === 'function';
- },
- undefined: function(input) {
- return input !== null && typeof input === 'undefined';
- }
- };
-
// 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;
}
// 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(' ');
+ var fullscreen = (function() {
+ // Determine the prefix
+ var prefix = (function() {
+ var value = false;
- // 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;
- }
+ 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;
+ })();
+
+ return {
+ 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) {
@@ -653,29 +820,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;
}
@@ -694,334 +875,1087 @@
// 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(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.leanbackplayer.com/test/h5mt.html
+ mime: function(player, type) {
+ var media = player.media;
+
+ try {
+ // Bail if no checking function
+ if (!is.function(media.canPlayType)) {
+ return false;
+ }
+
+ // Type specific checks
+ if (player.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 (player.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 player = this;
+ var timers = {};
+ var api;
+
+ player.fullscreen = {
+ active: false
+ };
+
+ // Elements cache
+ player.elements = {
+ buttons: {},
+ display: {},
+ progress: {},
+ inputs: {},
+ settings: {
+ menu: null,
+ panes: {},
+ tabs: {}
+ },
+ media: media
+ };
+
+ // Captions
+ player.captions = {
+ enabled: false,
+ textTracks: false,
+ captions: []
+ };
// 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, {
- 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);
+ window.console[type].apply(window.console, args);
+ }
+ }
+ var log = function() {
+ logger('log', arguments);
+ };
+ var warn = function() {
+ logger('warn', arguments);
+ };
+ // Log config options and support
+ log('Config', config);
+ log('Support', support);
+
+ // Trigger events, with plyr instance passed
+ function trigger(element, type, bubbles, properties) {
+ event(element, type, bubbles, extend({}, properties, {
+ plyr: api
+ }));
+ }
+
+ // Find all elements
+ function getElements(selector) {
+ return player.elements.container.querySelectorAll(selector);
+ }
+
+ // Find a single element
+ function getElement(selector) {
+ return getElements(selector)[0];
+ }
+
+ // Trap focus inside container
+ function focusTrap() {
+ var tabbables = getElements('input:not([disabled]), button:not([disabled])');
+ var first = tabbables[0];
+ var last = tabbables[tabbables.length - 1];
+
+ function checkFocus(event) {
+ // If it is TAB
+ if (event.which === 9 && player.fullscreen.active) {
+ if (event.target === last && !event.shiftKey) {
+ // Move focus to first element that can be tabbed if Shift isn't used
+ event.preventDefault();
+ first.focus();
+ } else if (event.target === first && event.shiftKey) {
+ // Move focus to last element that can be tabbed if Shift is used
+ event.preventDefault();
+ last.focus();
+ }
+ }
}
+
+ // Bind the handler
+ on(player.elements.container, 'keydown', checkFocus);
}
- var _log = function() { _console('log', arguments) },
- _warn = function() { _console('warn', arguments) };
- // Log config options
- _log('Config', config);
+ // Add elements to HTML5 media (source, tracks, etc)
+ function insertElements(type, attributes) {
+ if (is.string(attributes)) {
+ insertElement(type, player.elements.media, {
+ src: attributes
+ });
+ } else if (is.array(attributes)) {
+ attributes.forEach(function(attribute) {
+ insertElement(type, player.elements.media, attribute);
+ });
+ }
+ }
// 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) || player.browser.isIE
};
}
- // Build the default HTML
- function _buildControls() {
- // Create html array
- var html = [],
- iconUrl = _getIconUrl(),
- iconPath = (!iconUrl.absolute ? iconUrl.url : '') + '#' + config.iconPrefix;
+ // Create <svg> icon
+ function createIcon(type, attributes) {
+ var namespace = 'http://www.w3.org/2000/svg';
+ var iconUrl = getIconUrl();
+ var iconPath = (!iconUrl.absolute ? iconUrl.url : '') + '#' + config.iconPrefix;
- // Larger overlaid play button
- if (_inArray(config.controls, 'play-large')) {
- 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>'
- );
+ // Create <svg>
+ var icon = document.createElementNS(namespace, 'svg');
+ setAttributes(icon, extend(attributes, {
+ role: 'presentation'
+ }));
+
+ // Create the <use> to reference sprite
+ var use = document.createElementNS(namespace, 'use');
+ use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', iconPath + '-' + type);
+
+ // Add <use> to <svg>
+ icon.appendChild(use);
+
+ return icon;
+ }
+
+ // Create hidden text label
+ function createLabel(type) {
+ var text = config.i18n[type];
+
+ switch (type) {
+ case 'pip':
+ text = 'PIP';
+ break;
+
+ case 'airplay':
+ text = 'AirPlay';
+ break;
+ }
+
+ return createElement('span', {
+ class: config.classes.hidden
+ }, text);
+ }
+
+ // Create a badge
+ function createBadge(text) {
+ var badge = createElement('span', {
+ class: config.classes.menu.value
+ });
+
+ badge.appendChild(createElement('span', {
+ class: config.classes.menu.badge
+ }, text));
+
+ return badge;
+ }
+
+ // Create a <button>
+ function createButton(type, attributes) {
+ var button = createElement('button');
+ var iconDefault;
+ var iconToggled;
+ var labelKey;
+
+ if (!is.object(attributes)) {
+ attributes = {};
+ }
+
+ if ('class' in attributes) {
+ if (attributes.class.indexOf(config.classes.control) === -1) {
+ attributes.class += ' ' + config.classes.control;
+ }
+ } else {
+ attributes.class = config.classes.control;
+ }
+
+ // Large play button
+ switch (type) {
+ case 'mute':
+ labelKey = 'toggleMute';
+ iconDefault = 'volume';
+ iconToggled = 'muted';
+ break;
+
+ case 'captions':
+ labelKey = 'toggleCaptions';
+ iconDefault = 'captions-off';
+ iconToggled = 'captions-on';
+ break;
+
+ case 'fullscreen':
+ labelKey = 'toggleFullscreen';
+ iconDefault = 'enter-fullscreen';
+ iconToggled = 'exit-fullscreen';
+ break;
+
+ case 'play-large':
+ attributes.class = 'plyr__play-large';
+ type = 'play';
+ labelKey = 'play';
+ iconDefault = 'play';
+ break;
+
+ default:
+ labelKey = type;
+ iconDefault = type;
+ }
+
+ // Merge attributes
+ extend(attributes, getAttributesFromSelector(config.selectors.buttons[type], attributes));
+
+ // Add toggle icon if needed
+ if (is.string(iconToggled)) {
+ button.appendChild(createIcon(iconToggled, {
+ class: 'icon--' + iconToggled
+ }));
}
- html.push('<div class="plyr__controls">');
+ // Add the icon
+ button.appendChild(createIcon(iconDefault));
+
+ // Add the label
+ button.appendChild(createLabel(labelKey));
+
+ // Set element attributes
+ setAttributes(button, attributes);
+
+ player.elements.buttons[type] = button;
+
+ return button;
+ }
+
+ // Create an <input type='range'>
+ function createRange(type, attributes) {
+ // Seek label
+ var label = createElement('label', {
+ for: attributes.id,
+ class: config.classes.hidden
+ }, config.i18n[type]);
+
+ // Seek input
+ var input = createElement('input', extend(getAttributesFromSelector(config.selectors.inputs[type]), {
+ type: 'range',
+ min: 0,
+ max: 100,
+ step: 0.1,
+ value: 0
+ }, attributes));
+
+ player.elements.inputs[type] = input;
+
+ return {
+ label: label,
+ input: input
+ }
+ }
+
+ // Create a <progress>
+ function createProgress(type, attributes) {
+ var progress = createElement('progress', extend(getAttributesFromSelector(config.selectors.display[type]), {
+ min: 0,
+ max: 100,
+ value: 0
+ }, attributes));
+
+ // Create the label inside
+ if (type !== 'volume') {
+ progress.appendChild(createElement('span', null, '0'));
+
+ var suffix = '';
+ switch (type) {
+ case 'played':
+ suffix = config.i18n.played;
+ break;
+
+ case 'buffer':
+ suffix = config.i18n.buffered;
+ break;
+ }
+
+ progress.textContent = '% ' + suffix.toLowerCase();
+ }
+
+ player.elements.display[type] = progress;
+
+ return progress;
+ }
+
+ // Create time display
+ function createTime(type) {
+ var container = createElement('span', {
+ class: 'plyr__time'
+ });
+
+ container.appendChild(createElement('span', {
+ class: config.classes.hidden
+ }, config.i18n[type]));
+
+ container.appendChild(createElement('span', getAttributesFromSelector(config.selectors.display[type]), '00:00'));
+
+ player.elements.display[type] = container;
+
+ return container;
+ }
+
+ // Build the default HTML
+ function createControls(data) {
+ // Create the container
+ var controls = createElement('div', getAttributesFromSelector(config.selectors.controls.wrapper));
// Restart button
- if (_inArray(config.controls, 'restart')) {
- html.push(
- '<button type="button" data-plyr="restart">',
- '<svg><use xlink:href="' + iconPath + '-restart" /></svg>',
- '<span class="plyr__sr-only">' + config.i18n.restart + '</span>',
- '</button>'
- );
+ if (inArray(config.controls, 'restart')) {
+ controls.appendChild(createButton('restart'));
}
// Rewind button
- if (_inArray(config.controls, 'rewind')) {
- html.push(
- '<button type="button" data-plyr="rewind">',
- '<svg><use xlink:href="' + iconPath + '-rewind" /></svg>',
- '<span class="plyr__sr-only">' + config.i18n.rewind + '</span>',
- '</button>'
- );
+ if (inArray(config.controls, 'rewind')) {
+ controls.appendChild(createButton('rewind'));
}
// Play Pause button
// TODO: This should be a toggle button really?
- if (_inArray(config.controls, 'play')) {
- html.push(
- '<button type="button" 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">',
- '<svg><use xlink:href="' + iconPath + '-pause" /></svg>',
- '<span class="plyr__sr-only">' + config.i18n.pause + '</span>',
- '</button>'
- );
+ if (inArray(config.controls, 'play')) {
+ controls.appendChild(createButton('play'));
+ controls.appendChild(createButton('pause'));
}
// Fast forward button
- if (_inArray(config.controls, 'fast-forward')) {
- html.push(
- '<button type="button" data-plyr="fast-forward">',
- '<svg><use xlink:href="' + iconPath + '-fast-forward" /></svg>',
- '<span class="plyr__sr-only">' + config.i18n.forward + '</span>',
- '</button>'
- );
+ if (inArray(config.controls, 'fast-forward')) {
+ controls.appendChild(createButton('fast-forward'));
}
// 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">',
- '<span>0</span>% ' + config.i18n.buffered,
- '</progress>');
+ if (inArray(config.controls, 'progress')) {
+ var container = createElement('span', getAttributesFromSelector(config.selectors.progress));
+
+ // Seek range slider
+ var seek = createRange('seek', {
+ id: 'plyr-seek-' + data.id
+ });
+ container.appendChild(seek.label);
+ container.appendChild(seek.input);
+
+ // TODO: Add loop display indicator
+
+ // Played progress
+ container.appendChild(createProgress('played'));
+
+ // Buffer progress
+ container.appendChild(createProgress('buffer'));
// Seek tooltip
if (config.tooltips.seek) {
- html.push('<span class="plyr__tooltip">00:00</span>');
+ var tooltip = createElement('span', {
+ role: 'tooltip',
+ class: config.classes.tooltip
+ }, '00:00');
+
+ container.appendChild(tooltip);
+ player.elements.display.seekTooltip = tooltip;
}
- // Close
- html.push('</span>');
+ player.elements.progress = container;
+ controls.appendChild(player.elements.progress);
}
// Media current time display
- if (_inArray(config.controls, 'current-time')) {
- html.push(
- '<span class="plyr__time">',
- '<span class="plyr__sr-only">' + config.i18n.currentTime + '</span>',
- '<span class="plyr__time--current">00:00</span>',
- '</span>'
- );
+ if (inArray(config.controls, 'current-time')) {
+ controls.appendChild(createTime('currentTime'));
}
// Media duration display
- if (_inArray(config.controls, 'duration')) {
- html.push(
- '<span class="plyr__time">',
- '<span class="plyr__sr-only">' + config.i18n.duration + '</span>',
- '<span class="plyr__time--duration">00:00</span>',
- '</span>'
- );
+ if (inArray(config.controls, 'duration')) {
+ controls.appendChild(createTime('duration'));
}
// Toggle mute button
- if (_inArray(config.controls, 'mute')) {
- html.push(
- '<button type="button" 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>'
- );
+ if (inArray(config.controls, 'mute')) {
+ controls.appendChild(createButton('mute'));
}
// Volume range control
- if (_inArray(config.controls, 'volume')) {
- 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">',
- '<progress class="plyr__volume--display" max="' + config.volumeMax + '" value="' + config.volumeMin + '" role="presentation"></progress>',
- '</span>'
- );
+ if (inArray(config.controls, 'volume')) {
+ var volume = createElement('span', {
+ class: 'plyr__volume'
+ });
+
+ // Set the attributes
+ var attributes = {
+ max: 10,
+ value: config.volume
+ };
+
+ // Create the volume range slider
+ var range = createRange('volume', extend(attributes, {
+ id: 'plyr-volume-' + data.id
+ }));
+ volume.appendChild(range.label);
+ volume.appendChild(range.input);
+
+ // Create the display progress
+ var progress = createProgress('volume', attributes);
+ volume.appendChild(progress);
+
+ controls.appendChild(volume);
}
// Toggle captions button
- if (_inArray(config.controls, 'captions')) {
- html.push(
- '<button type="button" data-plyr="captions">',
- '<svg class="icon--captions-on"><use xlink:href="' + iconPath + '-captions-on" /></svg>',
- '<svg><use xlink:href="' + iconPath+ '-captions-off" /></svg>',
- '<span class="plyr__sr-only">' + config.i18n.toggleCaptions + '</span>',
- '</button>'
- );
+ if (inArray(config.controls, 'captions')) {
+ controls.appendChild(createButton('captions'));
+ }
+
+ // Settings button / menu
+ if (inArray(config.controls, 'settings')) {
+ var menu = createElement('span', extend(getAttributesFromSelector(config.selectors.buttons.settings), {
+ class: 'plyr__menu'
+ }));
+
+ menu.appendChild(createButton('settings', {
+ id: 'plyr-settings-toggle-' + data.id,
+ 'aria-haspopup': true,
+ 'aria-controls': 'plyr-settings-' + data.id,
+ 'aria-expanded': false
+ }));
+
+ var form = createElement('form', {
+ class: 'plyr__menu__container',
+ id: 'plyr-settings-' + data.id,
+ 'aria-hidden': true,
+ 'aria-labelled-by': 'plyr-settings-toggle-' + data.id,
+ role: 'tablist',
+ tabindex: -1
+ });
+
+ var inner = createElement('div');
+
+ var home = createElement('div', {
+ id: 'plyr-settings-' + data.id + '-home',
+ 'aria-hidden': false,
+ 'aria-labelled-by': 'plyr-settings-toggle-' + data.id,
+ role: 'tabpanel',
+ tabindex: -1
+ });
+
+ var tabs = createElement('ul', {
+ role: 'tablist'
+ });
+
+ ['captions', 'quality', 'speed', 'loop'].forEach(function(type) {
+ var tab = createElement('li', {
+ role: 'tab'
+ });
+
+ var button = createElement('button', extend(getAttributesFromSelector(config.selectors.buttons.settings), {
+ type: 'button',
+ class: config.classes.control + ' ' + config.classes.control + '--forward',
+ id: 'plyr-settings-' + data.id + '-' + type + '-tab',
+ 'aria-haspopup': true,
+ 'aria-controls': 'plyr-settings-' + data.id + '-' + type,
+ 'aria-expanded': false
+ }), config.i18n[type]);
+
+ var value = createElement('span', {
+ class: config.classes.menu.value
+ });
+
+ // Speed contains HTML entities
+ value.innerHTML = data[type];
+
+ button.appendChild(value);
+
+ tab.appendChild(button);
+
+ tabs.appendChild(tab);
+
+ player.elements.settings.tabs[type] = tab;
+ });
+
+ home.appendChild(tabs);
+
+ inner.appendChild(home);
+
+ ['captions', 'quality', 'speed', 'loop'].forEach(function(type) {
+ var pane = createElement('div', {
+ id: 'plyr-settings-' + data.id + '-' + type,
+ 'aria-hidden': true,
+ 'aria-labelled-by': 'plyr-settings-tab-' + data.id,
+ role: 'tabpanel',
+ tabindex: -1
+ });
+
+ var back = createElement('button', {
+ type: 'button',
+ class: config.classes.control + ' ' + config.classes.control + '--back',
+ 'aria-haspopup': true,
+ 'aria-controls': 'plyr-settings-' + data.id + '-home',
+ 'aria-expanded': false
+ }, config.i18n[type]);
+
+ pane.appendChild(back);
+
+ var options = createElement('ul');
+
+ /*switch (type) {
+ case 'captions':
+ if (is.array(config.tracks)) {
+ config.tracks.forEach(function(track, index) {
+ if (is.function(track)) {
+ return;
+ }
+
+ var option = createElement('li');
+
+ var button = createButton('language', {
+ 'data-language': track.srclang,
+ 'data-index': index
+ }, track.label);
+
+ option.appendChild(button);
+
+ options.appendChild(options);
+ });
+ }
+ break;
+ }*/
+
+ pane.appendChild(options);
+
+ inner.appendChild(pane);
+
+ player.elements.settings.panes[type] = pane;
+ });
+
+ form.appendChild(inner);
+
+ menu.appendChild(form);
+
+ controls.appendChild(menu);
+
+ player.elements.settings.menu = menu;
+
+ /*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>',
+ captionsMenuItem,
+ '<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">',
+
+ //showQuality,
+
+ '<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 data-captions="langs">',
+ buildCaptionsMenu(),
+ '</li>',
+ '<li>',
+ '<button type="button" class="plyr__control" data-plyr="captions_menu">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&times;',
+ '</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&times;',
+ '</label>',
+ '</li>',
+ '<li>',
+ '<label class="plyr__control">',
+ '<input type="radio" name="speed" data-plyr="speed" value="1.0" '+ (config.currentSpeed === 1 ? 'checked' : '') +'>',
+ '1.0&times;',
+ '</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&times;',
+ '</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>'
+ ); */
+ }
+
+ // Picture in picture button
+ if (inArray(config.controls, 'pip') && support.pip) {
+ controls.appendChild(createButton('pip'));
+ }
+
+ // Airplay button
+ if (inArray(config.controls, 'airplay') && support.airplay) {
+ controls.appendChild(createButton('airplay'));
}
// Toggle fullscreen button
- if (_inArray(config.controls, 'fullscreen')) {
- html.push(
- '<button type="button" 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>'
- );
+ if (inArray(config.controls, 'fullscreen')) {
+ controls.appendChild(createButton('fullscreen'));
+ }
+
+ player.elements.controls = controls;
+
+ setLoopMenu();
+
+ return controls;
+ }
+
+ // Set the YouTube quality menu
+ // TODO: Support for HTML5
+ // YouTube: "hd2160", "hd1440", "hd1080", "hd720", "large", "medium", "small", "tiny", "auto"
+ function setQualityMenu(available, current) {
+ if (is.object(player.quality)) {
+ return;
+ }
+
+ player.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 null;
+ }
+
+ return createBadge(label);
+ }
+
+ // 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 = player.elements.settings.panes.quality.querySelector('ul');
+
+ filtered.forEach(function(quality) {
+ var item = createElement('li');
+
+ var label = createElement('label', {
+ class: config.classes.control
+ });
+
+ var radio = createElement('input', {
+ type: 'radio',
+ name: 'quality',
+ value: quality,
+ });
+
+ if (quality === player.quality.current) {
+ radio.setAttribute('checked', '');
+ }
+
+ label.appendChild(radio);
+ label.appendChild(document.createTextNode(getLabel(quality)));
+
+ var badge = getBadge(quality);
+ if (is.htmlElement(badge)) {
+ label.appendChild(badge);
+ }
+
+ item.appendChild(label);
+
+ list.appendChild(item);
+ });
}
+ }
+
+ // Set the looping options
+ function setLoopMenu() {
+ var options = ['start', 'end', 'all', 'reset'];
+ var list = player.elements.settings.panes.loop.querySelector('ul');
- // Close everything
- html.push('</div>');
+ options.forEach(function(option) {
+ var item = createElement('li');
+
+ var button = createElement('button', {
+ type: 'button',
+ class: config.classes.control,
+ 'data-plyr': 'loop',
+ 'data-plyr-loop-action': option
+ }, config.i18n[option]);
+
+ if (inArray(['start', 'end'], option)) {
+ var badge = createBadge('0:00');
+ button.appendChild(badge);
+ }
- return html.join('');
+ item.appendChild(button);
+
+ list.appendChild(item);
+ });
}
// Setup fullscreen
- function _setupFullscreen() {
- if (!plyr.supported.full) {
+ function setupFullscreen() {
+ if (!player.supported.full) {
return;
}
- if ((plyr.type !== 'audio' || config.fullscreen.allowAudio) && config.fullscreen.enabled) {
+ if ((player.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(player.elements.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);
+ if (player.elements.buttons && player.elements.buttons.fullscreen) {
+ toggleState(player.elements.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') {
+ if (player.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)) {
+ player.elements.wrapper.insertAdjacentHTML('afterbegin', '<div class="' + getClassname(config.selectors.captions) + '"></div>');
}
// Determine if HTML5 textTracks is supported
- plyr.usingTextTracks = false;
- if (plyr.media.textTracks) {
- plyr.usingTextTracks = true;
+ player.captions.textTracks = false;
+ if (player.elements.media.textTracks) {
+ player.captions.textTracks = true;
}
// 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 = [];
+ var captionSrc = '';
+
+ player.elements.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 === '') {
- plyr.captionExists = false;
- _log('No caption track found');
+ player.captions.exist = true;
+ if (captionSources.length === 0) {
+ player.captions.exist = false;
+ log('No caption track found');
+ } else if ((Number(config.captions.selectedIndex) + 1) > captionSources.length) {
+ player.captions.exist = false;
+ 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);
+ if (!player.captions.exist) {
+ toggleClass(player.elements.container, config.classes.captions.enabled);
} else {
+ var tracks = player.elements.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(player);
// Disable unsupported browsers than report false positive
// Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1033144
- if ((plyr.browser.isIE && plyr.browser.version >= 10) ||
- (plyr.browser.isFirefox && plyr.browser.version >= 31)) {
+ if ((player.browser.isIE && player.browser.version >= 10) ||
+ (player.browser.isFirefox && player.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;
+ player.captions.textTracks = false;
}
// Rendering caption tracks
// Native support required - http://caniuse.com/webvtt
- if (plyr.usingTextTracks) {
- _log('TextTracks supported');
+ if (player.captions.textTracks) {
+ 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 = '';
- plyr.captions = [];
+ player.captions.current = '';
+ player.captions.captions = [];
if (captionSrc !== '') {
// Create XMLHttpRequest Object
@@ -1030,45 +1964,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;
+ player.captions.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();
+ player.captions.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');
}
}
};
@@ -1081,350 +2008,377 @@
}
}
- // Set the current caption
- function _setCaption(caption) {
- /* jshint unused:false */
- var container = _getElement(config.selectors.captions),
- content = document.createElement('span');
+ // Select active caption
+ function setCaptionIndex(index) {
+ // Save active caption
+ config.captions.selectedIndex = index || config.captions.selectedIndex;
+
+ // Clear caption
+ setCaption();
- // Empty the container
- container.innerHTML = '';
+ // Re-run setup
+ setupCaptions();
- // Default to empty
- if (_is.undefined(caption)) {
- caption = '';
+ //getElement('[data-captions="settings"]').innerHTML = getSelectedLanguage();
+ }
+
+ // Get current selected caption language
+ function getSelectedLanguage() {
+ if (config.tracks.length === 0) {
+ return 'No Subs';
}
- // Set the span content
- if (_is.string(caption)) {
- content.innerHTML = caption.trim();
+ if (player.captions.enabled || !is.boolean(player.captions.enabled) && player.storage.captions) {
+ return config.tracks[config.captions.selectedIndex].label;
} else {
- content.appendChild(caption);
+ return 'Disabled';
}
+ }
+
+ // Set the current caption
+ function setCaption(caption) {
+ var captions = getElement(config.selectors.captions);
- // Set new caption text
- container.appendChild(content);
+ if (is.htmlElement(captions)) {
+ var content = createElement('span');
- // Force redraw (for Safari)
- var redraw = container.offsetHeight;
+ // Empty the container
+ emptyElement(captions);
+
+ // Default to empty
+ if (is.undefined(caption)) {
+ caption = '';
+ }
+
+ // Set the span content
+ if (is.string(caption)) {
+ content.innerHTML = caption.trim();
+ } else {
+ content.appendChild(caption);
+ }
+
+ // Set new caption text
+ captions.appendChild(content);
+
+ // Force redraw (for Safari)
+ // 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) {
- var tcpair = [];
- tcpair = tc.split(' --> ');
- for(var i = 0; i < tcpair.length; i++) {
+ function timecodeCommon(timecode, pos) {
+ var parts = [];
+ parts = timecode.split(' --> ');
+ for (var i = 0; i < parts.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");
+ parts[i] = parts[i].replace(/(\d+:\d+:\d+\.\d+).*/, "$1");
}
- return _subTcSecs(tcpair[pos]);
+ return subTcSecs(parts[pos]);
}
- function _timecodeMin(tc) {
- return _timecodeCommon(tc, 0);
+
+ function timecodeMin(timecode) {
+ return timecodeCommon(timecode, 0);
}
- function _timecodeMax(tc) {
- return _timecodeCommon(tc, 1);
+
+ function timecodeMax(timecode) {
+ return timecodeCommon(timecode, 1);
}
- function _subTcSecs(tc) {
- if (tc === null || tc === undefined) {
+
+ function subTcSecs(timecode) {
+ if (is.undefined(timecode)) {
return 0;
} else {
- var tc1 = [],
- tc2 = [],
- seconds;
- tc1 = tc.split(',');
+ var tc1 = [];
+ var tc2 = [];
+ var seconds = 0;
+ tc1 = timecode.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;
}
}
// If it's not video, or we're using textTracks, bail.
- if (plyr.usingTextTracks || plyr.type !== 'video' || !plyr.supported.full) {
+ if (player.captions.textTracks || player.type !== 'video' || !player.supported.full) {
return;
}
// Reset subcount
- plyr.subcount = 0;
+ player.captions.count = 0;
// 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 : player.elements.media.currentTime;
// If there's no subs available, bail
- if (!plyr.captions[plyr.subcount]) {
+ if (!player.captions.captions[player.captions.count]) {
return;
}
- 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;
+ while (timecodeMax(player.captions.captions[player.captions.count][0]) < time.toFixed(1)) {
+ player.captions.count++;
+
+ if (player.captions.count > player.captions.captions.length - 1) {
+ player.captions.count = player.captions.captions.length - 1;
break;
}
}
// 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 (player.elements.media.currentTime.toFixed(1) >= timecodeMin(player.captions[player.subcount][0]) &&
+ player.elements.media.currentTime.toFixed(1) <= timecodeMax(player.captions[player.subcount][0])) {
+ player.captions.current = player.captions.captions[player.captions.count][1];
// Render the caption
- _setCaption(plyr.currentCaption);
+ setCaption(player.captions.current);
} 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) {
+ if (!player.elements.buttons.captions) {
return;
}
- _toggleClass(plyr.container, config.classes.captions.enabled, true);
+ toggleClass(player.elements.container, config.classes.captions.enabled, true);
// Try to load the value from storage
- var active = plyr.storage.captionsEnabled;
+ var active = player.storage.captions;
// 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(player.elements.container, config.classes.captions.active, true);
+ toggleState(player.elements.buttons.captions, true);
}
}
- // Find all elements
- function _getElements(selector) {
- return plyr.container.querySelectorAll(selector);
- }
-
- // Find a single element
- function _getElement(selector) {
- return _getElements(selector)[0];
- }
-
- // Determine if we're in an iframe
- function _inFrame() {
- try {
- return window.self !== window.top;
+ // Toggle captions
+ function toggleCaptions(show) {
+ // If there's no full support, or there's no caption toggle
+ if (!player.supported.full || !player.elements.buttons.captions) {
+ return;
}
- catch (e) {
- return true;
+
+ // If the method is called without parameter, toggle based on current value
+ if (!is.boolean(show)) {
+ show = (player.elements.container.className.indexOf(config.classes.captions.active) === -1);
}
- }
- // Trap focus inside container
- function _focusTrap() {
- var tabbables = _getElements('input:not([disabled]), button:not([disabled])'),
- first = tabbables[0],
- last = tabbables[tabbables.length - 1];
+ // Set global
+ player.captions.enabled = show;
+ //player.elements.buttons.captions_menu.innerHTML = show ? 'Off' : 'On';
+ //TODO: display lang getElement('[data-captions="settings"]').innerHTML = getSubsLangValue();
- function _checkFocus(event) {
- // If it is TAB
- if (event.which === 9 && plyr.isFullscreen) {
- if (event.target === last && !event.shiftKey) {
- // Move focus to first element that can be tabbed if Shift isn't used
- event.preventDefault();
- first.focus();
- } else if (event.target === first && event.shiftKey) {
- // Move focus to last element that can be tabbed if Shift is used
- event.preventDefault();
- last.focus();
- }
- }
- }
+ // Set current language etc
+ //elements.buttons.captions_menu.innerHTML = show ? 'Off' : 'On';
+ //getElement('[data-captions="settings"]').innerHTML = getSubsLangValue();
- // Bind the handler
- _on(plyr.container, 'keydown', _checkFocus);
- }
+ // Toggle state
+ toggleState(player.elements.buttons.captions, player.captions.enabled);
- // Add elements to HTML5 media (source, tracks, etc)
- 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]);
- }
- }
+ // Add class hook
+ toggleClass(player.elements.container, config.classes.captions.active, player.captions.enabled);
+
+ // Trigger an event
+ trigger(player.elements.container, player.captions.enabled ? 'captionsenabled' : 'captionsdisabled', true);
+
+ // Save captions state to localStorage
+ updateStorage({
+ captions: player.captions.enabled
+ });
}
// 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' + (player.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');
}
}
- // Make a copy of the html
- var html = config.html;
-
- // Insert custom video controls
- _log('Injecting custom controls');
-
- // If no controls are specified, create default
- if (!html) {
- html = _buildControls();
- }
-
- // Replace seek time instances
- html = _replaceAll(html, '{seektime}', config.seekTime);
-
- // Replace all id references with random numbers
- html = _replaceAll(html, '{id}', Math.floor(Math.random() * (10000)));
+ // Larger overlaid play button
+ if (inArray(config.controls, 'play-large')) {
+ player.elements.buttons.playLarge = createButton('play-large');
+ player.elements.container.appendChild(player.elements.buttons.playLarge);
+ }
+
+ // Create a unique ID
+ player.id = Math.floor(Math.random() * 10000);
+
+ // Create controls
+ var controls = createControls({
+ id: player.id,
+ seektime: config.seekTime,
+ speed: getSpeedDisplayValue(),
+ // TODO: Get current quality
+ quality: 'HD',
+ // TODO: Set language automatically based on UA?
+ captions: 'English',
+ // TODO: Get loop
+ loop: 'None'
+ });
// 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)) {
- target = plyr.container
+ if (!is.htmlElement(target)) {
+ target = player.elements.container
}
// Inject controls HTML
- target.insertAdjacentHTML('beforeend', html);
+ // target.insertAdjacentHTML('beforeend', html);
+ target.appendChild(controls);
// 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);
}
}
}
// Find the UI controls and store references
- function _findElements() {
+ // TODO: Restore when re-enabling custom HTML
+ /*function findElements() {
try {
- plyr.controls = _getElement(config.selectors.controls.wrapper);
+ player.elements.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);
-
- // Inputs
- plyr.buttons.mute = _getElement(config.selectors.buttons.mute);
- plyr.buttons.captions = _getElement(config.selectors.buttons.captions);
+ player.elements.buttons = {
+ 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),
+ //lang: getElement(config.selectors.buttons.captions_lang),
+ speed: getElement(config.selectors.buttons.speed),
+ loop: getElement(config.selectors.buttons.loop),
+ mute: getElement(config.selectors.buttons.mute),
+ captions: getElement(config.selectors.buttons.captions)
+ };
// Progress
- plyr.progress = {};
- plyr.progress.container = _getElement(config.selectors.progress.container);
+ player.elements.progress = getElement(config.selectors.progress);
- // 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];
+ // Inputs
+ player.elements.inputs = {
+ seek: getElement(config.selectors.inputs.seek),
+ volume: getElement(config.selectors.inputs.volume)
+ };
- // Progress - Played
- plyr.progress.played = _getElement(config.selectors.progress.played);
+ // Display
+ player.elements.display = {
+ buffer: getElement(config.selectors.display.buffer),
+ played: getElement(config.selectors.display.played),
+ volume: getElement(config.selectors.display.volume),
+ duration: getElement(config.selectors.display.duration),
+ currentTime: getElement(config.selectors.display.currentTime),
+ };
// Seek 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);
-
- // Timing
- plyr.duration = _getElement(config.selectors.duration);
- plyr.currentTime = _getElement(config.selectors.currentTime);
- plyr.seekTime = _getElements(config.selectors.seekTime);
+ if (is.htmlElement(player.elements.progress)) {
+ player.elements.display.seekTooltip = player.elements.progress.querySelector('.' + config.classes.tooltip);
+ }
return true;
- }
- catch(e) {
- _warn('It looks like there is a problem with your controls HTML');
+ } catch (error) {
+ warn('It looks like there is a problem with your custom controls HTML', error);
// 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(player.elements.container, config.selectors.container.replace('.', ''), player.supported.full);
}
// Toggle native controls
- function _toggleNativeControls(toggle) {
- if (toggle && _inArray(config.types.html5, plyr.type)) {
- plyr.media.setAttribute('controls', '');
+ function toggleNativeControls(toggle) {
+ if (toggle && inArray(config.types.html5, player.type)) {
+ player.elements.media.setAttribute('controls', '');
} else {
- plyr.media.removeAttribute('controls');
+ player.elements.media.removeAttribute('controls');
}
}
// 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
- plyr.container.setAttribute('aria-label', config.title);
+ player.elements.container.setAttribute('aria-label', config.title);
}
// If there's a play button, set label
- if (plyr.supported.full && plyr.buttons.play) {
- for (var i = plyr.buttons.play.length - 1; i >= 0; i--) {
- plyr.buttons.play[i].setAttribute('aria-label', label);
+ if (player.supported.full) {
+ if (is.htmlElement(player.elements.buttons.play)) {
+ player.elements.buttons.play.setAttribute('aria-label', label);
+ }
+ if (is.htmlElement(player.elements.buttons.playLarge)) {
+ player.elements.buttons.playLarge.setAttribute('aria-label', label);
}
}
// 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 = {};
+ player.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;
}
@@ -1440,260 +2394,290 @@
return;
} else if (/^\d+(\.\d+)?$/.test(value)) {
// If value is a number, it's probably volume from an older
- // version of plyr. See: https://github.com/Selz/plyr/pull/313
+ // version of player. 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);
+ player.storage = JSON.parse(value);
}
}
// 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(player.storage, value);
// Update storage
- window.localStorage.setItem(config.storage.key, JSON.stringify(plyr.storage));
+ window.localStorage.setItem(config.storage.key, JSON.stringify(player.storage));
}
// Setup media
- function _setupMedia() {
+ function setupMedia() {
// If there's no media, bail
- if (!plyr.media) {
- _warn('No media element found!');
+ if (!player.elements.media) {
+ warn('No media element found!');
return;
}
- if (plyr.supported.full) {
+ if (player.supported.full) {
// Add type class
- _toggleClass(plyr.container, config.classes.type.replace('{0}', plyr.type), true);
+ toggleClass(player.elements.container, config.classes.type.replace('{0}', player.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, player.type)) {
+ toggleClass(player.elements.container, config.classes.type.replace('{0}', 'video'), true);
}
+ // Check for picture-in-picture support
+ toggleClass(player.elements.container, config.classes.pip.enabled, support.pip && player.type === 'video');
+
+ // Check for airplay support
+ toggleClass(player.elements.container, config.classes.airplay.enabled, support.airplay && inArray(config.types.html5, player.type));
+
// If there's no autoplay attribute, assume the video is stopped and add state class
- _toggleClass(plyr.container, config.classes.stopped, config.autoplay);
+ toggleClass(player.elements.container, config.classes.stopped, config.autoplay);
// Add iOS class
- _toggleClass(plyr.container, config.classes.isIos, plyr.browser.isIos);
+ toggleClass(player.elements.container, config.classes.isIos, player.browser.isIos);
// Add touch class
- _toggleClass(plyr.container, config.classes.isTouch, plyr.browser.isTouch);
+ toggleClass(player.elements.container, config.classes.isTouch, player.browser.isTouch);
// Inject the player wrapper
- if (plyr.type === 'video') {
+ if (player.type === 'video') {
// Create the wrapper div
- var wrapper = document.createElement('div');
+ var wrapper = createElement('div');
wrapper.setAttribute('class', config.classes.videoWrapper);
// Wrap the video in a container
- _wrap(plyr.media, wrapper);
+ wrap(player.elements.media, wrapper);
// Cache the container
- plyr.videoContainer = wrapper;
+ player.elements.wrapper = wrapper;
}
}
// Embeds
- if (_inArray(config.types.embed, plyr.type)) {
- _setupEmbed();
+ if (inArray(config.types.embed, player.type)) {
+ setupEmbed();
}
}
// Setup YouTube/Vimeo
- function _setupEmbed() {
- var container = document.createElement('div'),
- mediaId,
- id = plyr.type + '-' + Math.floor(Math.random() * (10000));
+ function setupEmbed() {
+ var container = createElement('div');
+ var mediaId;
+ var id = player.type + '-' + Math.floor(Math.random() * (10000));
// Parse IDs from URLs if supplied
- switch (plyr.type) {
+ switch (player.type) {
case 'youtube':
- mediaId = _parseYouTubeId(plyr.embedId);
+ mediaId = parseYouTubeId(player.embedId);
break;
default:
- mediaId = plyr.embedId;
+ mediaId = player.embedId;
}
// Remove old containers
- var containers = _getElements('[id^="' + plyr.type + '-"]');
+ var containers = getElements('[id^="' + player.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(player.elements.media, config.classes.videoWrapper, true);
+ toggleClass(player.elements.media, config.classes.embedWrapper, true);
- if (plyr.type === 'youtube') {
+ if (player.type === 'youtube') {
// Create the YouTube container
- plyr.media.appendChild(container);
+ player.elements.media.appendChild(container);
// Set ID
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') {
+ } else if (player.type === 'vimeo') {
// Vimeo needs an extra div to hide controls on desktop (which has full support)
- if (plyr.supported.full) {
- plyr.media.appendChild(container);
+ if (player.supported.full) {
+ player.elements.media.appendChild(container);
} else {
- container = plyr.media;
+ container = player.elements.media;
}
// Set ID
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') {
+ } else if (player.type === 'soundcloud') {
// TODO: Currently unsupported and undocumented
// Inject the iframe
- var soundCloud = document.createElement('iframe');
+ var soundCloud = createElement('iframe');
// 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);
- plyr.media.appendChild(container);
+ player.elements.media.appendChild(container);
// 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();
+ if (player.supported.full) {
+ 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, {
+ player.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: (player.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(player.elements.container, 'error', true, {
+ code: event.data,
+ embed: event.target
});
},
+ 'onPlaybackQualityChange': function(event) {
+ // Get the instance
+ var instance = event.target;
+
+ // Get current quality
+ var quality = instance.getPlaybackQuality();
+
+ // var set = player.setPlaybackQuality();
+ console.warn(quality);
+ },
'onReady': function(event) {
// Get the instance
var instance = event.target;
// Create a faux HTML5 API using the YouTube API
- plyr.media.play = function() {
+ player.elements.media.play = function() {
instance.playVideo();
- plyr.media.paused = false;
+ player.elements.media.paused = false;
};
- plyr.media.pause = function() {
+ player.elements.media.pause = function() {
instance.pauseVideo();
- plyr.media.paused = true;
+ player.elements.media.paused = true;
};
- plyr.media.stop = function() {
+ player.elements.media.stop = function() {
instance.stopVideo();
- plyr.media.paused = true;
+ player.elements.media.paused = true;
};
- plyr.media.duration = instance.getDuration();
- plyr.media.paused = true;
- plyr.media.currentTime = 0;
- plyr.media.muted = instance.isMuted();
+ player.elements.media.duration = instance.getDuration();
+ player.elements.media.paused = true;
+ player.elements.media.currentTime = 0;
+ player.elements.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;
// Set the tabindex
- if (plyr.supported.full) {
- plyr.media.querySelector('iframe').setAttribute('tabindex', '-1');
+ if (player.supported.full) {
+ player.elements.media.querySelector('iframe').setAttribute('tabindex', '-1');
}
// Update UI
- _embedReady();
+ embedReady();
// Trigger timeupdate
- _triggerEvent(plyr.media, 'timeupdate');
+ trigger(player.elements.media, 'timeupdate');
// Trigger timeupdate
- _triggerEvent(plyr.media, 'durationchange');
+ trigger(player.elements.media, 'durationchange');
// Reset timer
window.clearInterval(timers.buffering);
@@ -1701,22 +2685,22 @@
// Setup buffering
timers.buffering = window.setInterval(function() {
// Get loaded % from YouTube
- plyr.media.buffered = instance.getVideoLoadedFraction();
+ player.elements.media.buffered = instance.getVideoLoadedFraction();
// Trigger progress only when we actually buffer something
- if (plyr.media.lastBuffered === null || plyr.media.lastBuffered < plyr.media.buffered) {
- _triggerEvent(plyr.media, 'progress');
+ if (player.elements.media.lastBuffered === null || player.elements.media.lastBuffered < player.elements.media.buffered) {
+ trigger(player.elements.media, 'progress');
}
// Set last buffer point
- plyr.media.lastBuffered = plyr.media.buffered;
+ player.elements.media.lastBuffered = player.elements.media.buffered;
// Bail if we're at 100%
- if (plyr.media.buffered === 1) {
+ if (player.elements.media.buffered === 1) {
window.clearInterval(timers.buffering);
// Trigger event
- _triggerEvent(plyr.media, 'canplaythrough');
+ trigger(player.elements.media, 'canplaythrough');
}
}, 200);
},
@@ -1736,48 +2720,53 @@
// 5 Video cued
switch (event.data) {
case 0:
- plyr.media.paused = true;
- _triggerEvent(plyr.media, 'ended');
+ player.elements.media.paused = true;
+ trigger(player.elements.media, 'ended');
break;
case 1:
- plyr.media.paused = false;
+ player.elements.media.paused = false;
// If we were seeking, fire seeked event
- if (plyr.media.seeking) {
- _triggerEvent(plyr.media, 'seeked');
+ if (player.elements.media.seeking) {
+ trigger(player.elements.media, 'seeked');
}
- plyr.media.seeking = false;
- _triggerEvent(plyr.media, 'play');
- _triggerEvent(plyr.media, 'playing');
+ player.elements.media.seeking = false;
+ trigger(player.elements.media, 'play');
+ trigger(player.elements.media, 'playing');
// Poll to get playback progress
timers.playing = window.setInterval(function() {
// Set the current time
- plyr.media.currentTime = instance.getCurrentTime();
+ player.elements.media.currentTime = instance.getCurrentTime();
// Trigger timeupdate
- _triggerEvent(plyr.media, 'timeupdate');
+ trigger(player.elements.media, 'timeupdate');
}, 100);
// Check duration again due to YouTube bug
// https://github.com/Selz/plyr/issues/374
// 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');
+ if (player.elements.media.duration !== instance.getDuration()) {
+ player.elements.media.duration = instance.getDuration();
+ trigger(player.elements.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');
+ player.elements.media.paused = true;
+ trigger(player.elements.media, 'pause');
break;
}
- _triggerEvent(plyr.container, 'statechange', false, {
+ trigger(player.elements.container, 'statechange', false, {
code: event.data
});
}
@@ -1786,7 +2775,7 @@
}
// 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, {
@@ -1799,222 +2788,347 @@
});
// Create a faux HTML5 API using the Vimeo API
- plyr.media.play = function() {
- plyr.embed.play();
- plyr.media.paused = false;
+ player.elements.media.play = function() {
+ player.embed.play();
+ player.elements.media.paused = false;
};
- plyr.media.pause = function() {
- plyr.embed.pause();
- plyr.media.paused = true;
+ player.elements.media.pause = function() {
+ player.embed.pause();
+ player.elements.media.paused = true;
};
- plyr.media.stop = function() {
- plyr.embed.stop();
- plyr.media.paused = true;
+ player.elements.media.stop = function() {
+ player.embed.stop();
+ player.elements.media.paused = true;
};
- plyr.media.paused = true;
- plyr.media.currentTime = 0;
+ player.elements.media.paused = true;
+ player.elements.media.currentTime = 0;
// Update UI
- _embedReady();
+ embedReady();
- plyr.embed.getCurrentTime().then(function(value) {
- plyr.media.currentTime = value;
+ player.embed.getCurrentTime().then(function(value) {
+ player.elements.media.currentTime = value;
// Trigger timeupdate
- _triggerEvent(plyr.media, 'timeupdate');
+ trigger(player.elements.media, 'timeupdate');
});
- plyr.embed.getDuration().then(function(value) {
- plyr.media.duration = value;
+ player.embed.getDuration().then(function(value) {
+ player.elements.media.duration = value;
// Trigger timeupdate
- _triggerEvent(plyr.media, 'durationchange');
+ trigger(player.elements.media, 'durationchange');
});
// TODO: Captions
/*if (config.captions.defaultActive) {
- plyr.embed.enableTextTrack('en');
+ player.embed.enableTextTrack('en');
}*/
- plyr.embed.on('loaded', function() {
+ player.embed.on('loaded', function() {
// Fix keyboard focus issues
// https://github.com/Selz/plyr/issues/317
- if (_is.htmlElement(plyr.embed.element) && plyr.supported.full) {
- plyr.embed.element.setAttribute('tabindex', '-1');
+ if (is.htmlElement(player.embed.element) && player.supported.full) {
+ player.embed.element.setAttribute('tabindex', '-1');
}
});
- plyr.embed.on('play', function() {
- plyr.media.paused = false;
- _triggerEvent(plyr.media, 'play');
- _triggerEvent(plyr.media, 'playing');
+ player.embed.on('play', function() {
+ player.elements.media.paused = false;
+ trigger(player.elements.media, 'play');
+ trigger(player.elements.media, 'playing');
});
- plyr.embed.on('pause', function() {
- plyr.media.paused = true;
- _triggerEvent(plyr.media, 'pause');
+ player.embed.on('pause', function() {
+ player.elements.media.paused = true;
+ trigger(player.elements.media, 'pause');
});
- plyr.embed.on('timeupdate', function(data) {
- plyr.media.seeking = false;
- plyr.media.currentTime = data.seconds;
- _triggerEvent(plyr.media, 'timeupdate');
+ player.embed.on('timeupdate', function(data) {
+ player.elements.media.seeking = false;
+ player.elements.media.currentTime = data.seconds;
+ trigger(player.elements.media, 'timeupdate');
});
- plyr.embed.on('progress', function(data) {
- plyr.media.buffered = data.percent;
- _triggerEvent(plyr.media, 'progress');
+ player.embed.on('progress', function(data) {
+ player.elements.media.buffered = data.percent;
+ trigger(player.elements.media, 'progress');
if (parseInt(data.percent) === 1) {
// Trigger event
- _triggerEvent(plyr.media, 'canplaythrough');
+ trigger(player.elements.media, 'canplaythrough');
}
});
- plyr.embed.on('seeked', function() {
- plyr.media.seeking = false;
- _triggerEvent(plyr.media, 'seeked');
- _triggerEvent(plyr.media, 'play');
+ player.embed.on('seeked', function() {
+ player.elements.media.seeking = false;
+ trigger(player.elements.media, 'seeked');
+ trigger(player.elements.media, 'play');
});
- plyr.embed.on('ended', function() {
- plyr.media.paused = true;
- _triggerEvent(plyr.media, 'ended');
+ player.embed.on('ended', function() {
+ player.elements.media.paused = true;
+ trigger(player.elements.media, 'ended');
});
}
// Soundcloud ready
- function _soundcloudReady() {
+ function soundcloudReady() {
/* jshint validthis: true */
- plyr.embed = window.SC.Widget(this);
+ player.embed = window.SC.Widget(this);
// Setup on ready
- plyr.embed.bind(window.SC.Widget.Events.READY, function() {
+ player.embed.bind(window.SC.Widget.Events.READY, function() {
// Create a faux HTML5 API using the Soundcloud API
- plyr.media.play = function() {
- plyr.embed.play();
- plyr.media.paused = false;
+ player.elements.media.play = function() {
+ player.embed.play();
+ player.elements.media.paused = false;
};
- plyr.media.pause = function() {
- plyr.embed.pause();
- plyr.media.paused = true;
+ player.elements.media.pause = function() {
+ player.embed.pause();
+ player.elements.media.paused = true;
};
- plyr.media.stop = function() {
- plyr.embed.seekTo(0);
- plyr.embed.pause();
- plyr.media.paused = true;
+ player.elements.media.stop = function() {
+ player.embed.seekTo(0);
+ player.embed.pause();
+ player.elements.media.paused = true;
};
- plyr.media.paused = true;
- plyr.media.currentTime = 0;
+ player.elements.media.paused = true;
+ player.elements.media.currentTime = 0;
- plyr.embed.getDuration(function(value) {
- plyr.media.duration = value/1000;
+ player.embed.getDuration(function(value) {
+ player.elements.media.duration = value / 1000;
// Update UI
- _embedReady();
+ embedReady();
});
- plyr.embed.getPosition(function(value) {
- plyr.media.currentTime = value;
+ player.embed.getPosition(function(value) {
+ player.elements.media.currentTime = value;
// Trigger timeupdate
- _triggerEvent(plyr.media, 'timeupdate');
+ trigger(player.elements.media, 'timeupdate');
});
- plyr.embed.bind(window.SC.Widget.Events.PLAY, function() {
- plyr.media.paused = false;
- _triggerEvent(plyr.media, 'play');
- _triggerEvent(plyr.media, 'playing');
+ player.embed.bind(window.SC.Widget.Events.PLAY, function() {
+ player.elements.media.paused = false;
+ trigger(player.elements.media, 'play');
+ trigger(player.elements.media, 'playing');
});
- plyr.embed.bind(window.SC.Widget.Events.PAUSE, function() {
- plyr.media.paused = true;
- _triggerEvent(plyr.media, 'pause');
+ player.embed.bind(window.SC.Widget.Events.PAUSE, function() {
+ player.elements.media.paused = true;
+ trigger(player.elements.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');
+ player.embed.bind(window.SC.Widget.Events.PLAY_PROGRESS, function(data) {
+ player.elements.media.seeking = false;
+ player.elements.media.currentTime = data.currentPosition / 1000;
+ trigger(player.elements.media, 'timeupdate');
});
- plyr.embed.bind(window.SC.Widget.Events.LOAD_PROGRESS, function(data) {
- plyr.media.buffered = data.loadProgress;
- _triggerEvent(plyr.media, 'progress');
+ player.embed.bind(window.SC.Widget.Events.LOAD_PROGRESS, function(data) {
+ player.elements.media.buffered = data.loadProgress;
+ trigger(player.elements.media, 'progress');
if (parseInt(data.loadProgress) === 1) {
// Trigger event
- _triggerEvent(plyr.media, 'canplaythrough');
+ trigger(player.elements.media, 'canplaythrough');
}
});
- plyr.embed.bind(window.SC.Widget.Events.FINISH, function() {
- plyr.media.paused = true;
- _triggerEvent(plyr.media, 'ended');
+ player.embed.bind(window.SC.Widget.Events.FINISH, function() {
+ player.elements.media.paused = true;
+ trigger(player.elements.media, 'ended');
});
});
}
// Play media
- function _play() {
- if ('play' in plyr.media) {
- plyr.media.play();
+ function play() {
+ if ('play' in player.elements.media) {
+ player.elements.media.play();
}
}
// Pause media
- function _pause() {
- if ('pause' in plyr.media) {
- plyr.media.pause();
+ function pause() {
+ if ('pause' in player.elements.media) {
+ player.elements.media.pause();
}
}
// Toggle playback
- function _togglePlay(toggle) {
+ function togglePlay(toggle) {
// True toggle
- if (!_is.boolean(toggle)) {
- toggle = plyr.media.paused;
+ if (!is.boolean(toggle)) {
+ toggle = player.elements.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(player.elements.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 = player.elements.display.played.value;
+ break;
+
+ case 'end':
+ if (config.loop.start >= currentTime) {
+ return;
+ }
+ config.loop.end = currentTime;
+ config.loop.indicator.end = player.elements.display.played.value;
+ break;
+
+ case 'all':
+ config.loop.start = 0;
+ config.loop.end = player.elements.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 = player.elements.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';
+ }
+ }
+
+ // Set playback speed
+ function setSpeed(speed) {
+ // Load speed from storage or default value
+ if (is.undefined(speed)) {
+ speed = player.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
+ player.elements.media.playbackRate = speed;
+
+ // Save speed to localStorage
+ updateStorage({
+ speed: speed
+ });
+
+ // Update current value of menu
+ // document.querySelector('[data-menu="speed"]').innerHTML = getSpeedDisplayValue();
+ }
+
+ // Get the current speed value
+ function getSpeedDisplayValue() {
+ return config.currentSpeed.toFixed(1).toString().replace('.0', '') + '&times;'
+ }
+
// 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(player.elements.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(player.elements.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;
+ var paused = player.elements.media.paused;
+ var 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);
@@ -2028,64 +3142,63 @@
}
// 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) {}
+ player.elements.media.currentTime = targetTime.toFixed(4);
+ } catch (e) {}
// Embeds
- if (_inArray(config.types.embed, plyr.type)) {
- switch(plyr.type) {
+ if (inArray(config.types.embed, player.type)) {
+ switch (player.type) {
case 'youtube':
- plyr.embed.seekTo(targetTime);
+ player.embed.seekTo(targetTime);
break;
case 'vimeo':
// Round to nearest second for vimeo
- plyr.embed.setCurrentTime(targetTime.toFixed(0));
+ player.embed.setCurrentTime(targetTime.toFixed(0));
break;
case 'soundcloud':
- plyr.embed.seekTo(targetTime * 1000);
+ player.embed.seekTo(targetTime * 1000);
break;
}
if (paused) {
- _pause();
+ pause();
}
// Trigger timeupdate
- _triggerEvent(plyr.media, 'timeupdate');
+ trigger(player.elements.media, 'timeupdate');
// Set seeking flag
- plyr.media.seeking = true;
+ player.elements.media.seeking = true;
// Trigger seeking
- _triggerEvent(plyr.media, 'seeking');
+ trigger(player.elements.media, 'seeking');
}
// Logging
- _log('Seeking to ' + plyr.media.currentTime + ' seconds');
+ log('Seeking to ' + player.elements.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),
+ var duration = parseInt(config.duration);
// True duration
- mediaDuration = 0;
+ var mediaDuration = 0;
// Only if duration available
- if (plyr.media.duration !== null && !isNaN(plyr.media.duration)) {
- mediaDuration = plyr.media.duration;
+ if (player.elements.media.duration !== null && !isNaN(player.elements.media.duration)) {
+ mediaDuration = player.elements.media.duration;
}
// If custom duration is funky, use regular duration
@@ -2093,16 +3206,16 @@
}
// Check playing state
- function _checkPlaying() {
- _toggleClass(plyr.container, config.classes.playing, !plyr.media.paused);
+ function checkPlaying() {
+ toggleClass(player.elements.container, config.classes.playing, !player.elements.media.paused);
- _toggleClass(plyr.container, config.classes.stopped, plyr.media.paused);
+ toggleClass(player.elements.container, config.classes.stopped, player.elements.media.paused);
- _toggleControls(plyr.media.paused);
+ toggleControls(player.elements.media.paused);
}
// Save scroll position
- function _saveScrollPosition() {
+ function saveScrollPosition() {
scroll = {
x: window.pageXOffset || 0,
y: window.pageYOffset || 0
@@ -2110,110 +3223,175 @@
}
// 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) {
- plyr.isFullscreen = fullscreen.isFullScreen(plyr.container);
+ if (event && event.type === fullscreen.eventType) {
+ player.fullscreen.active = fullscreen.isFullScreen(player.elements.container);
} else {
// Else it's a user request to enter or exit
- if (!fullscreen.isFullScreen(plyr.container)) {
+ if (!fullscreen.isFullScreen(player.elements.container)) {
// Save scroll position
- _saveScrollPosition();
+ saveScrollPosition();
// Request full screen
- fullscreen.requestFullScreen(plyr.container);
+ fullscreen.requestFullScreen(player.elements.container);
} else {
// Bail from fullscreen
fullscreen.cancelFullScreen();
}
// Check if we're actually full screen (it could fail)
- plyr.isFullscreen = fullscreen.isFullScreen(plyr.container);
+ player.fullscreen.active = fullscreen.isFullScreen(player.elements.container);
return;
}
} else {
// Otherwise, it's a simple toggle
- plyr.isFullscreen = !plyr.isFullscreen;
+ player.fullscreen.active = !player.fullscreen.active;
// Bind/unbind escape key
- document.body.style.overflow = plyr.isFullscreen ? 'hidden' : '';
+ document.body.style.overflow = player.fullscreen.active ? 'hidden' : '';
}
// Set class hook
- _toggleClass(plyr.container, config.classes.fullscreen.active, plyr.isFullscreen);
+ toggleClass(player.elements.container, config.classes.fullscreen.active, player.fullscreen.active);
// Trap focus
- _focusTrap(plyr.isFullscreen);
+ focusTrap(player.fullscreen.active);
// Set button state
- if (plyr.buttons && plyr.buttons.fullscreen) {
- _toggleState(plyr.buttons.fullscreen, plyr.isFullscreen);
+ if (player.elements.buttons && player.elements.buttons.fullscreen) {
+ toggleState(player.elements.buttons.fullscreen, player.fullscreen.active);
}
// Trigger an event
- _triggerEvent(plyr.container, plyr.isFullscreen ? 'enterfullscreen' : 'exitfullscreen', true);
+ trigger(player.elements.container, player.fullscreen.active ? 'enterfullscreen' : 'exitfullscreen', true);
// Restore scroll position
- if (!plyr.isFullscreen && nativeSupport) {
- _restoreScrollPosition();
+ if (!player.fullscreen.active && nativeSupport) {
+ restoreScrollPosition();
+ }
+ }
+
+ // Toggle Menu
+ function toggleMenu(event) {
+ var menu = player.elements.settings.menu.parentNode;
+ var toggle = event.target;
+ var target = document.getElementById(toggle.getAttribute('aria-controls'));
+ var 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';
+ var targetWidth;
+ var targetHeight;
+ var 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);
}
}
// Mute
- function _toggleMute(muted) {
+ function toggleMute(muted) {
// If the method is called without parameter, toggle based on current value
- if (!_is.boolean(muted)) {
- muted = !plyr.media.muted;
+ if (!is.boolean(muted)) {
+ muted = !player.elements.media.muted;
}
// Set button state
- _toggleState(plyr.buttons.mute, muted);
+ toggleState(player.elements.buttons.mute, muted);
// Set mute on the player
- plyr.media.muted = muted;
+ player.elements.media.muted = muted;
// If volume is 0 after unmuting, set to default
- if (plyr.media.volume === 0) {
- _setVolume(config.volume);
+ if (player.elements.media.volume === 0) {
+ setVolume(config.volume);
}
// Embeds
- if (_inArray(config.types.embed, plyr.type)) {
+ if (inArray(config.types.embed, player.type)) {
// YouTube
- switch(plyr.type) {
+ switch (player.type) {
case 'youtube':
- plyr.embed[plyr.media.muted ? 'mute' : 'unMute']();
+ player.embed[player.elements.media.muted ? 'mute' : 'unMute']();
break;
case 'vimeo':
case 'soundcloud':
- plyr.embed.setVolume(plyr.media.muted ? 0 : parseFloat(config.volume / config.volumeMax));
+ player.embed.setVolume(player.elements.media.muted ? 0 : parseFloat(config.volume / 10));
break;
}
// Trigger volumechange for embeds
- _triggerEvent(plyr.media, 'volumechange');
+ trigger(player.elements.media, 'volumechange');
}
}
// Set volume
- function _setVolume(volume) {
- var max = config.volumeMax,
- min = config.volumeMin;
+ function setVolume(volume) {
+ var max = 10;
+ var min = 0;
+
+ // If volume is event, get from input
+ if (is.event(volume)) {
+ volume = volume.target.value;
+ }
// Load volume from storage if no value specified
- if (_is.undefined(volume)) {
- volume = plyr.storage.volume;
+ if (is.undefined(volume)) {
+ volume = player.storage.volume;
}
// Use config if all else fails
@@ -2231,117 +3409,91 @@
}
// Set the player volume
- plyr.media.volume = parseFloat(volume / max);
+ player.elements.media.volume = parseFloat(volume / max);
// Set the display
- if (plyr.volume.display) {
- plyr.volume.display.value = volume;
+ if (player.elements.display.volume) {
+ player.elements.display.volume.value = volume;
}
// Embeds
- if (_inArray(config.types.embed, plyr.type)) {
- switch(plyr.type) {
+ if (inArray(config.types.embed, player.type)) {
+ switch (player.type) {
case 'youtube':
- plyr.embed.setVolume(plyr.media.volume * 100);
+ player.embed.setVolume(player.elements.media.volume * 100);
break;
case 'vimeo':
case 'soundcloud':
- plyr.embed.setVolume(plyr.media.volume);
+ player.embed.setVolume(player.elements.media.volume);
break;
}
// Trigger volumechange for embeds
- _triggerEvent(plyr.media, 'volumechange');
+ trigger(player.elements.media, 'volumechange');
}
// Toggle muted state
if (volume === 0) {
- plyr.media.muted = true;
- } else if (plyr.media.muted && volume > 0) {
- _toggleMute();
+ player.elements.media.muted = true;
+ } else if (player.elements.media.muted && volume > 0) {
+ toggleMute();
}
}
// Increase volume
- function _increaseVolume(step) {
- var volume = plyr.media.muted ? 0 : (plyr.media.volume * config.volumeMax);
+ function increaseVolume(step) {
+ var volume = player.elements.media.muted ? 0 : (player.elements.media.volume * 10);
- if (!_is.number(step)) {
- step = config.volumeStep;
+ if (!is.number(step)) {
+ step = 1;
}
- _setVolume(volume + step);
+ setVolume(volume + step);
}
// Decrease volume
- function _decreaseVolume(step) {
- var volume = plyr.media.muted ? 0 : (plyr.media.volume * config.volumeMax);
+ function decreaseVolume(step) {
+ var volume = player.elements.media.muted ? 0 : (player.elements.media.volume * 10);
- if (!_is.number(step)) {
- step = config.volumeStep;
+ if (!is.number(step)) {
+ step = 1;
}
- _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);
+ var volume = player.elements.media.muted ? 0 : (player.elements.media.volume * 10);
// Update the <input type="range"> if present
- if (plyr.supported.full) {
- if (plyr.volume.input) {
- plyr.volume.input.value = volume;
+ if (player.supported.full) {
+ if (player.elements.inputs.volume) {
+ player.elements.inputs.volume.value = volume;
}
- if (plyr.volume.display) {
- plyr.volume.display.value = volume;
+ if (player.elements.display.volume) {
+ player.elements.display.volume.value = volume;
}
}
// Update the volume in storage
- _updateStorage({volume: volume});
+ updateStorage({
+ volume: volume
+ });
// Toggle class if muted
- _toggleClass(plyr.container, config.classes.muted, (volume === 0));
+ toggleClass(player.elements.container, config.classes.muted, (volume === 0));
// Update checkbox for mute state
- if (plyr.supported.full && plyr.buttons.mute) {
- _toggleState(plyr.buttons.mute, (volume === 0));
- }
- }
-
- // Toggle captions
- function _toggleCaptions(show) {
- // If there's no full support, or there's no caption toggle
- if (!plyr.supported.full || !plyr.buttons.captions) {
- return;
+ if (player.supported.full && player.elements.buttons.mute) {
+ toggleState(player.elements.buttons.mute, (volume === 0));
}
-
- // If the method is called without parameter, toggle based on current value
- if (!_is.boolean(show)) {
- show = (plyr.container.className.indexOf(config.classes.captions.active) === -1);
- }
-
- // Set global
- plyr.captionsEnabled = show;
-
- // Toggle state
- _toggleState(plyr.buttons.captions, plyr.captionsEnabled);
-
- // Add class hook
- _toggleClass(plyr.container, config.classes.captions.active, plyr.captionsEnabled);
-
- // Trigger an event
- _triggerEvent(plyr.container, plyr.captionsEnabled ? 'captionsenabled' : 'captionsdisabled', true);
-
- // Save captions state to localStorage
- _updateStorage({captionsEnabled: plyr.captionsEnabled});
}
// Check if media is loading
- function _checkLoading(event) {
+ function checkLoading(event) {
var loading = (event.type === 'waiting');
// Clear timer
@@ -2350,52 +3502,52 @@
// Timer to prevent flicker when seeking
timers.loading = setTimeout(function() {
// Toggle container class hook
- _toggleClass(plyr.container, config.classes.loading, loading);
+ toggleClass(player.elements.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) {
- if (!plyr.supported.full) {
+ function updateProgress(event) {
+ if (!player.supported.full) {
return;
}
- var progress = plyr.progress.played,
- value = 0,
- duration = _getDuration();
+ var progress = player.elements.display.played,
+ value = 0,
+ duration = getDuration();
if (event) {
switch (event.type) {
// Video playing
case 'timeupdate':
case 'seeking':
- if (plyr.controls.pressed) {
+ if (player.elements.controls.pressed) {
return;
}
- value = _getPercentage(plyr.media.currentTime, duration);
+ value = getPercentage(player.elements.media.currentTime, duration);
// Set seek range value only if it's a 'natural' time event
- if (event.type === 'timeupdate' && plyr.buttons.seek) {
- plyr.buttons.seek.value = value;
+ if (event.type === 'timeupdate' && player.elements.inputs.seek) {
+ player.elements.inputs.seek.value = value;
}
break;
- // Check buffer status
+ // Check buffer status
case 'playing':
case 'progress':
- progress = plyr.progress.buffer;
- value = (function() {
- var buffered = plyr.media.buffered;
+ progress = player.elements.display.buffer;
+ value = (function() {
+ var buffered = player.elements.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);
}
@@ -2407,45 +3559,46 @@
}
}
- // Set values
- _setProgress(progress, value);
+ if (is.number(config.loop.start) && is.number(config.loop.end) && player.elements.media.currentTime >= config.loop.end) {
+ seek(config.loop.start);
+ }
+
+ setProgress(progress, value);
}
// Set <progress> value
- function _setProgress(progress, value) {
- if (!plyr.supported.full) {
+ function setProgress(progress, value) {
+ if (!player.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 (plyr.progress && plyr.progress.buffer) {
- progress = plyr.progress.buffer;
+ if (is.undefined(progress)) {
+ if (is.htmlElement(player.elements.display.buffer)) {
+ progress = player.elements.display.buffer;
} else {
return;
}
}
- // One progress element passed
- if (_is.htmlElement(progress)) {
+ // Update value and label
+ if (is.htmlElement(progress)) {
progress.value = value;
- } else if (progress) {
- // Object of progress + text element
- if (progress.bar) {
- progress.bar.value = value;
- }
- if (progress.text) {
- progress.text.innerHTML = value;
+
+ // Update text label inside
+ var label = progress.getElementsByTagName('span')[0];
+ if (is.htmlElement(label)) {
+ label.childNodes[0].nodeValue = value;
}
}
}
// Update the displayed time
- function _updateTimeDisplay(time, element) {
+ function updateTimeDisplay(time, element) {
// Bail if there's no duration display
if (!element) {
return;
@@ -2456,102 +3609,108 @@
time = 0;
}
- plyr.secs = parseInt(time % 60);
- plyr.mins = parseInt((time / 60) % 60);
- plyr.hours = parseInt(((time / 60) / 60) % 60);
+ var secs = parseInt(time % 60);
+ var mins = parseInt((time / 60) % 60);
+ var 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);
+ secs = ('0' + secs).slice(-2);
+ mins = ('0' + mins).slice(-2);
+
+ // Generate display
+ var display = (displayHours ? hours + ':' : '') + mins + ':' + secs;
// Render
- element.innerHTML = (displayHours ? plyr.hours + ':' : '') + plyr.mins + ':' + plyr.secs;
+ element.textContent = display;
+
+ // Return for looping
+ return display;
}
// Show the duration on metadataloaded
- function _displayDuration() {
- if (!plyr.supported.full) {
+ function displayDuration() {
+ if (!player.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);
+ if (!player.elements.display.duration && config.displayDuration && player.elements.media.paused) {
+ updateTimeDisplay(duration, player.elements.display.currentTime);
}
// If there's a duration element, update content
- if (plyr.duration) {
- _updateTimeDisplay(duration, plyr.duration);
+ if (player.elements.display.duration) {
+ updateTimeDisplay(duration, player.elements.display.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(player.elements.media.currentTime, player.elements.display.currentTime);
// Ignore updates while seeking
- if (event && event.type === 'timeupdate' && plyr.media.seeking) {
+ if (event && event.type === 'timeupdate' && player.elements.media.seeking) {
return;
}
// 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) {
- plyr.progress.played.value = value;
+ if (player.elements.progress && player.elements.display.played) {
+ player.elements.display.played.value = value;
}
// Update seek range input
- if (plyr.buttons && plyr.buttons.seek) {
- plyr.buttons.seek.value = value;
+ if (player.elements.buttons && player.elements.inputs.seek) {
+ player.elements.inputs.seek.value = value;
}
}
// 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) {
+ if (!config.tooltips.seek || !is.htmlElement(player.elements.inputs.seek) || !is.htmlElement(player.elements.display.seekTooltip) || duration === 0) {
return;
}
// Calculate percentage
- var clientRect = plyr.progress.container.getBoundingClientRect(),
- percent = 0,
- visible = config.classes.tooltip + '--visible';
+ var clientRect = player.elements.inputs.seek.getBoundingClientRect();
+ var percent = 0;
+ var visible = config.classes.tooltip + '--visible';
// Determine percentage, if already visible
- if (!event) {
- if (_hasClass(plyr.progress.tooltip, visible)) {
- percent = plyr.progress.tooltip.style.left.replace('%', '');
+ if (is.event(event)) {
+ percent = ((100 / clientRect.width) * (event.pageX - clientRect.left));
+ } else {
+ if (hasClass(player.elements.display.seekTooltip, visible)) {
+ percent = player.elements.display.seekTooltip.style.left.replace('%', '');
} else {
return;
}
- } else {
- percent = ((100 / clientRect.width) * (event.pageX - clientRect.left));
}
// Set bounds
@@ -2562,41 +3721,41 @@
}
// Display the time a click would seek to
- _updateTimeDisplay(((duration / 100) * percent), plyr.progress.tooltip);
+ updateTimeDisplay(((duration / 100) * percent), player.elements.display.seekTooltip);
// Set position
- plyr.progress.tooltip.style.left = percent + "%";
+ player.elements.display.seekTooltip.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 (is.event(event) && inArray(['mouseenter', 'mouseleave'], event.type)) {
+ toggleClass(player.elements.display.seekTooltip, 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') {
+ if (!config.hideControls || player.type === 'audio') {
return;
}
- var delay = 0,
- isEnterFullscreen = false,
- show = toggle,
- loading = _hasClass(plyr.container, config.classes.loading);
+ var delay = 0;
+ var isEnterFullscreen = false;
+ var show = toggle;
+ var loading = hasClass(player.elements.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;
}
@@ -2605,7 +3764,7 @@
delay = 3000;
}
} else {
- show = _hasClass(plyr.container, config.classes.hideControls);
+ show = hasClass(player.elements.container, config.classes.hideControls);
}
}
@@ -2613,63 +3772,63 @@
window.clearTimeout(timers.hover);
// 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);
+ if (show || player.elements.media.paused || loading) {
+ toggleClass(player.elements.container, config.classes.hideControls, false);
// Always show controls when paused or if touch
- if (plyr.media.paused || loading) {
+ if (player.elements.media.paused || loading) {
return;
}
// Delay for hiding on touch
- if (plyr.browser.isTouch) {
+ if (player.browser.isTouch) {
delay = 3000;
}
}
// If toggle is false or if we're playing (regardless of toggle),
// then set the timer to hide the controls
- if (!show || !plyr.media.paused) {
+ if (!show || !player.elements.media.paused) {
timers.hover = window.setTimeout(function() {
// If the mouse is over the controls (and not entering fullscreen), bail
- if ((plyr.controls.pressed || plyr.controls.hover) && !isEnterFullscreen) {
+ if ((player.elements.controls.pressed || player.elements.controls.hover) && !isEnterFullscreen) {
return;
}
- _toggleClass(plyr.container, config.classes.hideControls, true);
+ toggleClass(player.elements.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 (player.type) {
case 'youtube':
- url = plyr.embed.getVideoUrl();
+ url = player.embed.getVideoUrl();
break;
case 'vimeo':
- plyr.embed.getVideoUrl.then(function (value) {
+ player.embed.getVideoUrl.then(function(value) {
url = value;
});
break;
case 'soundcloud':
- plyr.embed.getCurrentSound(function(object) {
+ player.embed.getCurrentSound(function(object) {
url = object.permalink_url;
});
break;
default:
- url = plyr.media.currentSrc;
+ url = player.elements.media.currentSrc;
break;
}
@@ -2678,202 +3837,182 @@
// 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(player.elements.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() {
// Remove embed object
- plyr.embed = null;
+ player.embed = null;
// Remove the old media
- _remove(plyr.media);
+ remove(player.elements.media);
// Remove video container
- if (plyr.type === 'video' && plyr.videoContainer) {
- _remove(plyr.videoContainer);
+ if (player.type === 'video' && player.elements.wrapper) {
+ remove(player.elements.wrapper);
}
// Reset class name
- if (plyr.container) {
- plyr.container.removeAttribute('class');
+ if (player.elements.container) {
+ player.elements.container.removeAttribute('class');
}
// Set the type
if ('type' in source) {
- plyr.type = source.type;
+ player.type = source.type;
// Get child type for video (it might be an embed)
- if (plyr.type === 'video') {
+ if (player.type === 'video') {
var firstSource = source.sources[0];
- if ('type' in firstSource && _inArray(config.types.embed, firstSource.type)) {
- plyr.type = firstSource.type;
+ if ('type' in firstSource && inArray(config.types.embed, firstSource.type)) {
+ player.type = firstSource.type;
}
}
}
// Check for support
- plyr.supported = supported(plyr.type);
+ player.supported = checkSupport(player.type);
// Create new markup
- switch(plyr.type) {
+ switch (player.type) {
case 'video':
- plyr.media = document.createElement('video');
+ player.elements.media = createElement('video');
break;
case 'audio':
- plyr.media = document.createElement('audio');
+ player.elements.media = createElement('audio');
break;
case 'youtube':
case 'vimeo':
case 'soundcloud':
- plyr.media = document.createElement('div');
- plyr.embedId = source.sources[0].src;
+ player.elements.media = createElement('div');
+ player.embedId = source.sources[0].src;
break;
}
// Inject the new element
- _prependChild(plyr.container, plyr.media);
+ prependChild(player.elements.container, player.elements.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, player.type)) {
if (config.crossorigin) {
- plyr.media.setAttribute('crossorigin', '');
+ player.elements.media.setAttribute('crossorigin', '');
}
if (config.autoplay) {
- plyr.media.setAttribute('autoplay', '');
+ player.elements.media.setAttribute('autoplay', '');
}
if ('poster' in source) {
- plyr.media.setAttribute('poster', source.poster);
+ player.elements.media.setAttribute('poster', source.poster);
}
- if (config.loop) {
- plyr.media.setAttribute('loop', '');
+ if (config.loop.active) {
+ player.elements.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(player.elements.container, config.classes.fullscreen.active, player.fullscreen.active);
+ toggleClass(player.elements.container, config.classes.captions.active, player.captions.enabled);
+ toggleStyleHook();
// Set new sources for html5
- if (_inArray(config.types.html5, plyr.type)) {
- _insertChildElements('source', source.sources);
+ if (inArray(config.types.html5, player.type)) {
+ insertElements('source', source.sources);
}
// Set up from scratch
- _setupMedia();
+ setupMedia();
// HTML5 stuff
- if (_inArray(config.types.html5, plyr.type)) {
+ if (inArray(config.types.html5, player.type)) {
// Setup captions
if ('tracks' in source) {
- _insertChildElements('track', source.tracks);
+ insertElements('track', source.tracks);
}
// Load HTML5 sources
- plyr.media.load();
+ player.elements.media.load();
}
// 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, player.type) || (inArray(config.types.embed, player.type) && !player.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) {
- if (plyr.type === 'video') {
- plyr.media.setAttribute('poster', source);
+ function updatePoster(source) {
+ if (player.type === 'video') {
+ player.elements.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');
+ var inputEvent = (player.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'],
- target = plyr.buttons[play ? 'pause' : 'play'];
-
- // Get the last play button to account for the large play button
- if (target && target.length > 1) {
- target = target[target.length - 1];
- } else {
- target = target[0];
- }
+ var trigger = player.elements.buttons[play ? 'play' : 'pause'];
+ var target = player.elements.buttons[play ? 'pause' : 'play'];
// 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);
}
}
- // Get the focused element
- function getFocusElement() {
- var focused = document.activeElement;
-
- if (!focused || focused === document.body) {
- focused = null;
- } else {
- focused = document.querySelector(':focus');
- }
-
- return focused;
- }
-
// Get the key code for an event
function getKeyCode(event) {
return event.keyCode ? event.keyCode : event.which;
@@ -2881,84 +4020,92 @@
// Detect tab focus
function checkTabFocus(focused) {
- for (var button in plyr.buttons) {
- var element = plyr.buttons[button];
+ for (var button in player.elements.buttons) {
+ var element = player.elements.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(player.elements.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;
}
// Seek by the number keys
function seekByKey() {
// Get current duration
- var duration = plyr.media.duration;
+ var duration = player.elements.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:
@@ -2968,30 +4115,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 && player.fullscreen.active && code === 27) {
+ toggleFullscreen();
}
// Store last code for next cycle
@@ -3002,157 +4196,247 @@
}
// Focus/tab management
- _on(window, 'keyup', function(event) {
- var code = getKeyCode(event),
- focused = getFocusElement();
+ on(window, 'keyup', function(event) {
+ var code = getKeyCode(event);
+ var focused = getFocusElement();
if (code === 9) {
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];
+ for (var button in player.elements.buttons) {
+ var element = player.elements.buttons[button];
- _on(element, 'blur', function() {
- _toggleClass(element, 'tab-focus', false);
+ on(element, 'blur', function() {
+ toggleClass(element, 'tab-focus', false);
});
}
+ // Trigger custom and default handlers
+ var handlerProxy = function(event, customHandler, defaultHandler) {
+ if (is.function(customHandler)) {
+ customHandler.call(this, event);
+ }
+ if (is.function(defaultHandler)) {
+ defaultHandler.call(this, event);
+ }
+ }
+
// Play
- _proxyListener(plyr.buttons.play, 'click', config.listeners.play, togglePlay);
+ proxy(player.elements.buttons.play, 'click', config.listeners.play, _togglePlay);
+ proxy(player.elements.buttons.playLarge, 'click', config.listeners.play, _togglePlay);
// Pause
- _proxyListener(plyr.buttons.pause, 'click', config.listeners.pause, togglePlay);
+ proxy(player.elements.buttons.pause, 'click', config.listeners.pause, _togglePlay);
- // Restart
- _proxyListener(plyr.buttons.restart, 'click', config.listeners.restart, _seek);
+ // Pause
+ proxy(player.elements.buttons.restart, 'click', config.listeners.restart, seek);
// Rewind
- _proxyListener(plyr.buttons.rewind, 'click', config.listeners.rewind, _rewind);
+ proxy(player.elements.buttons.rewind, 'click', config.listeners.rewind, rewind);
- // Fast forward
- _proxyListener(plyr.buttons.forward, 'click', config.listeners.forward, _forward);
+ // Rewind
+ proxy(player.elements.buttons.forward, 'click', config.listeners.forward, forward);
- // Seek
- _proxyListener(plyr.buttons.seek, inputEvent, config.listeners.seek, _seek);
+ // Mute
+ proxy(player.elements.buttons.mute, 'click', config.listeners.mute, toggleMute);
- // Set volume
- _proxyListener(plyr.volume.input, inputEvent, config.listeners.volume, function() {
- _setVolume(plyr.volume.input.value);
+ // Captions
+ proxy(player.elements.buttons.captions, 'click', config.listeners.captions, toggleCaptions);
+
+ // Fullscreen
+ proxy(player.elements.buttons.fullscreen, 'click', config.listeners.fullscreen, toggleFullscreen);
+
+ // Picture-in-Picture
+ proxy(player.elements.buttons.pip, 'click', config.listeners.pip, function(event) {
+ if (!support.pip) {
+ return;
+ }
+ player.elements.media.webkitSetPresentationMode(player.elements.media.webkitPresentationMode === 'picture-in-picture' ? 'inline' : 'picture-in-picture');
});
- // Mute
- _proxyListener(plyr.buttons.mute, 'click', config.listeners.mute, _toggleMute);
+ // Airplay
+ proxy(player.elements.buttons.airplay, 'click', config.listeners.airplay, function(event) {
+ if (!support.airplay) {
+ return;
+ }
+ player.elements.media.webkitShowPlaybackTargetPicker();
+ });
- // Fullscreen
- _proxyListener(plyr.buttons.fullscreen, 'click', config.listeners.fullscreen, _toggleFullscreen);
+ // Settings menu
+ on(player.elements.settings.menu, 'click', toggleMenu);
- // Handle user exiting fullscreen by escaping etc
- if (fullscreen.supportsFullScreen) {
- _on(document, fullscreen.fullScreenEventName, _toggleFullscreen);
- }
+ // Click anywhere closes menu
+ on(document.body, 'click', function(event) {
+ var menu = player.elements.settings.menu;
+ var form = menu.querySelector('form');
- // Captions
- _proxyListener(plyr.buttons.captions, 'click', config.listeners.captions, _toggleCaptions);
+ if (form.getAttribute('aria-hidden') === 'true' || menu.contains(event.target)) {
+ return;
+ }
+
+ // TODO: This should call some sort of menuToggle function?
+ form.setAttribute('aria-hidden', true);
+ });
+
+ // Settings menu items - use event delegation as items are added/removed
+ on(player.elements.settings.menu, 'click', function(event) {
+ // Settings - Speed
+ if (matches(event.target, config.selectors.buttons.speed)) {
+ handlerProxy.call(this, event, config.listeners.speed, function() {
+ //var speedValue = document.querySelector('[data-plyr="speed"]:checked').value;
+ //setSpeed(Number(speedValue));
+ console.warn("Set speed");
+ });
+ }
+
+ // Settings - Quality
+ else if (matches(event.target, config.selectors.buttons.quality)) {
+ handlerProxy.call(this, event, config.listeners.quality, function() {
+ console.warn("Set quality");
+ });
+ }
+
+ // Settings - Looping
+ else if (matches(event.target, config.selectors.buttons.loop)) {
+ handlerProxy.call(this, event, config.listeners.loop, function() {
+ // TODO: This should be done in the method itself I think
+ var value = event.target.getAttribute('data-loop__value') || event.target.getAttribute('data-loop__type');
+
+ if (inArray(['start', 'end', 'all', 'none'], value)) {
+ toggleLoop(value);
+ }
+ });
+ }
+
+ // Settings - Language
+ else if (matches(event.target, config.selectors.buttons.language)) {
+ handlerProxy.call(this, event, config.listeners.language, function(event) {
+ // TODO: This should be done in the method itself I think
+ var index = event.target.attributes.getNamedItem("data-index").value;
+ setCaptionIndex(index);
+ });
+ }
+ });
+
+ // Seek
+ proxy(player.elements.inputs.seek, inputEvent, config.listeners.seek, seek);
+
+ // Seek
+ proxy(player.elements.inputs.volume, inputEvent, config.listeners.volume, setVolume);
// Seek tooltip
- _on(plyr.progress.container, 'mouseenter mouseleave mousemove', _updateSeekTooltip);
+ on(player.elements.progress, '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(player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen', toggleControls);
// Watch for cursor over controls so they don't hide when trying to interact
- _on(plyr.controls, 'mouseenter mouseleave', function(event) {
- plyr.controls.hover = event.type === 'mouseenter';
+ on(player.elements.controls, 'mouseenter mouseleave', function(event) {
+ player.elements.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(player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function(event) {
+ player.elements.controls.pressed = inArray(['mousedown', 'touchstart'], event.type);
});
// Focus in/out on controls
- _on(plyr.controls, 'focus blur', _toggleControls, true);
+ on(player.elements.controls, 'focus blur', toggleControls, true);
}
- // Adjust volume on scroll
- _on(plyr.volume.input, 'wheel', function(event) {
- event.preventDefault();
-
+ // Mouse wheel for volume
+ proxy(player.elements.inputs.volume, 'wheel', config.listeners.volume, function(event) {
// Detect "natural" scroll - suppored on OS X Safari only
// Other browsers on OS X will be inverted until support improves
- var inverted = event.webkitDirectionInvertedFromDevice,
- step = (config.volumeStep / 5);
+ var inverted = event.webkitDirectionInvertedFromDevice;
+ var step = (1 / 5);
+ var direction = 0;
// Scroll down (or up on natural) to decrease
if (event.deltaY < 0 || event.deltaX > 0) {
if (inverted) {
- _decreaseVolume(step);
+ decreaseVolume(step);
+ direction = -1;
} else {
- _increaseVolume(step);
+ increaseVolume(step);
+ direction = 1;
}
}
// Scroll up (or down on natural) to increase
if (event.deltaY > 0 || event.deltaX < 0) {
if (inverted) {
- _increaseVolume(step);
+ increaseVolume(step);
+ direction = 1;
} else {
- _decreaseVolume(step);
+ decreaseVolume(step);
+ direction = -1;
}
}
+
+ // Don't break page scrolling at max and min
+ if ((direction === 1 && player.elements.media.volume < 1) ||
+ (direction === -1 && player.elements.media.volume > 0)) {
+ event.preventDefault();
+ }
});
+
+ // Handle user exiting fullscreen by escaping etc
+ if (support.fullscreen) {
+ on(document, fullscreen.eventType, toggleFullscreen);
+ }
}
// Listen for media events
- function _mediaListeners() {
+ function mediaListeners() {
// Time change on media
- _on(plyr.media, 'timeupdate seeking', _timeUpdate);
+ on(player.elements.media, 'timeupdate seeking', timeUpdate);
// Update manual captions
- _on(plyr.media, 'timeupdate', _seekManualCaptions);
+ on(player.elements.media, 'timeupdate', seekManualCaptions);
// Display duration
- _on(plyr.media, 'durationchange loadedmetadata', _displayDuration);
+ on(player.elements.media, 'durationchange loadedmetadata', displayDuration);
// Handle the media finishing
- _on(plyr.media, 'ended', function() {
+ on(player.elements.media, 'ended', function() {
// Show poster on end
- if (plyr.type === 'video' && config.showPosterOnEnd) {
+ if (player.type === 'video' && config.showPosterOnEnd) {
// Clear
- if (plyr.type === 'video') {
- _setCaption();
+ if (player.type === 'video') {
+ setCaption();
}
// Restart
- _seek();
+ seek();
// Re-load media
- plyr.media.load();
+ player.elements.media.load();
}
});
// Check for buffer progress
- _on(plyr.media, 'progress playing', _updateProgress);
+ on(player.elements.media, 'progress playing', updateProgress);
// Handle native mute
- _on(plyr.media, 'volumechange', _updateVolume);
+ on(player.elements.media, 'volumechange', updateVolume);
// Handle native play/pause
- _on(plyr.media, 'play pause ended', _checkPlaying);
+ on(player.elements.media, 'play pause ended', checkPlaying);
// Loading
- _on(plyr.media, 'waiting canplay seeked', _checkLoading);
+ on(player.elements.media, 'waiting canplay seeked', checkLoading);
// Click video
- if (config.clickToPlay && plyr.type !== 'audio') {
+ if (config.clickToPlay && player.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) {
@@ -3163,80 +4447,82 @@
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) {
+ if (config.hideControls && player.browser.isTouch && !player.elements.media.paused) {
return;
}
- if (plyr.media.paused) {
- _play();
- } else if (plyr.media.ended) {
- _seek();
- _play();
+ if (player.elements.media.paused) {
+ play();
+ } else if (player.elements.media.ended) {
+ seek();
+ play();
} else {
- _pause();
+ pause();
}
});
}
// Disable right click
if (config.disableContextMenu) {
- _on(plyr.media, 'contextmenu', function(event) { event.preventDefault(); });
+ on(player.elements.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(player.elements.media, config.events.concat(['keyup', 'keydown']).join(' '), function(event) {
+ trigger(player.elements.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, player.type)) {
return;
}
// Remove child sources
- var sources = plyr.media.querySelectorAll('source');
+ var sources = player.elements.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);
+ player.elements.media.setAttribute('src', 'https://cdn.selz.com/plyr/blank.mp4');
// Load the new empty source
// This will cancel existing requests
// See https://github.com/Selz/plyr/issues/174
- plyr.media.load();
+ player.elements.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) {
+ if (!player.init) {
return null;
}
// Type specific stuff
- switch (plyr.type) {
+ switch (player.type) {
case 'youtube':
// Clear timers
window.clearInterval(timers.buffering);
window.clearInterval(timers.playing);
// Destroy YouTube API
- plyr.embed.destroy();
+ player.embed.destroy();
// Clean up
cleanUp();
@@ -3246,17 +4532,17 @@
case 'vimeo':
// Destroy Vimeo API
// then clean up (wait, to prevent postmessage errors)
- plyr.embed.unload().then(cleanUp);
+ player.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();
@@ -3265,15 +4551,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);
}
@@ -3283,217 +4567,255 @@
}
// Remove init flag
- plyr.init = false;
+ player.init = false;
// Replace the container with the original element provided
- plyr.container.parentNode.replaceChild(original, plyr.container);
+ player.elements.container.parentNode.replaceChild(original, player.elements.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) {
+ if (player.init) {
return null;
}
- // Setup the fullscreen api
- fullscreen = _fullscreen();
-
// Sniff out the browser
- plyr.browser = _browserSniff();
+ player.browser = getBrowser();
// Bail if nothing to setup
- if (!_is.htmlElement(plyr.media)) {
+ if (!is.htmlElement(player.elements.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');
+ player.type = media.getAttribute('data-type');
+ player.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));
+ player.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
- plyr.supported = supported(plyr.type);
+ player.supported = checkSupport(player.type);
// If no native support, bail
- if (!plyr.supported.basic) {
+ if (!player.supported.basic) {
return;
}
// Wrap media
- plyr.container = _wrap(media, document.createElement('div'));
+ player.elements.container = wrap(media, createElement('div'));
// Allow focus to be captured
- plyr.container.setAttribute('tabindex', 0);
+ player.elements.container.setAttribute('tabindex', 0);
// Add style hook
- _toggleStyleHook();
+ toggleStyleHook();
// Debug info
- _log('' + plyr.browser.name + ' ' + plyr.browser.version);
+ log('' + player.browser.name + ' ' + player.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, player.type) || (inArray(config.types.embed, player.type) && !player.supported.full)) {
// Setup UI
- _setupInterface();
+ setupInterface();
// Call ready
- _ready();
+ ready();
// Set title on button and frame
- _setTitle();
+ setTitle();
}
// Successful setup
- plyr.init = true;
+ player.init = true;
}
// Setup the UI
- function _setupInterface() {
+ function setupInterface() {
// Don't setup interface if no support
- if (!plyr.supported.full) {
- _warn('Basic support only', plyr.type);
+ if (!player.supported.full) {
+ warn('Basic support only', player.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;
- if (controlsMissing) {
+ if (!is.htmlElement(getElement(config.selectors.controls.wrapper))) {
// Inject custom controls
- _injectControls();
+ injectControls();
+ controlListeners();
}
// Find the elements
- if (!_findElements()) {
+ // TODO: re-enable when custom HTML is restored
+ /*if (!findElements()) {
return;
- }
-
- // If the controls are injected, re-bind listeners for controls
- if (controlsMissing) {
- _controlListeners();
- }
+ }*/
// Media element listeners
- _mediaListeners();
+ mediaListeners();
// Remove native controls
- _toggleNativeControls();
+ toggleNativeControls();
// Setup fullscreen
- _setupFullscreen();
+ setupFullscreen();
// Captions
- _setupCaptions();
+ setupCaptions();
+ setCaptionIndex();
// 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 player.elements.container
+ },
+ getEmbed: function() {
+ return player.embed;
+ },
+ getMedia: function() {
+ return player.elements.media;
+ },
+ getType: function() {
+ return player.type;
+ },
+ getDuration: getDuration,
+ getCurrentTime: function() {
+ return player.elements.media.currentTime;
+ },
+ getVolume: function() {
+ return player.elements.media.volume;
+ },
+ isMuted: function() {
+ return player.elements.media.muted;
+ },
+ isReady: function() {
+ return hasClass(player.elements.container, config.classes.ready);
+ },
+ isLoading: function() {
+ return hasClass(player.elements.container, config.classes.loading);
+ },
+ isPaused: function() {
+ return player.elements.media.paused;
+ },
+ isLooping: function() {
+ return config.loop.active;
+ },
+ on: function(event, callback) {
+ on(player.elements.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 player.fullscreen.active || false;
+ },
+ support: function(mimeType) {
+ return support.mime(player, mimeType);
+ },
+ destroy: destroy
};
// Everything done
- function _ready() {
+ function ready() {
// Ready event at end of execution stack
window.setTimeout(function() {
- _triggerEvent(plyr.media, 'ready');
+ trigger(player.elements.media, 'ready');
}, 0);
// Set class hook on media element
- _toggleClass(plyr.media, defaults.classes.setup, true);
+ toggleClass(player.elements.media, defaults.classes.setup, true);
// Set container class for ready
- _toggleClass(plyr.container, config.classes.ready, true);
+ toggleClass(player.elements.container, config.classes.ready, true);
// Store a refernce to instance
- plyr.media.plyr = api;
+ player.elements.media.plyr = api;
// Autoplay
if (config.autoplay) {
- _play();
+ play();
}
}
// Initialize instance
- _init();
+ init();
// If init failed, return null
- if (!plyr.init) {
+ if (!player.init) {
return null;
}
@@ -3505,14 +4827,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');
+ var container = 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]);
@@ -3533,79 +4855,63 @@
}
// 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;
+ function checkSupport(type) {
+ var browser = getBrowser();
+ var isOldIE = (browser.isIE && browser.version <= 9);
+ var isIos = browser.isIos;
+ var isIphone = /iPhone|iPod/i.test(navigator.userAgent);
+ var audio = !!createElement('audio').canPlayType;
+ var video = !!createElement('video').canPlayType;
+ var basic;
+ var 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 = [];
+ var instances = [];
+ var 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;
}
@@ -3614,26 +4920,26 @@
}
// Convert NodeList to array
- if (_is.nodeList(targets)) {
+ if (is.nodeList(targets)) {
targets = Array.prototype.slice.call(targets);
}
// Bail if disabled or no basic support
// You may want to disable certain UAs etc
- if (!supported().basic || !targets.length) {
+ if (!checkSupport().basic || !targets.length) {
return false;
}
// 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
});
}
}
@@ -3650,7 +4956,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);
}
@@ -3658,9 +4964,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;
+ var media = player.media;
+ var match = false;
// The target element can also be the media element
if (media === element) {
@@ -3672,10 +4978,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) {
@@ -3686,7 +4993,7 @@
var instance = new Plyr(media, config);
// Go to next if setup failed
- if (!_is.object(instance)) {
+ if (!is.object(instance)) {
return;
}
@@ -3694,13 +5001,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
});
@@ -3713,21 +5020,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);
}
});
@@ -3739,22 +5046,26 @@
}
return {
- setup: setup,
- supported: supported,
+ setup: setup,
+ supported: checkSupport,
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..8e08cf40 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 {
@@ -439,14 +466,173 @@
// Some options are hidden by default
.plyr [data-plyr='captions'],
-.plyr [data-plyr='fullscreen'] {
+.plyr [data-plyr='fullscreen'],
+.plyr [data-plyr='pip'],
+.plyr [data-plyr='airplay'] {
display: none;
}
.plyr--captions-enabled [data-plyr='captions'],
-.plyr--fullscreen-enabled [data-plyr='fullscreen'] {
+.plyr--fullscreen-enabled [data-plyr='fullscreen'],
+.plyr--pip-enabled [data-plyr='pip'],
+.plyr--airplay-enabled [data-plyr='airplay'] {
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;
+ z-index: 1;
+ bottom: 100%;
+ right: -5px;
+ margin-bottom: 10px;
+ animation: plyr-popup .2s ease;
+
+ background: @plyr-menu-bg;
+ 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: @plyr-control-padding;
+ 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;
+ @horizontal-padding: (@plyr-control-padding * 2);
+ width: ~"calc(100% - @{horizontal-padding})";
+ margin: @plyr-control-padding;
+ margin-bottom: floor(@plyr-control-padding / 2);
+ padding-left: ceil(@plyr-control-padding * 4);
+ 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 +649,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 +673,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 +696,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 +708,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..bcc80a02 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, 60%);
@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/scss/variables.scss b/src/scss/variables.scss
index da75f11a..39b147da 100644
--- a/src/scss/variables.scss
+++ b/src/scss/variables.scss
@@ -18,7 +18,7 @@ $plyr-font-size-small: 14px !default;
$plyr-font-size-base: 16px !default;
// Captions
-$plyr-captions-bg: transparentize(#000, .3) !default;
+$plyr-captions-bg: transparentize(#000, .4) !default;
$plyr-captions-color: #fff !default;
$plyr-font-size-captions-base: $plyr-font-size-base !default;
$plyr-font-size-captions-medium: ceil($plyr-font-size-base * 1.5) !default;
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