diff options
Diffstat (limited to 'src/js')
-rw-r--r-- | src/js/plyr.js | 563 |
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×</button>', + '</li>', + '<li>', + '<button type="button">1.5×</button>', + '</li>', + '<li>', + '<button type="button">1×</button>', + '</li>', + '<li>', + '<button type="button">0.5×</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', '') + '×'); + + // 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), |