aboutsummaryrefslogtreecommitdiffstats
path: root/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'src/js')
-rw-r--r--src/js/plyr.js563
1 files changed, 408 insertions, 155 deletions
diff --git a/src/js/plyr.js b/src/js/plyr.js
index 18099312..2ce6cd3b 100644
--- a/src/js/plyr.js
+++ b/src/js/plyr.js
@@ -9,7 +9,7 @@
;(function(root, factory) {
'use strict';
- /*global define,module*/
+ /* global define,module */
if (typeof module === 'object' && typeof module.exports === 'object') {
// Node, CommonJS-like
@@ -25,8 +25,7 @@
'use strict';
// Globals
- var fullscreen,
- scroll = { x: 0, y: 0 },
+ var scroll = { x: 0, y: 0 },
// Default config
defaults = {
@@ -39,6 +38,9 @@
volumeMin: 0,
volumeMax: 10,
volumeStep: 1,
+ defaultSpeed: 1.0,
+ currentSpeed: 1.0,
+ speeds: [ 0.5, 1.0, 1.5, 2.0 ],
duration: null,
displayDuration: true,
loadSprite: true,
@@ -75,7 +77,8 @@
forward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]',
- fullscreen: '[data-plyr="fullscreen"]'
+ fullscreen: '[data-plyr="fullscreen"]',
+ settings: '[data-plyr="settings"]'
},
volume: {
input: '[data-plyr="volume"]',
@@ -114,10 +117,15 @@
enabled: 'plyr--fullscreen-enabled',
active: 'plyr--fullscreen-active'
},
+ pip: {
+ enabled: 'plyr--pip-enabled',
+ active: 'plyr--pip-active'
+ },
tabFocus: 'tab-focus'
},
captions: {
- defaultActive: false
+ defaultActive: false,
+ selectedIndex: 0
},
fullscreen: {
enabled: true,
@@ -128,7 +136,7 @@
enabled: true,
key: 'plyr'
},
- controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'fullscreen'],
+ controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'settings', 'fullscreen'],
i18n: {
restart: 'Restart',
rewind: 'Rewind {seektime} secs',
@@ -143,7 +151,11 @@
toggleMute: 'Toggle Mute',
toggleCaptions: 'Toggle Captions',
toggleFullscreen: 'Toggle Fullscreen',
- frameTitle: 'Player for {title}'
+ frameTitle: 'Player for {title}',
+ captions: 'Captions',
+ settings: 'Settings',
+ speed: 'Speed',
+ quality: 'Quality'
},
types: {
embed: ['youtube', 'vimeo', 'soundcloud'],
@@ -172,7 +184,8 @@
mute: null,
volume: null,
captions: null,
- fullscreen: null
+ fullscreen: null,
+ speed: 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', 'emptied'],
@@ -182,7 +195,7 @@
// Credits: http://paypal.github.io/accessible-html5-video-player/
// Unfortunately, due to mixed support, UA sniffing is required
- function _browserSniff() {
+ function _getBrowser() {
var ua = navigator.userAgent,
name = navigator.appName,
fullVersion = '' + parseFloat(navigator.appVersion),
@@ -261,32 +274,6 @@
};
}
- // 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) {
if (document.querySelectorAll('script[src="' + source + '"]').length) {
@@ -343,21 +330,6 @@
}
}
- // 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) {
if (!element) {
@@ -441,7 +413,7 @@
}
// 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
@@ -451,10 +423,10 @@
}
// 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;
@@ -462,23 +434,23 @@
// 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
+ // Bind event handler
function _on(element, events, callback, useCapture) {
- if (element) {
+ if (_is.htmlElement(element)) {
_toggleListener(element, events, callback, true, useCapture);
}
}
- // Unbind event
- /*function _off(element, events, callback, useCapture) {
- if (element) {
+ // Unbind event handler
+ function _off(element, events, callback, useCapture) {
+ if (_is.htmlElement(element)) {
_toggleListener(element, events, callback, false, useCapture);
}
- }*/
+ }
// Trigger event
function _event(element, type, bubbles, properties) {
@@ -568,10 +540,10 @@
// Check variable types
var _is = {
object: function(input) {
- return input !== null && typeof(input) === 'object';
+ return input !== null && typeof(input) === 'object' && input.constructor === Object;
},
array: function(input) {
- return input !== null && (typeof(input) === 'object' && input.constructor === Array);
+ return input !== null && typeof(input) === 'object' && input.constructor === Array;
},
number: function(input) {
return input !== null && (typeof(input) === 'number' && !isNaN(input - 0) || (typeof input === 'object' && input.constructor === Number));
@@ -591,51 +563,50 @@
function: function(input) {
return input !== null && typeof input === 'function';
},
+ event: function(input) {
+ return input !== null && typeof input === 'object' && (input.constructor === Event || input.constructor === CustomEvent);
+ },
undefined: function(input) {
return input !== null && typeof input === 'undefined';
+ },
+ empty: function(input) {
+ return input === null || this.undefined(input) || ((this.string(input) || this.array(input) || this.nodeList(input)) && input.length === 0) || (this.object(input) && Object.keys(input).length === 0);
}
};
// Fullscreen API
- function _fullscreen() {
- var fullscreen = {
- supportsFullScreen: false,
- isFullScreen: function() { return false; },
- requestFullScreen: function() {},
- cancelFullScreen: function() {},
- fullScreenEventName: '',
- element: null,
- prefix: ''
- },
- browserPrefixes = 'webkit o moz ms khtml'.split(' ');
-
- // Check for native support
- if (!_is.undefined(document.cancelFullScreen)) {
- fullscreen.supportsFullScreen = true;
- } else {
- // Check for fullscreen support by vendor prefix
- for (var i = 0, il = browserPrefixes.length; i < il; i++ ) {
- fullscreen.prefix = browserPrefixes[i];
-
- if (!_is.undefined(document[fullscreen.prefix + 'CancelFullScreen'])) {
- fullscreen.supportsFullScreen = true;
- break;
- } else if (!_is.undefined(document.msExitFullscreen) && document.msFullscreenEnabled) {
- // Special case for MS (when isn't it?)
- fullscreen.prefix = 'ms';
- fullscreen.supportsFullScreen = true;
- break;
- }
+ var _fullscreen;
+ (function() {
+ // Determine the prefix
+ var prefix = (function() {
+ if (!_is.undefined(document.cancelFullScreen)) {
+ return '';
+ } else {
+ // Check for fullscreen support by vendor prefix
+ ['webkit', 'o', 'moz', 'ms', 'khtml'].forEach(function(prefix) {
+ if (!_is.undefined(document[prefix + 'CancelFullScreen'])) {
+ return prefix;
+ } else if (!_is.undefined(document.msExitFullscreen) && document.msFullscreenEnabled) {
+ // Special case for MS (when isn't it?)
+ return 'ms';
+ }
+ });
}
- }
- // Update methods to do something useful
- if (fullscreen.supportsFullScreen) {
+ // If we got this far, there's no support
+ return false;
+ })();
+
+ _fullscreen = {
// 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) {
+ // Is an element fullscreen
+ isFullScreen: function(element) {
+ if (!_support.fullscreen) {
+ return false;
+ }
if (_is.undefined(element)) {
element = document.body;
}
@@ -645,29 +616,40 @@
case 'moz':
return document.mozFullScreenElement === element;
default:
- return document[this.prefix + 'FullscreenElement'] === element;
+ return document[prefix + 'FullscreenElement'] === element;
+ }
+ },
+ requestFullScreen: function(element) {
+ if (!_support.fullscreen) {
+ return false;
}
- };
- fullscreen.requestFullScreen = function(element) {
if (_is.undefined(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;
- }
+ 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;
}
@@ -692,7 +674,37 @@
}
return false;
- })()
+ })(),
+ // Picture-in-picture support
+ // Safari only currently
+ pip: function(plyr) {
+ return _is.function(plyr.media.webkitSetPresentationMode);
+ },
+ // Check for mime type support against a player instance
+ // Credits: http://diveintohtml5.info/everything.html
+ // Related: http://www.leanbackplyr.com/test/h5mt.html
+ mime: function(plyr, type) {
+ var media = plyr.media;
+
+ if (plyr.type === 'video') {
+ // Check type
+ switch (type) {
+ 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 (type) {
+ 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;
+ }
};
// Player instance
@@ -806,8 +818,8 @@
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">',
+ '<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,
@@ -857,8 +869,8 @@
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">',
+ '<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>'
);
@@ -875,6 +887,98 @@
);
}
+ // Settings button / menu
+ if (_inArray(config.controls, 'settings')) {
+ html.push(
+ '<div class="plyr__menu" data-plyr="settings">',
+ '<button type="button" id="plyr-settings-toggle-{id}" 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>',
+ '<div class="plyr__menu__container" id="plyr-settings-{id}" aria-hidden="true" aria-labelled-by="plyr-settings-toggle-{id}" role="tablist" tabindex="-1">',
+ '<div>',
+ '<div class="plyr__menu__primary" id="plyr-settings-{id}-primary" aria-hidden="false" aria-labelled-by="plyr-settings-toggle-{id}" role="tabpanel" tabindex="-1">',
+ '<ul>',
+ '<li role="tab">',
+ '<button type="button" class="plyr__menu__btn plyr__menu__btn--forward" id="plyr-settings-{id}-captions-toggle" aria-haspopup="true" aria-controls="plyr-settings-{id}-captions" aria-expanded="false">',
+ config.i18n.captions + ' <span class="plyr__menu__btn__value">{lang}</span>',
+ '</button>',
+ '</li>',
+ '<li role="tab">',
+ '<button type="button" class="plyr__menu__btn plyr__menu__btn--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__btn__value">{speed}</span>',
+ '</button>',
+ '</li>',
+ '<li role="tab">',
+ '<button type="button" class="plyr__menu__btn plyr__menu__btn--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__btn__value">Auto</span>',
+ '</button>',
+ '</li>',
+ '</ul>',
+ '</div>',
+ '<div class="plyr__menu__secondary" 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__menu__btn plyr__menu__btn--back" aria-haspopup="true" aria-controls="plyr-settings-{id}-primary" aria-expanded="false">',
+ config.i18n.captions,
+ '</button>',
+ '</li>',
+ '<li>',
+ '<button type="button">English</button>',
+ '</li>',
+ '<li>',
+ '<button type="button">Off</button>',
+ '</li>',
+ '</ul>',
+ '</div>',
+ '<div class="plyr__menu__secondary" 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__menu__btn plyr__menu__btn--back" aria-haspopup="true" aria-controls="plyr-settings-{id}-primary" aria-expanded="false">',
+ config.i18n.speed,
+ '</button>',
+ '</li>',
+ '<li>',
+ '<button type="button">2&times;</button>',
+ '</li>',
+ '<li>',
+ '<button type="button">1.5&times;</button>',
+ '</li>',
+ '<li>',
+ '<button type="button">1&times;</button>',
+ '</li>',
+ '<li>',
+ '<button type="button">0.5&times;</button>',
+ '</li>',
+ '</ul>',
+ '</div>',
+ '<div class="plyr__menu__secondary" 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__menu__btn plyr__menu__btn--back" aria-haspopup="true" aria-controls="plyr-settings-{id}-primary" aria-expanded="false">',
+ config.i18n.quality,
+ '</button>',
+ '</li>',
+ '<li>',
+ '<button type="button">1080P <span class="plyr__menu__btn__badge"><span>HD</span></span></button>',
+ '</li>',
+ '<li>',
+ '<button type="button">720P <span class="plyr__menu__btn__badge"><span>HD</span></span></button>',
+ '</li>',
+ '<li>',
+ '<button type="button">480P</button>',
+ '</li>',
+ '<li>',
+ '<button type="button">320P</button>',
+ '</li>',
+ '</ul>',
+ '</div>',
+ '</div>',
+ '</div>',
+ '</div>'
+ );
+ }
+
// Toggle fullscreen button
if (_inArray(config.controls, 'fullscreen')) {
html.push(
@@ -900,7 +1004,7 @@
if ((plyr.type !== 'audio' || config.fullscreen.allowAudio) && config.fullscreen.enabled) {
// Check for native support
- var nativeSupport = fullscreen.supportsFullScreen;
+ var nativeSupport = _support.fullscreen;
if (nativeSupport || (config.fullscreen.fallback && !_inFrame())) {
_log((nativeSupport ? 'Native' : 'Fallback') + ' fullscreen enabled');
@@ -921,6 +1025,25 @@
}
}
+ // Caption cue change helper event
+ /*function _captionCueChange() {
+ _setActiveCueForTrack(this);
+ }*/
+
+ // Display active caption if it contains text
+ function _setActiveCueForTrack(track) {
+ 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() {
// Bail if not HTML5 video
@@ -940,7 +1063,8 @@
}
// Get URL of caption file if exists
- var captionSrc = '',
+ var captionSources = [],
+ captionSrc = '',
kind,
children = plyr.media.childNodes;
@@ -948,17 +1072,21 @@
if (children[i].nodeName.toLowerCase() === 'track') {
kind = children[i].kind;
if (kind === 'captions' || kind === 'subtitles') {
- captionSrc = children[i].getAttribute('src');
+ captionSources.push(children[i].getAttribute('src'));
}
}
}
// Record if caption file exists or not
plyr.captionExists = true;
- if (captionSrc === '') {
+ if (captionSources.length === 0) {
plyr.captionExists = false;
_log('No caption track found');
+ } else if ((config.captions.selectedIndex + 1) > captionSources.length) {
+ plyr.captionExists = false;
+ _log('Caption index out of bound');
} else {
+ captionSrc = captionSources[config.captions.selectedIndex];
_log('Caption track found; URI: ' + captionSrc);
}
@@ -970,6 +1098,8 @@
// 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++) {
+ // Remove the listener to prevent event overlapping
+ _off(tracks[x], 'cuechange', _setActiveCueForTrack);
tracks[x].mode = 'hidden';
}
@@ -993,18 +1123,14 @@
if (plyr.usingTextTracks) {
_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', _setActiveCueForTrack);
+
+ // if we change the active track while a cue is already displayed we need to update it
+ if (track.activeCues && track.activeCues.length > 0) {
+ _setActiveCueForTrack(track);
}
}
} else {
@@ -1281,6 +1407,12 @@
// Replace seek time instances
html = _replaceAll(html, '{seektime}', config.seekTime);
+ // Replace seek time instances
+ html = _replaceAll(html, '{speed}', config.currentSpeed.toFixed(1).toString().replace('.0', '') + '&times;');
+
+ // Replace current captions language
+ html = _replaceAll(html, '{lang}', 'English');
+
// Replace all id references with random numbers
html = _replaceAll(html, '{id}', Math.floor(Math.random() * (10000)));
@@ -1327,6 +1459,7 @@
plyr.buttons.rewind = _getElement(config.selectors.buttons.rewind);
plyr.buttons.forward = _getElement(config.selectors.buttons.forward);
plyr.buttons.fullscreen = _getElement(config.selectors.buttons.fullscreen);
+ plyr.buttons.settings = _getElement(config.selectors.buttons.settings);
// Inputs
plyr.buttons.mute = _getElement(config.selectors.buttons.mute);
@@ -1416,7 +1549,7 @@
plyr.storage = {};
// Bail if we don't have localStorage support or it's disabled
- if (!_storage.supported || !config.storage.enabled) {
+ if (!_support.storage || !config.storage.enabled) {
return;
}
@@ -1444,7 +1577,7 @@
// Save a value back to local storage
function _updateStorage(value) {
// Bail if we don't have localStorage support or it's disabled
- if (!_storage.supported || !config.storage.enabled) {
+ if (!_support.storage || !config.storage.enabled) {
return;
}
@@ -1473,6 +1606,9 @@
_toggleClass(plyr.container, config.classes.type.replace('{0}', 'video'), true);
}
+ // Check for picture-in-picture support
+ _toggleClass(plyr.container, config.classes.pip.enabled, _support.pip(plyr));
+
// If there's no autoplay attribute, assume the video is stopped and add state class
_toggleClass(plyr.container, config.classes.stopped, config.autoplay);
@@ -1967,6 +2103,36 @@
_seek(plyr.media.currentTime + seekTime);
}
+ // Speed-up
+ function _speed(speed) {
+ if (!_is.array(config.speeds)) {
+ _warn('Invalid speeds format');
+ return;
+ }
+ if (!_is.number(speed)) {
+ var index = config.speeds.indexOf(config.currentSpeed);
+
+ if (index !== -1) {
+ var nextIndex = index + 1;
+ if (nextIndex >= config.speeds.length) {
+ nextIndex = 0;
+ }
+ speed = config.speeds[nextIndex];
+ } else {
+ speed = config.defaultSpeed;
+ }
+ }
+
+ // Store current speed
+ config.currentSpeed = speed;
+
+ // Set HTML5 speed
+ plyr.media.playbackRate = speed;
+
+ // Save speed to localStorage
+ _updateStorage({speed: speed});
+ }
+
// Seek to time
// The input parameter can be an event or a number
function _seek(input) {
@@ -1976,7 +2142,7 @@
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);
@@ -2077,27 +2243,27 @@
// Toggle fullscreen
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) {
+ plyr.isFullscreen = _fullscreen.isFullScreen(plyr.container);
} else {
// Else it's a user request to enter or exit
- if (!fullscreen.isFullScreen(plyr.container)) {
+ if (!_fullscreen.isFullScreen(plyr.container)) {
// Save scroll position
_saveScrollPosition();
// Request full screen
- fullscreen.requestFullScreen(plyr.container);
+ _fullscreen.requestFullScreen(plyr.container);
} else {
// Bail from fullscreen
- fullscreen.cancelFullScreen();
+ _fullscreen.cancelFullScreen();
}
// Check if we're actually full screen (it could fail)
- plyr.isFullscreen = fullscreen.isFullScreen(plyr.container);
+ plyr.isFullscreen = _fullscreen.isFullScreen(plyr.container);
return;
}
@@ -2300,6 +2466,18 @@
_updateStorage({captionsEnabled: plyr.captionsEnabled});
}
+ // Select active caption
+ function _setCaptionIndex(index) {
+ // Save active caption
+ config.captions.selectedIndex = index;
+
+ // Clear caption
+ _setCaption();
+
+ // Re-run setup
+ _setupCaptions();
+ }
+
// Check if media is loading
function _checkLoading(event) {
var loading = (event.type === 'waiting');
@@ -2534,6 +2712,16 @@
}
}
+ // Set playback speed
+ function _setSpeed(speed) {
+ // Load speed from storage or default value
+ if (_is.undefined(speed)) {
+ speed = plyr.storage.speed || config.defaultSpeed;
+ }
+
+ _speed(speed);
+ }
+
// Show the player controls in fullscreen mode
function _toggleControls(toggle) {
// Don't hide if config says not to, it's audio, or not ready or loading
@@ -2950,7 +3138,7 @@
// Escape is handle natively when in full screen
// So we only need to worry about non native
- if (!fullscreen.supportsFullScreen && plyr.isFullscreen && code === 27) {
+ if (!_support.fullscreen && plyr.isFullscreen && code === 27) {
_toggleFullscreen();
}
@@ -2996,6 +3184,9 @@
// Fast forward
_proxyListener(plyr.buttons.forward, 'click', config.listeners.forward, _forward);
+ // Speed-up
+ _proxyListener(plyr.buttons.speed, 'click', config.listeners.speed, _speed);
+
// Seek
_proxyListener(plyr.buttons.seek, inputEvent, config.listeners.seek, _seek);
@@ -3011,13 +3202,73 @@
_proxyListener(plyr.buttons.fullscreen, 'click', config.listeners.fullscreen, _toggleFullscreen);
// Handle user exiting fullscreen by escaping etc
- if (fullscreen.supportsFullScreen) {
- _on(document, fullscreen.fullScreenEventName, _toggleFullscreen);
+ if (_support.fullscreen) {
+ _on(document, _fullscreen.eventType, _toggleFullscreen);
}
// Captions
_on(plyr.buttons.captions, 'click', _toggleCaptions);
+ // Settings
+ _on(plyr.buttons.settings, 'click', function(event) {
+ var menu = this,
+ toggle = event.target,
+ target = document.getElementById(toggle.getAttribute('aria-controls')),
+ show = (toggle.getAttribute('aria-expanded') === 'false');
+
+ // Nothing to show, bail
+ if (!_is.htmlElement(target)) {
+ return;
+ }
+
+ // Are we targetting a tab?
+ var isTab = target.getAttribute('role') === 'tabpanel',
+ targetWidth,
+ targetHeight,
+ container;
+
+ // Hide all other tabs
+ if (isTab) {
+ // Get other tabs
+ var current = menu.querySelector('[role="tabpanel"][aria-hidden="false"]');
+ container = current.parentNode;
+
+ [].forEach.call(menu.querySelectorAll('[aria-controls="' + current.getAttribute('id') + '"]'), function(toggle) {
+ toggle.setAttribute('aria-expanded', false);
+ });
+
+ container.style.width = current.scrollWidth + 'px';
+ container.style.height = current.scrollHeight + 'px';
+
+ current.setAttribute('aria-hidden', true);
+ current.setAttribute('tabindex', -1);
+
+ // Get the natural element size
+ var clone = target.cloneNode(true);
+ clone.style.position = "absolute";
+ clone.style.opacity = 0;
+ clone.setAttribute('aria-hidden', false);
+ container.appendChild(clone);
+ targetWidth = clone.scrollWidth;
+ targetHeight = clone.scrollHeight;
+ _remove(clone);
+ }
+
+ target.setAttribute('aria-hidden', !show);
+ toggle.setAttribute('aria-expanded', show);
+ target.setAttribute('tabindex', 0);
+
+ if (isTab) {
+ container.style.width = targetWidth + 'px';
+ container.style.height = targetHeight + 'px';
+
+ window.setTimeout(function() {
+ container.style.width = '';
+ container.style.height = '';
+ }, 300);
+ }
+ });
+
// Seek tooltip
_on(plyr.progress.container, 'mouseenter mouseleave mousemove', _updateSeekTooltip);
@@ -3258,11 +3509,8 @@
return null;
}
- // Setup the fullscreen api
- fullscreen = _fullscreen();
-
// Sniff out the browser
- plyr.browser = _browserSniff();
+ plyr.browser = _getBrowser();
// Bail if nothing to setup
if (!_is.htmlElement(plyr.media)) {
@@ -3381,6 +3629,9 @@
_setVolume();
_updateVolume();
+ // Set playback speed
+ _setSpeed();
+
// Reset time display
_timeUpdate();
@@ -3411,13 +3662,15 @@
source: _source,
poster: _updatePoster,
setVolume: _setVolume,
+ setSpeed: _setSpeed,
togglePlay: _togglePlay,
toggleMute: _toggleMute,
toggleCaptions: _toggleCaptions,
toggleFullscreen: _toggleFullscreen,
toggleControls: _toggleControls,
+ setCaptionIndex: _setCaptionIndex,
isFullscreen: function() { return plyr.isFullscreen || false; },
- support: function(mimeType) { return _supportMime(plyr, mimeType); },
+ support: function(mimeType) { return _support.mime(plyr, mimeType); },
destroy: _destroy
};
@@ -3488,7 +3741,7 @@
// Check for support
function supported(type) {
- var browser = _browserSniff(),
+ var browser = _getBrowser(),
isOldIE = (browser.isIE && browser.version <= 9),
isIos = browser.isIos,
isIphone = /iPhone|iPod/i.test(navigator.userAgent),