diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/js/plyr.js | 576 | ||||
-rw-r--r-- | src/less/plyr.less | 192 | ||||
-rw-r--r-- | src/less/variables.less | 15 | ||||
-rw-r--r-- | src/sprite/plyr-settings.svg | 6 |
4 files changed, 612 insertions, 177 deletions
diff --git a/src/js/plyr.js b/src/js/plyr.js index dd14f767..7eb72067 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.undefined(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.undefined(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,21 @@ } } + // 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() { // Bail if not HTML5 video @@ -940,7 +1059,8 @@ } // Get URL of caption file if exists - var captionSrc = '', + var captionSources = [], + captionSrc = '', kind, children = plyr.media.childNodes; @@ -948,17 +1068,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); } @@ -966,12 +1090,17 @@ if (!plyr.captionExists) { _toggleClass(plyr.container, config.classes.captions.enabled); } else { + var tracks = plyr.media.textTracks; + // Turn off native caption rendering to avoid double captions // This doesn't seem to work in Safari 7+, so the <track> elements are removed from the dom below - var tracks = plyr.media.textTracks; - for (var x = 0; x < tracks.length; x++) { - tracks[x].mode = 'hidden'; - } + [].forEach.call(tracks, function(track) { + // Remove the listener to prevent event overlapping + _off(track, 'cuechange', _setActiveCue); + + // Hide captions + track.mode = 'hidden'; + }); // Enable UI _showCaptions(plyr); @@ -993,18 +1122,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', _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 { @@ -1029,15 +1154,15 @@ //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 (req.indexOf(lineSeparator + lineSeparator) === -1) { + if (req.indexOf('\r\r') !== -1) { lineSeparator = '\r'; } else { lineSeparator = '\n'; } } - captions = req.split(lineSeparator+lineSeparator); + captions = req.split(lineSeparator + lineSeparator); for (var r = 0; r < captions.length; r++) { caption = captions[r]; @@ -1281,6 +1406,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 +1458,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 +1548,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 +1576,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 +1605,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); @@ -1975,6 +2110,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) { @@ -1984,7 +2149,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); @@ -2085,27 +2250,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; } @@ -2308,6 +2473,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'); @@ -2542,6 +2719,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 @@ -2958,7 +3145,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(); } @@ -3004,6 +3191,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); @@ -3019,13 +3209,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); @@ -3266,11 +3516,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)) { @@ -3389,6 +3636,9 @@ _setVolume(); _updateVolume(); + // Set playback speed + _setSpeed(); + // Reset time display _timeUpdate(); @@ -3420,13 +3670,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 }; @@ -3497,7 +3749,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), diff --git a/src/less/plyr.less b/src/less/plyr.less index 143e7f06..a23924e6 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; @@ -225,7 +241,6 @@ color: @plyr-captions-color; font-size: @plyr-font-size-captions-base; text-align: center; - font-weight: 400; span { border-radius: 2px; @@ -272,7 +287,8 @@ // Spacing > button, .plyr__progress, - .plyr__time { + .plyr__time, + .plyr__menu { margin-left: (@plyr-control-spacing / 2); &:first-child { @@ -324,9 +340,16 @@ @media (min-width: @plyr-bp-screen-sm) { > button, .plyr__progress, - .plyr__time { + .plyr__time, + .plyr__menu { margin-left: @plyr-control-spacing; } + + > button + button, + .plyr__menu + button, + > button + .plyr__menu { + margin-left: (@plyr-control-spacing / 2); + } } } // Hide controls @@ -342,7 +365,7 @@ right: 0; bottom: 0; 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; @@ -351,7 +374,8 @@ button { // Hover and tab focus &.tab-focus:focus, - &:hover { + &:hover, + &[aria-expanded='true'] { background: @plyr-video-control-bg-hover; color: @plyr-video-control-color-hover; } @@ -369,7 +393,8 @@ button { // Hover and tab focus &.tab-focus:focus, - &:hover { + &:hover, + &[aria-expanded='true'] { background: @plyr-audio-control-bg-hover; color: @plyr-audio-control-color-hover; } @@ -446,6 +471,152 @@ display: inline-block; } +// Menus +// -------------------------------------------------------------- +.plyr__menu { + position: relative; + + // Hide tooltip + button svg { + transition: transform .3s ease; + } + button[aria-expanded='true'] { + svg { + transform: rotate(45deg); + } + .plyr__tooltip { + display: none; + } + } + + // The actual menu container + &__container { + position: absolute; + bottom: 100%; + right: -5px; + margin-bottom: 10px; + animation: plyr-popup .2s ease; + + background: @plyr-menu-bg; + box-shadow: 0 1px 0 fade(#000, 20%); + border-radius: 4px; + + white-space: nowrap; + text-align: left; + color: @plyr-menu-color; + font-size: @plyr-font-size-small; + + > div { + overflow: hidden; + transition: height .35s cubic-bezier(.4,0,.2,1), width .35s cubic-bezier(.4,0,.2,1); + } + + // Arrow + &::after { + content: ""; + position: absolute; + top: 100%; + right: 15px; + height: 0; + width: 0; + border: 6px solid transparent; + border-top-color: @plyr-menu-bg; + } + + ul { + margin: 0; + padding: 5px; + list-style: none; + overflow: hidden; + } + button { + display: flex; + width: 100%; + padding: @plyr-control-padding (@plyr-control-padding * 2); + color: @plyr-menu-color; + font-weight: 600; + user-select: none; + } + + // Buttons + .plyr__menu__btn::after { + content: ""; + position: absolute; + top: 50%; + transform: translateY(-50%); + border: 5px solid transparent; + } + .plyr__menu__btn--forward { + padding-right: ceil(@plyr-control-padding * 4); + + &::after { + right: 5px; + border-left-color: fade(@plyr-menu-color, 80%); + } + } + .plyr__menu__btn--back { + position: relative; + padding-top: floor(@plyr-control-padding * .7); + padding-bottom: floor(@plyr-control-padding * .7); + padding-left: ceil(@plyr-control-padding * 4); + margin-bottom: ceil(@plyr-control-padding * 1.3); + font-weight: 500; + + &::after { + left: 5px; + border-right-color: fade(@plyr-menu-color, 80%); + } + &::before { + content: ""; + position: absolute; + top: 100%; + left: 0; + right: 0; + height: 1px; + overflow: hidden; + margin-top: ceil(@plyr-control-padding / 2); + background: fade(#000, 15%); + box-shadow: 0 1px 0 fade(#fff, 10%); + } + } + + // Option value + .plyr__menu__btn__value, + .plyr__menu__btn__badge { + display: inherit; + margin-left: auto; + padding-left: ceil(@plyr-control-padding * 3.5); + pointer-events: none; + overflow: hidden; + } + + // Option value + .plyr__menu__btn__value { + font-weight: 500; + color: fade(@plyr-menu-color, 80%); + } + + // Option value + .plyr__menu__btn__badge span { + padding: 2px 4px; + border-radius: 2px; + background: @plyr-menu-color; + color: @plyr-menu-bg; + font-size: 10px; + } + + // 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); + + ul { + //opacity: 0; + } + } + } +} + // Tooltips // -------------------------------------------------------------- .plyr__tooltip { @@ -462,6 +633,7 @@ color: @plyr-tooltip-color; font-size: @plyr-font-size-small; + font-weight: 500; line-height: 1.3; transform: translate(-50%, 10px) scale(.8); @@ -496,7 +668,7 @@ } // First tooltip -.plyr__controls button:first-child .plyr__tooltip { +.plyr__controls > button:first-child .plyr__tooltip { left: 0; transform: translate(0, 10px) scale(.8); transform-origin: 0 100%; @@ -507,7 +679,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%; @@ -519,8 +691,8 @@ } } -.plyr__controls button:first-child, -.plyr__controls button:last-child { +.plyr__controls > button:first-child, +.plyr__controls > button: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..0d528380 100644 --- a/src/less/variables.less +++ b/src/less/variables.less @@ -17,7 +17,7 @@ @plyr-font-size-base: 16px; // Captions -@plyr-captions-bg: fade(#000, 70%); +@plyr-captions-bg: fade(#000, 85%); @plyr-captions-color: #fff; @plyr-font-size-captions-base: @plyr-font-size-base; @plyr-font-size-captions-medium: ceil(@plyr-font-size-base * 1.5); @@ -27,7 +27,7 @@ @plyr-control-icon-size: 18px; @plyr-control-spacing: 10px; @plyr-control-padding: (@plyr-control-spacing * .7); -@plyr-video-controls-bg: #000; +@plyr-video-controls-bg: #343f4a; @plyr-video-control-color: #fff; @plyr-video-control-color-hover: #fff; @plyr-video-control-bg-hover: @plyr-color-main; @@ -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/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 |