diff options
Diffstat (limited to 'src/js/plyr.js')
-rw-r--r-- | src/js/plyr.js | 2485 |
1 files changed, 1307 insertions, 1178 deletions
diff --git a/src/js/plyr.js b/src/js/plyr.js index 2a44a13b..459bc6fd 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -5,25 +5,20 @@ // License: The MIT License (MIT) // ========================================================================== -(function(root, factory) { +(function(name, context, definition) { + /* global define,module,require,YUI */ 'use strict'; - /* global define,module */ - if (typeof module === 'object' && typeof module.exports === 'object') { - // Node, CommonJS-like - module.exports = factory(root, document); + if (typeof exports === 'object') { + module.exports = definition(require); } else if (typeof define === 'function' && define.amd) { - // AMD - define([], function() { - return factory(root, document); - }); + define(definition); } else { - // Browser globals (root is window) - root.Plyr = factory(root, document); + context[name] = definition(); } -}(typeof window !== 'undefined' ? window : this, function(window, document) { +}).call(this, 'Plyr', this, function(require) { 'use strict'; - /* global jQuery */ + /* global jQuery, console */ // Globals var scroll = { @@ -132,10 +127,8 @@ // Class hooks added to the player in different states classes: { - setup: 'plyr--setup', - ready: 'plyr--ready', - videoWrapper: 'plyr__video-wrapper', - embedWrapper: 'plyr__video-embed', + video: 'plyr__video-wrapper', + embed: 'plyr__video-embed', control: 'plyr__control', type: 'plyr--{0}', stopped: 'plyr--stopped', @@ -306,7 +299,7 @@ html5: ['video', 'audio'] }; - // Utilities outside of Plyr scope + // Utilities var utils = { // Check variable types is: { @@ -1197,25 +1190,30 @@ }; // Player instance - function Player(element, options) { + function Player(media, options) { var player = this; var timers = {}; var api = {}; + // Get the media element + player.media = media; + // String selector passed - if (utils.is.string(element)) { - element = document.querySelectorAll(element); + if (utils.is.string(player.media)) { + player.media = document.querySelectorAll(player.media); } // jQuery, NodeList or Array passed, use first element - if ((window.jQuery && element instanceof jQuery) || utils.is.nodeList(element) || utils.is.array(element)) { - element = element[0]; + if ((window.jQuery && player.media instanceof jQuery) || + utils.is.nodeList(player.media) || + utils.is.array(player.media)) { + player.media = player.media[0]; } - // Config - var config = utils.extend({}, defaults, options, (function() { + // Set config + player.config = utils.extend({}, defaults, options, (function() { try { - return JSON.parse(element.getAttribute('data-plyr')); + return JSON.parse(player.media.getAttribute('data-plyr')); } catch (e) {} })()); @@ -1231,7 +1229,6 @@ panes: {}, tabs: {} }, - media: element, captions: null }; @@ -1252,20 +1249,21 @@ var log = function() {}; var warn = function() {}; var error = function() {}; - if (config.debug && 'console' in window) { - log = window.console.log; - warn = window.console.warn; - error = window.console.error; + if (player.config.debug && 'console' in window) { + log = console.log; + warn = console.warn; + error = console.error; + log('Debugging enabled'); } // Log config options and support - log('Config', config); + log('Config', player.config); log('Support', support); // Trigger events, with plyr instance passed function trigger(element, type, bubbles, properties) { utils.event(element, type, bubbles, utils.extend({}, properties, { - plyr: api + plyr: player })); } @@ -1318,12 +1316,12 @@ // Add elements to HTML5 media (source, tracks, etc) function insertElements(type, attributes) { if (utils.is.string(attributes)) { - utils.insertElement(type, player.elements.media, { + utils.insertElement(type, player.media, { src: attributes }); } else if (utils.is.array(attributes)) { attributes.forEach(function(attribute) { - utils.insertElement(type, player.elements.media, attribute); + utils.insertElement(type, player.media, attribute); }); } } @@ -1331,8 +1329,8 @@ // Get icon URL function getIconUrl() { return { - url: config.iconUrl, - absolute: (config.iconUrl.indexOf("http") === 0) || player.browser.isIE + url: player.config.iconUrl, + absolute: (player.config.iconUrl.indexOf("http") === 0) || player.browser.isIE }; } @@ -1340,7 +1338,7 @@ function createIcon(type, attributes) { var namespace = 'http://www.w3.org/2000/svg'; var iconUrl = getIconUrl(); - var iconPath = (!iconUrl.absolute ? iconUrl.url : '') + '#' + config.iconPrefix; + var iconPath = (!iconUrl.absolute ? iconUrl.url : '') + '#' + player.config.iconPrefix; // Create <svg> var icon = document.createElementNS(namespace, 'svg'); @@ -1360,7 +1358,7 @@ // Create hidden text label function createLabel(type) { - var text = config.i18n[type]; + var text = player.config.i18n[type]; switch (type) { case 'pip': @@ -1373,18 +1371,18 @@ } return utils.createElement('span', { - class: config.classes.hidden + class: player.config.classes.hidden }, text); } // Create a badge function createBadge(text) { var badge = utils.createElement('span', { - class: config.classes.menu.value + class: player.config.classes.menu.value }); badge.appendChild(utils.createElement('span', { - class: config.classes.menu.badge + class: player.config.classes.menu.badge }, text)); return badge; @@ -1402,11 +1400,11 @@ } if ('class' in attributes) { - if (attributes.class.indexOf(config.classes.control) === -1) { - attributes.class += ' ' + config.classes.control; + if (attributes.class.indexOf(player.config.classes.control) === -1) { + attributes.class += ' ' + player.config.classes.control; } } else { - attributes.class = config.classes.control; + attributes.class = player.config.classes.control; } // Large play button @@ -1442,7 +1440,7 @@ } // Merge attributes - utils.extend(attributes, utils.getAttributesFromSelector(config.selectors.buttons[type], attributes)); + utils.extend(attributes, utils.getAttributesFromSelector(player.config.selectors.buttons[type], attributes)); // Add toggle icon if needed if (utils.is.string(iconToggled)) { @@ -1466,11 +1464,11 @@ // Seek label var label = utils.createElement('label', { for: attributes.id, - class: config.classes.hidden - }, config.i18n[type]); + class: player.config.classes.hidden + }, player.config.i18n[type]); // Seek input - var input = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(config.selectors.inputs[type]), { + var input = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(player.config.selectors.inputs[type]), { type: 'range', min: 0, max: 100, @@ -1489,7 +1487,7 @@ // Create a <progress> function createProgress(type, attributes) { - var progress = utils.createElement('progress', utils.extend(utils.getAttributesFromSelector(config.selectors.display[type]), { + var progress = utils.createElement('progress', utils.extend(utils.getAttributesFromSelector(player.config.selectors.display[type]), { min: 0, max: 100, value: 0 @@ -1502,11 +1500,11 @@ var suffix = ''; switch (type) { case 'played': - suffix = config.i18n.played; + suffix = player.config.i18n.played; break; case 'buffer': - suffix = config.i18n.buffered; + suffix = player.config.i18n.buffered; break; } @@ -1525,10 +1523,10 @@ }); container.appendChild(utils.createElement('span', { - class: config.classes.hidden - }, config.i18n[type])); + class: player.config.classes.hidden + }, player.config.i18n[type])); - container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(config.selectors.display[type]), '00:00')); + container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(player.config.selectors.display[type]), '00:00')); player.elements.display[type] = container; @@ -1538,33 +1536,33 @@ // Build the default HTML function createControls(data) { // Create the container - var controls = utils.createElement('div', utils.getAttributesFromSelector(config.selectors.controls.wrapper)); + var controls = utils.createElement('div', utils.getAttributesFromSelector(player.config.selectors.controls.wrapper)); // Restart button - if (utils.inArray(config.controls, 'restart')) { + if (utils.inArray(player.config.controls, 'restart')) { controls.appendChild(createButton('restart')); } // Rewind button - if (utils.inArray(config.controls, 'rewind')) { + if (utils.inArray(player.config.controls, 'rewind')) { controls.appendChild(createButton('rewind')); } // Play Pause button // TODO: This should be a toggle button really? - if (utils.inArray(config.controls, 'play')) { + if (utils.inArray(player.config.controls, 'play')) { controls.appendChild(createButton('play')); controls.appendChild(createButton('pause')); } // Fast forward button - if (utils.inArray(config.controls, 'fast-forward')) { + if (utils.inArray(player.config.controls, 'fast-forward')) { controls.appendChild(createButton('fast-forward')); } // Progress - if (utils.inArray(config.controls, 'progress')) { - var container = utils.createElement('span', utils.getAttributesFromSelector(config.selectors.progress)); + if (utils.inArray(player.config.controls, 'progress')) { + var container = utils.createElement('span', utils.getAttributesFromSelector(player.config.selectors.progress)); // Seek range slider var seek = createRange('seek', { @@ -1582,10 +1580,10 @@ container.appendChild(createProgress('buffer')); // Seek tooltip - if (config.tooltips.seek) { + if (player.config.tooltips.seek) { var tooltip = utils.createElement('span', { role: 'tooltip', - class: config.classes.tooltip + class: player.config.classes.tooltip }, '00:00'); container.appendChild(tooltip); @@ -1597,22 +1595,22 @@ } // Media current time display - if (utils.inArray(config.controls, 'current-time')) { + if (utils.inArray(player.config.controls, 'current-time')) { controls.appendChild(createTime('currentTime')); } // Media duration display - if (utils.inArray(config.controls, 'duration')) { + if (utils.inArray(player.config.controls, 'duration')) { controls.appendChild(createTime('duration')); } // Toggle mute button - if (utils.inArray(config.controls, 'mute')) { + if (utils.inArray(player.config.controls, 'mute')) { controls.appendChild(createButton('mute')); } // Volume range control - if (utils.inArray(config.controls, 'volume')) { + if (utils.inArray(player.config.controls, 'volume')) { var volume = utils.createElement('span', { class: 'plyr__volume' }); @@ -1620,7 +1618,7 @@ // Set the attributes var attributes = { max: 10, - value: config.volume + value: player.config.volume }; // Create the volume range slider @@ -1638,12 +1636,12 @@ } // Toggle captions button - if (utils.inArray(config.controls, 'captions')) { + if (utils.inArray(player.config.controls, 'captions')) { controls.appendChild(createButton('captions')); } // Settings button / menu - if (utils.inArray(config.controls, 'settings')) { + if (utils.inArray(player.config.controls, 'settings')) { var menu = utils.createElement('div', { class: 'plyr__menu' }); @@ -1679,23 +1677,23 @@ }); // Build the tabs - config.settings.forEach(function(type) { + player.config.settings.forEach(function(type) { var tab = utils.createElement('li', { role: 'tab', hidden: '' }); - var button = utils.createElement('button', utils.extend(utils.getAttributesFromSelector(config.selectors.buttons.settings), { + var button = utils.createElement('button', utils.extend(utils.getAttributesFromSelector(player.config.selectors.buttons.settings), { type: 'button', - class: config.classes.control + ' ' + config.classes.control + '--forward', + class: player.config.classes.control + ' ' + player.config.classes.control + '--forward', id: 'plyr-settings-' + data.id + '-' + type + '-tab', 'aria-haspopup': true, 'aria-controls': 'plyr-settings-' + data.id + '-' + type, 'aria-expanded': false - }), config.i18n[type]); + }), player.config.i18n[type]); var value = utils.createElement('span', { - class: config.classes.menu.value + class: player.config.classes.menu.value }); // Speed contains HTML entities @@ -1712,7 +1710,7 @@ inner.appendChild(home); // Build the panes - config.settings.forEach(function(type) { + player.config.settings.forEach(function(type) { var pane = utils.createElement('div', { id: 'plyr-settings-' + data.id + '-' + type, 'aria-hidden': true, @@ -1724,11 +1722,11 @@ var back = utils.createElement('button', { type: 'button', - class: config.classes.control + ' ' + config.classes.control + '--back', + class: player.config.classes.control + ' ' + player.config.classes.control + '--back', 'aria-haspopup': true, 'aria-controls': 'plyr-settings-' + data.id + '-home', 'aria-expanded': false - }, config.i18n[type]); + }, player.config.i18n[type]); pane.appendChild(back); @@ -1749,20 +1747,26 @@ } // Picture in picture button - if (utils.inArray(config.controls, 'pip') && support.pip) { + if (utils.inArray(player.config.controls, 'pip') && support.pip) { controls.appendChild(createButton('pip')); } // Airplay button - if (utils.inArray(config.controls, 'airplay') && support.airplay) { + if (utils.inArray(player.config.controls, 'airplay') && support.airplay) { controls.appendChild(createButton('airplay')); } // Toggle fullscreen button - if (utils.inArray(config.controls, 'fullscreen')) { + if (utils.inArray(player.config.controls, 'fullscreen')) { controls.appendChild(createButton('fullscreen')); } + // Larger overlaid play button + if (utils.inArray(player.config.controls, 'play-large')) { + player.elements.buttons.playLarge = createButton('play-large'); + player.elements.container.appendChild(player.elements.buttons.playLarge); + } + player.elements.controls = controls; setLoopMenu(); @@ -1840,16 +1844,16 @@ var item = utils.createElement('li'); var label = utils.createElement('label', { - class: config.classes.control + class: player.config.classes.control }); - var radio = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(config.selectors.inputs.quality), { + var radio = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(player.config.selectors.inputs.quality), { type: 'radio', name: 'plyr-quality', value: quality, })); - if (quality === config.quality.selected) { + if (quality === player.config.quality.selected) { radio.checked = true; } @@ -1882,11 +1886,11 @@ options.forEach(function(option) { var item = utils.createElement('li'); - var button = utils.createElement('button', utils.extend(utils.getAttributesFromSelector(config.selectors.buttons.loop), { + var button = utils.createElement('button', utils.extend(utils.getAttributesFromSelector(player.config.selectors.buttons.loop), { type: 'button', - class: config.classes.control, + class: player.config.classes.control, 'data-plyr-loop-action': option - }), config.i18n[option]); + }), player.config.i18n[option]); if (utils.inArray(['start', 'end'], option)) { var badge = createBadge('00:00'); @@ -1926,7 +1930,7 @@ // Add the "None" option to turn off captions tracks.unshift({ language: 'off', - label: config.i18n.none + label: player.config.i18n.none }); // Generate options @@ -1934,16 +1938,16 @@ var item = utils.createElement('li'); var label = utils.createElement('label', { - class: config.classes.control + class: player.config.classes.control }); - var radio = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(config.selectors.inputs.language), { + var radio = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(player.config.selectors.inputs.language), { type: 'radio', name: 'plyr-language', value: track.language, })); - if (track.language.toLowerCase() === config.captions.language.toLowerCase()) { + if (track.language.toLowerCase() === player.config.captions.language.toLowerCase()) { radio.checked = true; } @@ -1972,23 +1976,23 @@ // If there's no captions, bail if (!utils.is.array(options)) { - options = config.speed.options; + options = player.config.speed.options; } options.forEach(function(speed) { var item = utils.createElement('li'); var label = utils.createElement('label', { - class: config.classes.control + class: player.config.classes.control }); - var radio = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(config.selectors.inputs.speed), { + var radio = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(player.config.selectors.inputs.speed), { type: 'radio', name: 'plyr-speed', value: speed, })); - if (speed === config.speed.selected) { + if (speed === player.config.speed.selected) { radio.checked = true; } @@ -2005,15 +2009,15 @@ return; } - if ((player.type !== 'audio' || config.fullscreen.allowAudio) && config.fullscreen.enabled) { + if ((player.type !== 'audio' || player.config.fullscreen.allowAudio) && player.config.fullscreen.enabled) { // Check for native support var nativeSupport = support.fullscreen; - if (nativeSupport || (config.fullscreen.fallback && !utils.inFrame())) { + if (nativeSupport || (player.config.fullscreen.fallback && !utils.inFrame())) { log((nativeSupport ? 'Native' : 'Fallback') + ' fullscreen enabled'); // Add styling hook - utils.toggleClass(player.elements.container, config.classes.fullscreen.enabled, true); + utils.toggleClass(player.elements.container, player.config.classes.fullscreen.enabled, true); } else { log('Fullscreen not supported and fallback disabled'); } @@ -2037,15 +2041,15 @@ // Inject the container if (!utils.is.htmlElement(player.elements.captions)) { - player.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(config.selectors.captions)); + player.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(player.config.selectors.captions)); utils.insertAfter(player.elements.captions, player.elements.wrapper); } // Get tracks - player.captions.tracks = utils.is.array(tracks) ? tracks : player.elements.media.textTracks; + player.captions.tracks = utils.is.array(tracks) ? tracks : player.media.textTracks; // Set the class hook - utils.toggleClass(player.elements.container, config.classes.captions.enabled, !utils.is.empty(player.captions.tracks)); + utils.toggleClass(player.elements.container, player.config.classes.captions.enabled, !utils.is.empty(player.captions.tracks)); // If no caption file exists, hide container for caption text if (utils.is.empty(player.captions.tracks)) { @@ -2056,7 +2060,7 @@ showCaptions(); if (player.type === 'video') { - var language = config.captions.language.toLowerCase(); + var language = player.config.captions.language.toLowerCase(); // Turn off native caption rendering to avoid double captions [].forEach.call(player.captions.tracks, function(track) { @@ -2124,22 +2128,6 @@ } } - // Select active caption - function setLanguage(language) { - // Save config - if (utils.is.string(language)) { - config.captions.language = language.toLowerCase(); - } else if (utils.is.event(language)) { - config.captions.language = language.target.value.toLowerCase(); - } - - // Clear caption - setCaption(); - - // Re-run setup - setupCaptions(); - } - // Set the current caption function setCaption(caption) { if (utils.is.htmlElement(player.elements.captions)) { @@ -2182,51 +2170,21 @@ // Otherwise fall back to the default config if (!utils.is.boolean(active)) { - active = config.captions.active; + active = player.config.captions.active; } else { - config.captions.active = active; + player.config.captions.active = active; } if (active) { - utils.toggleClass(player.elements.container, config.classes.captions.active, true); + utils.toggleClass(player.elements.container, player.config.classes.captions.active, true); utils.toggleState(player.elements.buttons.captions, true); } } - // Toggle captions - function toggleCaptions(show) { - // If there's no full support, or there's no caption toggle - if (!player.supported.full || !player.elements.buttons.captions) { - return; - } - - // If the method is called without parameter, toggle based on current value - if (!utils.is.boolean(show)) { - show = (player.elements.container.className.indexOf(config.classes.captions.active) === -1); - } - - // Set global - player.captions.enabled = show; - - // Toggle state - utils.toggleState(player.elements.buttons.captions, player.captions.enabled); - - // Add class hook - utils.toggleClass(player.elements.container, config.classes.captions.active, player.captions.enabled); - - // Trigger an event - trigger(player.elements.container, player.captions.enabled ? 'captionsenabled' : 'captionsdisabled', true); - - // Save captions state to localStorage - updateStorage({ - captions: player.captions.enabled - }); - } - // Insert controls function injectControls() { // Sprite - if (config.loadSprite) { + if (player.config.loadSprite) { var iconUrl = getIconUrl(); // Only load external sprite using AJAX @@ -2238,12 +2196,6 @@ } } - // Larger overlaid play button - if (utils.inArray(config.controls, 'play-large')) { - player.elements.buttons.playLarge = createButton('play-large'); - player.elements.container.appendChild(player.elements.buttons.playLarge); - } - // Create a unique ID player.id = Math.floor(Math.random() * 10000); @@ -2251,22 +2203,22 @@ var controls = null; // HTML passed as the option - if (utils.is.string(config.controls)) { - controls = config.controls; + if (utils.is.string(player.config.controls)) { + controls = player.config.controls; } // A custom function to build controls // The function can return a HTMLElement or String - else if (utils.is.function(config.controls)) { - controls = config.controls({ + else if (utils.is.function(player.config.controls)) { + controls = player.config.controls({ id: player.id, - seektime: config.seekTime + seektime: player.config.seekTime }); } // Create controls else { controls = createControls({ id: player.id, - seektime: config.seekTime, + seektime: player.config.seekTime, speed: getSpeedLabel(), // TODO: Get current quality quality: 'HD', @@ -2280,8 +2232,8 @@ var target; // Inject to custom location - if (utils.is.string(config.selectors.controls.container)) { - target = document.querySelector(config.selectors.controls.container); + if (utils.is.string(player.config.selectors.controls.container)) { + target = document.querySelector(player.config.selectors.controls.container); } // Inject into the container by default @@ -2302,14 +2254,14 @@ } // Setup tooltips - if (config.tooltips.controls) { - var labels = getElements([config.selectors.controls.wrapper, ' ', config.selectors.labels, ' .', config.classes.hidden].join('')); + if (player.config.tooltips.controls) { + var labels = getElements([player.config.selectors.controls.wrapper, ' ', player.config.selectors.labels, ' .', player.config.classes.hidden].join('')); for (var i = labels.length - 1; i >= 0; i--) { var label = labels[i]; - utils.toggleClass(label, config.classes.hidden, false); - utils.toggleClass(label, config.classes.tooltip, true); + utils.toggleClass(label, player.config.classes.hidden, false); + utils.toggleClass(label, player.config.classes.tooltip, true); } } } @@ -2318,44 +2270,44 @@ // TODO: Allow settings menus with custom controls (coming soon!) function findElements() { try { - player.elements.controls = getElement(config.selectors.controls.wrapper); + player.elements.controls = getElement(player.config.selectors.controls.wrapper); // Buttons player.elements.buttons = { - play: getElements(config.selectors.buttons.play), - pause: getElement(config.selectors.buttons.pause), - restart: getElement(config.selectors.buttons.restart), - rewind: getElement(config.selectors.buttons.rewind), - forward: getElement(config.selectors.buttons.forward), - mute: getElement(config.selectors.buttons.mute), - pip: getElement(config.selectors.buttons.pip), - airplay: getElement(config.selectors.buttons.airplay), - settings: getElement(config.selectors.buttons.settings), - captions: getElement(config.selectors.buttons.captions), - fullscreen: getElement(config.selectors.buttons.fullscreen) + play: getElements(player.config.selectors.buttons.play), + pause: getElement(player.config.selectors.buttons.pause), + restart: getElement(player.config.selectors.buttons.restart), + rewind: getElement(player.config.selectors.buttons.rewind), + forward: getElement(player.config.selectors.buttons.forward), + mute: getElement(player.config.selectors.buttons.mute), + pip: getElement(player.config.selectors.buttons.pip), + airplay: getElement(player.config.selectors.buttons.airplay), + settings: getElement(player.config.selectors.buttons.settings), + captions: getElement(player.config.selectors.buttons.captions), + fullscreen: getElement(player.config.selectors.buttons.fullscreen) }; // Progress - player.elements.progress = getElement(config.selectors.progress); + player.elements.progress = getElement(player.config.selectors.progress); // Inputs player.elements.inputs = { - seek: getElement(config.selectors.inputs.seek), - volume: getElement(config.selectors.inputs.volume), + seek: getElement(player.config.selectors.inputs.seek), + volume: getElement(player.config.selectors.inputs.volume), }; // Display player.elements.display = { - buffer: getElement(config.selectors.display.buffer), - played: getElement(config.selectors.display.played), - volume: getElement(config.selectors.display.volume), - duration: getElement(config.selectors.display.duration), - currentTime: getElement(config.selectors.display.currentTime), + buffer: getElement(player.config.selectors.display.buffer), + played: getElement(player.config.selectors.display.played), + volume: getElement(player.config.selectors.display.volume), + duration: getElement(player.config.selectors.display.duration), + currentTime: getElement(player.config.selectors.display.currentTime), }; // Seek tooltip if (utils.is.htmlElement(player.elements.progress)) { - player.elements.display.seekTooltip = player.elements.progress.querySelector('.' + config.classes.tooltip); + player.elements.display.seekTooltip = player.elements.progress.querySelector('.' + player.config.classes.tooltip); } return true; @@ -2372,29 +2324,29 @@ // Toggle style hook function toggleStyleHook() { - utils.toggleClass(player.elements.container, config.selectors.container.replace('.', ''), player.supported.full); + utils.toggleClass(player.elements.container, player.config.selectors.container.replace('.', ''), player.supported.full); } // Toggle native controls function toggleNativeControls(toggle) { if (toggle && utils.inArray(types.html5, player.type)) { - player.elements.media.setAttribute('controls', ''); + player.media.setAttribute('controls', ''); } else { - player.elements.media.removeAttribute('controls'); + player.media.removeAttribute('controls'); } } // Setup aria attribute for play and iframe title function setTitle(iframe) { // Find the current text - var label = config.i18n.play; + var label = player.config.i18n.play; // If there's a media title set, use that for the label - if (utils.is.string(config.title) && !utils.is.empty(config.title)) { - label += ', ' + config.title; + if (utils.is.string(player.config.title) && !utils.is.empty(player.config.title)) { + label += ', ' + player.config.title; // Set container label - player.elements.container.setAttribute('aria-label', config.title); + player.elements.container.setAttribute('aria-label', player.config.title); } // If there's a play button, set label @@ -2410,8 +2362,8 @@ // Set iframe title // https://github.com/sampotts/plyr/issues/124 if (utils.is.htmlElement(iframe)) { - var title = utils.is.string(config.title) && !utils.is.empty(config.title) ? config.title : 'video'; - iframe.setAttribute('title', config.i18n.frameTitle.replace('{title}', title)); + var title = utils.is.string(player.config.title) && !utils.is.empty(player.config.title) ? player.config.title : 'video'; + iframe.setAttribute('title', player.config.i18n.frameTitle.replace('{title}', title)); } } @@ -2421,7 +2373,7 @@ player.storage = {}; // Bail if we don't have localStorage support or it's disabled - if (!support.storage || !config.storage.enabled) { + if (!support.storage || !player.config.storage.enabled) { return; } @@ -2430,7 +2382,7 @@ window.localStorage.removeItem('plyr-volume'); // load value from the current key - value = window.localStorage.getItem(config.storage.key); + value = window.localStorage.getItem(player.config.storage.key); if (!value) { // Key wasn't set (or had been cleared), move along @@ -2451,7 +2403,7 @@ // Save a value back to local storage function updateStorage(value) { // Bail if we don't have localStorage support or it's disabled - if (!support.storage || !config.storage.enabled) { + if (!support.storage || !player.config.storage.enabled) { return; } @@ -2459,52 +2411,52 @@ utils.extend(player.storage, value); // Update storage - window.localStorage.setItem(config.storage.key, JSON.stringify(player.storage)); + window.localStorage.setItem(player.config.storage.key, JSON.stringify(player.storage)); } // Setup media function setupMedia() { // If there's no media, bail - if (!player.elements.media) { + if (!player.media) { warn('No media element found!'); return; } if (player.supported.full) { // Add type class - utils.toggleClass(player.elements.container, config.classes.type.replace('{0}', player.type), true); + utils.toggleClass(player.elements.container, player.config.classes.type.replace('{0}', player.type), true); // Add video class for embeds // This will require changes if audio embeds are added if (utils.inArray(types.embed, player.type)) { - utils.toggleClass(player.elements.container, config.classes.type.replace('{0}', 'video'), true); + utils.toggleClass(player.elements.container, player.config.classes.type.replace('{0}', 'video'), true); } // Check for picture-in-picture support - utils.toggleClass(player.elements.container, config.classes.pip.enabled, support.pip && player.type === 'video'); + utils.toggleClass(player.elements.container, player.config.classes.pip.enabled, support.pip && player.type === 'video'); // Check for airplay support - utils.toggleClass(player.elements.container, config.classes.airplay.enabled, support.airplay && utils.inArray(types.html5, player.type)); + utils.toggleClass(player.elements.container, player.config.classes.airplay.enabled, support.airplay && utils.inArray(types.html5, player.type)); // If there's no autoplay attribute, assume the video is stopped and add state class - utils.toggleClass(player.elements.container, config.classes.stopped, config.autoplay); + utils.toggleClass(player.elements.container, player.config.classes.stopped, player.config.autoplay); // Add iOS class - utils.toggleClass(player.elements.container, config.classes.isIos, player.browser.isIos); + utils.toggleClass(player.elements.container, player.config.classes.isIos, player.browser.isIos); // Add touch class - utils.toggleClass(player.elements.container, config.classes.isTouch, support.touch); + utils.toggleClass(player.elements.container, player.config.classes.isTouch, support.touch); } // Inject the player wrapper if (utils.inArray(['video', 'youtube', 'vimeo'], player.type)) { // Create the wrapper div player.elements.wrapper = utils.createElement('div', { - class: config.classes.videoWrapper + class: player.config.classes.video }); // Wrap the video in a container - utils.wrap(player.elements.media, player.elements.wrapper); + utils.wrap(player.media, player.elements.wrapper); } // Embeds @@ -2536,18 +2488,18 @@ } // Add embed class for responsive - utils.toggleClass(player.elements.wrapper, config.classes.embedWrapper, true); + utils.toggleClass(player.elements.wrapper, player.config.classes.embed, true); if (player.type === 'youtube') { // Set ID - player.elements.media.setAttribute('id', id); + player.media.setAttribute('id', id); // Setup API if (utils.is.object(window.YT)) { youTubeReady(mediaId); } else { // Load the API - utils.injectScript(config.urls.youtube.api); + utils.injectScript(player.config.urls.youtube.api); // Setup callback for the API window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || []; @@ -2566,11 +2518,11 @@ } } else if (player.type === 'vimeo') { // Set ID - player.elements.media.setAttribute('id', id); + player.media.setAttribute('id', id); // Load the API if not already if (!utils.is.object(window.Vimeo)) { - utils.injectScript(config.urls.vimeo.api); + utils.injectScript(player.config.urls.vimeo.api); // Wait for fragaloop load var vimeoTimer = window.setInterval(function() { @@ -2598,11 +2550,11 @@ 'id': id }); - player.elements.media.appendChild(soundCloud); + player.media.appendChild(soundCloud); // Load the API if not already if (!window.SC) { - utils.injectScript(config.urls.soundcloud.api); + utils.injectScript(player.config.urls.soundcloud.api); } // Wait for SC load @@ -2631,15 +2583,15 @@ function youTubeReady(videoId) { // Setup instance // https://developers.google.com/youtube/iframe_api_reference - player.embed = new window.YT.Player(player.elements.media.id, { + player.embed = new window.YT.Player(player.media.id, { videoId: videoId, playerVars: { - autoplay: (config.autoplay ? 1 : 0), + autoplay: (player.config.autoplay ? 1 : 0), controls: (player.supported.full ? 0 : 1), rel: 0, showinfo: 0, iv_load_policy: 3, - cc_load_policy: (config.captions.active ? 1 : 0), + cc_load_policy: (player.config.captions.active ? 1 : 0), cc_lang_pref: 'en', wmode: 'transparent', modestbranding: 1, @@ -2669,22 +2621,22 @@ var instance = event.target; // Create a faux HTML5 API using the YouTube API - player.elements.media.play = function() { + player.media.play = function() { instance.playVideo(); - player.elements.media.paused = false; + player.media.paused = false; }; - player.elements.media.pause = function() { + player.media.pause = function() { instance.pauseVideo(); - player.elements.media.paused = true; + player.media.paused = true; }; - player.elements.media.stop = function() { + player.media.stop = function() { instance.stopVideo(); - player.elements.media.paused = true; + player.media.paused = true; }; - player.elements.media.duration = instance.getDuration(); - player.elements.media.paused = true; - player.elements.media.currentTime = 0; - player.elements.media.muted = instance.isMuted(); + player.media.duration = instance.getDuration(); + player.media.paused = true; + player.media.currentTime = 0; + player.media.muted = instance.isMuted(); // Get available speeds var speed = instance.getPlaybackRate(); @@ -2693,21 +2645,21 @@ console.warn(speed, speedOptions); // Set title - config.title = instance.getVideoData().title; + player.config.title = instance.getVideoData().title; // Set the tabindex if (player.supported.full) { - player.elements.media.setAttribute('tabindex', -1); + player.media.setAttribute('tabindex', -1); } // Update UI embedReady(); // Trigger timeupdate - trigger(player.elements.media, 'timeupdate'); + trigger(player.media, 'timeupdate'); // Trigger timeupdate - trigger(player.elements.media, 'durationchange'); + trigger(player.media, 'durationchange'); // Reset timer window.clearInterval(timers.buffering); @@ -2715,22 +2667,22 @@ // Setup buffering timers.buffering = window.setInterval(function() { // Get loaded % from YouTube - player.elements.media.buffered = instance.getVideoLoadedFraction(); + player.media.buffered = instance.getVideoLoadedFraction(); // Trigger progress only when we actually buffer something - if (player.elements.media.lastBuffered === null || player.elements.media.lastBuffered < player.elements.media.buffered) { - trigger(player.elements.media, 'progress'); + if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) { + trigger(player.media, 'progress'); } // Set last buffer point - player.elements.media.lastBuffered = player.elements.media.buffered; + player.media.lastBuffered = player.media.buffered; // Bail if we're at 100% - if (player.elements.media.buffered === 1) { + if (player.media.buffered === 1) { window.clearInterval(timers.buffering); // Trigger event - trigger(player.elements.media, 'canplaythrough'); + trigger(player.media, 'canplaythrough'); } }, 200); }, @@ -2751,43 +2703,43 @@ switch (event.data) { case 0: // YouTube doesn't support loop for a single video, so mimick it. - if (config.loop.active) { + if (player.config.loop.active) { // YouTube needs a call to `stopVideo` before playing again instance.stopVideo(); instance.playVideo(); break; } - player.elements.media.paused = true; - trigger(player.elements.media, 'ended'); + player.media.paused = true; + trigger(player.media, 'ended'); break; case 1: - player.elements.media.paused = false; + player.media.paused = false; // If we were seeking, fire seeked event - if (player.elements.media.seeking) { - trigger(player.elements.media, 'seeked'); + if (player.media.seeking) { + trigger(player.media, 'seeked'); } - player.elements.media.seeking = false; - trigger(player.elements.media, 'play'); - trigger(player.elements.media, 'playing'); + player.media.seeking = false; + trigger(player.media, 'play'); + trigger(player.media, 'playing'); // Poll to get playback progress timers.playing = window.setInterval(function() { // Set the current time - player.elements.media.currentTime = instance.getCurrentTime(); + player.media.currentTime = instance.getCurrentTime(); // Trigger timeupdate - trigger(player.elements.media, 'timeupdate'); + trigger(player.media, 'timeupdate'); }, 100); // Check duration again due to YouTube bug // https://github.com/sampotts/plyr/issues/374 // https://code.google.com/p/gdata-issues/issues/detail?id=8690 - if (player.elements.media.duration !== instance.getDuration()) { - player.elements.media.duration = instance.getDuration(); - trigger(player.elements.media, 'durationchange'); + if (player.media.duration !== instance.getDuration()) { + player.media.duration = instance.getDuration(); + trigger(player.media, 'durationchange'); } // Get quality @@ -2798,8 +2750,8 @@ break; case 2: - player.elements.media.paused = true; - trigger(player.elements.media, 'pause'); + player.media.paused = true; + trigger(player.media, 'pause'); break; } @@ -2815,47 +2767,47 @@ function vimeoReady(mediaId) { // Setup instance // https://github.com/vimeo/player.js - player.embed = new window.Vimeo.Player(player.elements.media, { + player.embed = new window.Vimeo.Player(player.media, { id: mediaId, - loop: config.loop.active, - autoplay: config.autoplay, + loop: player.config.loop.active, + autoplay: player.config.autoplay, byline: false, portrait: false, title: false }); // Create a faux HTML5 API using the Vimeo API - player.elements.media.play = function() { + player.media.play = function() { player.embed.play(); - player.elements.media.paused = false; + player.media.paused = false; }; - player.elements.media.pause = function() { + player.media.pause = function() { player.embed.pause(); - player.elements.media.paused = true; + player.media.paused = true; }; - player.elements.media.stop = function() { + player.media.stop = function() { player.embed.stop(); - player.elements.media.paused = true; + player.media.paused = true; }; - player.elements.media.paused = true; - player.elements.media.currentTime = 0; + player.media.paused = true; + player.media.currentTime = 0; // Update UI embedReady(); player.embed.getCurrentTime().then(function(value) { - player.elements.media.currentTime = value; + player.media.currentTime = value; // Trigger timeupdate - trigger(player.elements.media, 'timeupdate'); + trigger(player.media, 'timeupdate'); }); player.embed.getDuration().then(function(value) { - player.elements.media.duration = value; + player.media.duration = value; // Trigger timeupdate - trigger(player.elements.media, 'durationchange'); + trigger(player.media, 'durationchange'); }); // Get captions @@ -2864,8 +2816,8 @@ setupCaptions(tracks); // TODO: Captions - if (config.captions.active) { - player.embed.enableTextTrack(config.captions.language.toLowerCase()); + if (player.config.captions.active) { + player.embed.enableTextTrack(player.config.captions.language.toLowerCase()); } }); @@ -2888,41 +2840,41 @@ }); player.embed.on('play', function() { - player.elements.media.paused = false; - trigger(player.elements.media, 'play'); - trigger(player.elements.media, 'playing'); + player.media.paused = false; + trigger(player.media, 'play'); + trigger(player.media, 'playing'); }); player.embed.on('pause', function() { - player.elements.media.paused = true; - trigger(player.elements.media, 'pause'); + player.media.paused = true; + trigger(player.media, 'pause'); }); player.embed.on('timeupdate', function(data) { - player.elements.media.seeking = false; - player.elements.media.currentTime = data.seconds; - trigger(player.elements.media, 'timeupdate'); + player.media.seeking = false; + player.media.currentTime = data.seconds; + trigger(player.media, 'timeupdate'); }); player.embed.on('progress', function(data) { - player.elements.media.buffered = data.percent; - trigger(player.elements.media, 'progress'); + player.media.buffered = data.percent; + trigger(player.media, 'progress'); if (parseInt(data.percent) === 1) { // Trigger event - trigger(player.elements.media, 'canplaythrough'); + trigger(player.media, 'canplaythrough'); } }); player.embed.on('seeked', function() { - player.elements.media.seeking = false; - trigger(player.elements.media, 'seeked'); - trigger(player.elements.media, 'play'); + player.media.seeking = false; + trigger(player.media, 'seeked'); + trigger(player.media, 'play'); }); player.embed.on('ended', function() { - player.elements.media.paused = true; - trigger(player.elements.media, 'ended'); + player.media.paused = true; + trigger(player.media, 'ended'); }); } @@ -2934,219 +2886,75 @@ // Setup on ready player.embed.bind(window.SC.Widget.Events.READY, function() { // Create a faux HTML5 API using the Soundcloud API - player.elements.media.play = function() { + player.media.play = function() { player.embed.play(); - player.elements.media.paused = false; + player.media.paused = false; }; - player.elements.media.pause = function() { + player.media.pause = function() { player.embed.pause(); - player.elements.media.paused = true; + player.media.paused = true; }; - player.elements.media.stop = function() { + player.media.stop = function() { player.embed.seekTo(0); player.embed.pause(); - player.elements.media.paused = true; + player.media.paused = true; }; - player.elements.media.paused = true; - player.elements.media.currentTime = 0; + player.media.paused = true; + player.media.currentTime = 0; player.embed.getDuration(function(value) { - player.elements.media.duration = value / 1000; + player.media.duration = value / 1000; // Update UI embedReady(); }); player.embed.getPosition(function(value) { - player.elements.media.currentTime = value; + player.media.currentTime = value; // Trigger timeupdate - trigger(player.elements.media, 'timeupdate'); + trigger(player.media, 'timeupdate'); }); player.embed.bind(window.SC.Widget.Events.PLAY, function() { - player.elements.media.paused = false; - trigger(player.elements.media, 'play'); - trigger(player.elements.media, 'playing'); + player.media.paused = false; + trigger(player.media, 'play'); + trigger(player.media, 'playing'); }); player.embed.bind(window.SC.Widget.Events.PAUSE, function() { - player.elements.media.paused = true; - trigger(player.elements.media, 'pause'); + player.media.paused = true; + trigger(player.media, 'pause'); }); player.embed.bind(window.SC.Widget.Events.PLAY_PROGRESS, function(data) { - player.elements.media.seeking = false; - player.elements.media.currentTime = data.currentPosition / 1000; - trigger(player.elements.media, 'timeupdate'); + player.media.seeking = false; + player.media.currentTime = data.currentPosition / 1000; + trigger(player.media, 'timeupdate'); }); player.embed.bind(window.SC.Widget.Events.LOAD_PROGRESS, function(data) { - player.elements.media.buffered = data.loadProgress; - trigger(player.elements.media, 'progress'); + player.media.buffered = data.loadProgress; + trigger(player.media, 'progress'); if (parseInt(data.loadProgress) === 1) { // Trigger event - trigger(player.elements.media, 'canplaythrough'); + trigger(player.media, 'canplaythrough'); } }); player.embed.bind(window.SC.Widget.Events.FINISH, function() { - player.elements.media.paused = true; - trigger(player.elements.media, 'ended'); + player.media.paused = true; + trigger(player.media, 'ended'); }); }); } - // Play media - function play() { - if ('play' in player.elements.media) { - player.elements.media.play(); - } - } - - // Pause media - function pause() { - if ('pause' in player.elements.media) { - player.elements.media.pause(); - } - } - - // Toggle playback - function togglePlay(toggle) { - // True toggle - if (!utils.is.boolean(toggle)) { - toggle = player.elements.media.paused; - } - - if (toggle) { - play(); - } else { - pause(); - } - - return toggle; - } - - // Toggle loop - // TODO: Set the indicator on load as user may pass loop as config - function toggleLoop(type) { - // Set default to be a true toggle - if (!utils.inArray(['start', 'end', 'all', 'none', 'toggle'], type)) { - type = 'toggle'; - } - - var currentTime = Number(player.elements.media.currentTime); - - switch (type) { - case 'start': - if (config.loop.end && config.loop.end <= currentTime) { - config.loop.end = null; - } - config.loop.start = currentTime; - config.loop.indicator.start = player.elements.display.played.value; - break; - - case 'end': - if (config.loop.start >= currentTime) { - return; - } - config.loop.end = currentTime; - config.loop.indicator.end = player.elements.display.played.value; - break; - - case 'all': - config.loop.start = 0; - config.loop.end = player.elements.media.duration - 2; - config.loop.indicator.start = 0; - config.loop.indicator.end = 100; - break; - - case 'toggle': - if (config.loop.active) { - config.loop.start = 0; - config.loop.end = null; - } else { - config.loop.start = 0; - config.loop.end = player.elements.media.duration - 2; - } - break; - - default: - config.loop.start = 0; - config.loop.end = null; - break; - } - - // Check if can loop - config.loop.active = utils.is.number(config.loop.start) && utils.is.number(config.loop.end); - var start = updateTimeDisplay(config.loop.start, getElement('[data-plyr-loop="start"]')); - var end = null; - - if (utils.is.number(config.loop.end)) { - // Find the <span> inside button - end = updateTimeDisplay(config.loop.end, document.querySelector('[data-loop__value="loopout"]')); - } else { - // Find the <span> inside button - //end = document.querySelector('[data-loop__value="loopout"]').innerHTML = ''; - } - - if (config.loop.active) { - // TODO: Improve the design of the loop indicator and put styling in CSS where it's meant to be - //getElement('[data-menu="loop"]').innerHTML = start + ' - ' + end; - //getElement(config.selectors.progress.looped).style.position = 'absolute'; - //getElement(config.selectors.progress.looped).style.left = config.loopinPositionPercentage + '%'; - //getElement(config.selectors.progress.looped).style.width = (config.loopoutPositionPercentage - config.loopinPositionPercentage) + '%'; - //getElement(config.selectors.progress.looped).style.background = '#ffbb00'; - //getElement(config.selectors.progress.looped).style.height = '3px'; - //getElement(config.selectors.progress.looped).style.top = '3px'; - //getElement(config.selectors.progress.looped).style['border-radius'] = '100px'; - } else { - //getElement('[data-menu="loop"]').innerHTML = config.i18n.loopNone; - //getElement(config.selectors.progress.looped).style.width = '0px'; - } - } - - // Set playback speed - function setSpeed(speed) { - // Load speed from storage or default value - if (utils.is.event(speed)) { - speed = parseFloat(speed.target.value); - } else if (!utils.is.number(speed)) { - speed = parseFloat(player.storage.speed || config.speed.selected); - } - - // Set min/max - if (speed < 0.1) { - speed = 0.1; - } - if (speed > 2.0) { - speed = 2.0; - } - - if (!utils.is.array(config.speed.options)) { - warn('Invalid speeds format'); - return; - } - - // Store current speed - config.speed.selected = speed; - - // Set HTML5 speed - // TODO: set YouTube - player.elements.media.playbackRate = speed; - - // Save speed to localStorage - updateStorage({ - speed: speed - }); - } - // Get the current speed value function getSpeedLabel(speed) { if (!utils.is.number(speed)) { - speed = config.speed.selected; + speed = player.config.speed.selected; } if (speed === 1) { @@ -3156,182 +2964,13 @@ return speed + '×'; } - // Rewind - function rewind(seekTime) { - // Use default if needed - if (!utils.is.number(seekTime)) { - seekTime = config.seekTime; - } - seek(player.elements.media.currentTime - seekTime); - } - - // Fast forward - function forward(seekTime) { - // Use default if needed - if (!utils.is.number(seekTime)) { - seekTime = config.seekTime; - } - seek(player.elements.media.currentTime + seekTime); - } - - // Seek to time - // The input parameter can be an event or a number - function seek(input) { - var targetTime = 0; - var paused = player.elements.media.paused; - var duration = getDuration(); - - if (utils.is.number(input)) { - targetTime = input; - } else if (utils.is.event(input) && utils.inArray(['input', 'change'], input.type)) { - // It's the seek slider - // Seek to the selected time - targetTime = ((input.target.value / input.target.max) * duration); - } - - // Normalise targetTime - if (targetTime < 0) { - targetTime = 0; - } else if (targetTime > duration) { - targetTime = duration; - } - - // Update seek range and progress - updateSeekDisplay(targetTime); - - // Set the current time - // Try/catch incase the media isn't set and we're calling seek() from source() and IE moans - try { - player.elements.media.currentTime = targetTime.toFixed(4); - } catch (e) {} - - // Embeds - if (utils.inArray(types.embed, player.type)) { - switch (player.type) { - case 'youtube': - player.embed.seekTo(targetTime); - break; - - case 'vimeo': - // Round to nearest second for vimeo - player.embed.setCurrentTime(targetTime.toFixed(0)); - break; - - case 'soundcloud': - player.embed.seekTo(targetTime * 1000); - break; - } - - if (paused) { - pause(); - } - - // Trigger timeupdate - trigger(player.elements.media, 'timeupdate'); - - // Set seeking flag - player.elements.media.seeking = true; - - // Trigger seeking - trigger(player.elements.media, 'seeking'); - } - - // Logging - log('Seeking to ' + player.elements.media.currentTime + ' seconds'); - } - - // Get the duration (or custom if set) - function getDuration() { - // It should be a number, but parse it just incase - var duration = parseInt(config.duration); - - // True duration - var mediaDuration = 0; - - // Only if duration available - if (player.elements.media.duration !== null && !isNaN(player.elements.media.duration)) { - mediaDuration = player.elements.media.duration; - } - - // If custom duration is funky, use regular duration - return (isNaN(duration) ? mediaDuration : duration); - } - // Check playing state function checkPlaying() { - utils.toggleClass(player.elements.container, config.classes.playing, !player.elements.media.paused); + utils.toggleClass(player.elements.container, player.config.classes.playing, !player.media.paused); - utils.toggleClass(player.elements.container, config.classes.stopped, player.elements.media.paused); + utils.toggleClass(player.elements.container, player.config.classes.stopped, player.media.paused); - toggleControls(player.elements.media.paused); - } - - // Save scroll position - function saveScrollPosition() { - scroll = { - x: window.pageXOffset || 0, - y: window.pageYOffset || 0 - }; - } - - // Restore scroll position - function restoreScrollPosition() { - window.scrollTo(scroll.x, scroll.y); - } - - // Toggle fullscreen - function toggleFullscreen(event) { - // Check for native support - var nativeSupport = support.fullscreen; - - if (nativeSupport) { - // If it's a fullscreen change event, update the UI - if (event && event.type === fullscreen.eventType) { - player.fullscreen.active = fullscreen.isFullScreen(player.elements.container); - } else { - // Else it's a user request to enter or exit - if (!fullscreen.isFullScreen(player.elements.container)) { - // Save scroll position - saveScrollPosition(); - - // Request full screen - fullscreen.requestFullScreen(player.elements.container); - } else { - // Bail from fullscreen - fullscreen.cancelFullScreen(); - } - - // Check if we're actually full screen (it could fail) - player.fullscreen.active = fullscreen.isFullScreen(player.elements.container); - - return; - } - } else { - // Otherwise, it's a simple toggle - player.fullscreen.active = !player.fullscreen.active; - - // Bind/unbind escape key - document.body.style.overflow = player.fullscreen.active ? 'hidden' : ''; - } - - // Set class hook - utils.toggleClass(player.elements.container, config.classes.fullscreen.active, player.fullscreen.active); - - // Trap focus - focusTrap(player.fullscreen.active); - - // Set button state - if (player.elements.buttons && player.elements.buttons.fullscreen) { - utils.toggleState(player.elements.buttons.fullscreen, player.fullscreen.active); - } - - // Trigger an event - trigger(player.elements.container, player.fullscreen.active ? 'enterfullscreen' : 'exitfullscreen', true); - - // Restore scroll position - if (!player.fullscreen.active && nativeSupport) { - restoreScrollPosition(); - } + player.toggleControls(player.media.paused); } // Show/hide menu @@ -3471,131 +3110,10 @@ pane.removeAttribute('tabindex'); } - // Mute - function toggleMute(muted) { - // If the method is called without parameter, toggle based on current value - if (!utils.is.boolean(muted)) { - muted = !player.elements.media.muted; - } - - // Set button state - utils.toggleState(player.elements.buttons.mute, muted); - - // Set mute on the player - player.elements.media.muted = muted; - - // If volume is 0 after unmuting, set to default - if (player.elements.media.volume === 0) { - setVolume(config.volume); - } - - // Embeds - if (utils.inArray(types.embed, player.type)) { - // YouTube - switch (player.type) { - case 'youtube': - player.embed[player.elements.media.muted ? 'mute' : 'unMute'](); - break; - - case 'vimeo': - case 'soundcloud': - player.embed.setVolume(player.elements.media.muted ? 0 : parseFloat(config.volume / 10)); - break; - } - - // Trigger volumechange for embeds - trigger(player.elements.media, 'volumechange'); - } - } - - // Set volume - function setVolume(volume) { - var max = 10; - var min = 0; - - // If volume is event, get from input - if (utils.is.event(volume)) { - volume = volume.target.value; - } - - // Load volume from storage if no value specified - if (utils.is.undefined(volume)) { - volume = player.storage.volume; - } - - // Use config if all else fails - if (volume === null || isNaN(volume)) { - volume = config.volume; - } - - // Maximum is volumeMax - if (volume > max) { - volume = max; - } - // Minimum is volumeMin - if (volume < min) { - volume = min; - } - - // Set the player volume - player.elements.media.volume = parseFloat(volume / max); - - // Set the display - if (player.elements.display.volume) { - player.elements.display.volume.value = volume; - } - - // Embeds - if (utils.inArray(types.embed, player.type)) { - switch (player.type) { - case 'youtube': - player.embed.setVolume(player.elements.media.volume * 100); - break; - - case 'vimeo': - case 'soundcloud': - player.embed.setVolume(player.elements.media.volume); - break; - } - - // Trigger volumechange for embeds - trigger(player.elements.media, 'volumechange'); - } - - // Toggle muted state - if (volume === 0) { - player.elements.media.muted = true; - } else if (player.elements.media.muted && volume > 0) { - toggleMute(); - } - } - - // Increase volume - function increaseVolume(step) { - var volume = player.elements.media.muted ? 0 : (player.elements.media.volume * 10); - - if (!utils.is.number(step)) { - step = 1; - } - - setVolume(volume + step); - } - - // Decrease volume - function decreaseVolume(step) { - var volume = player.elements.media.muted ? 0 : (player.elements.media.volume * 10); - - if (!utils.is.number(step)) { - step = 1; - } - - setVolume(volume - step); - } - // Update volume UI and storage function updateVolume() { // Get the current volume - var volume = player.elements.media.muted ? 0 : (player.elements.media.volume * 10); + var volume = player.media.muted ? 0 : (player.media.volume * 10); // Update the <input type="range"> if present if (player.supported.full) { @@ -3613,7 +3131,7 @@ }); // Toggle class if muted - utils.toggleClass(player.elements.container, config.classes.muted, (volume === 0)); + utils.toggleClass(player.elements.container, player.config.classes.muted, (volume === 0)); // Update checkbox for mute state if (player.supported.full && player.elements.buttons.mute) { @@ -3623,7 +3141,7 @@ // Check if media is loading function checkLoading(event) { - var loading = (event.type === 'waiting'); + player.loading = (event.type === 'waiting'); // Clear timer clearTimeout(timers.loading); @@ -3631,11 +3149,11 @@ // Timer to prevent flicker when seeking timers.loading = setTimeout(function() { // Toggle container class hook - utils.toggleClass(player.elements.container, config.classes.loading, loading); + utils.toggleClass(player.elements.container, player.config.classes.loading, player.loading); // Show controls if loading, hide if done - toggleControls(loading); - }, (loading ? 250 : 0)); + player.toggleControls(player.loading); + }, (player.loading ? 250 : 0)); } // Update <progress> elements @@ -3644,9 +3162,9 @@ return; } - var progress = player.elements.display.played, - value = 0, - duration = getDuration(); + var progress = player.elements.display.played; + var value = 0; + var duration = player.getDuration(); if (event) { switch (event.type) { @@ -3657,7 +3175,7 @@ return; } - value = utils.getPercentage(player.elements.media.currentTime, duration); + value = utils.getPercentage(player.media.currentTime, duration); // Set seek range value only if it's a 'natural' time event if (event.type === 'timeupdate' && player.elements.inputs.seek) { @@ -3671,7 +3189,7 @@ case 'progress': progress = player.elements.display.buffer; value = (function() { - var buffered = player.elements.media.buffered; + var buffered = player.media.buffered; if (buffered && buffered.length) { // HTML5 @@ -3688,8 +3206,8 @@ } } - if (utils.is.number(config.loop.start) && utils.is.number(config.loop.end) && player.elements.media.currentTime >= config.loop.end) { - seek(config.loop.start); + if (utils.is.number(player.config.loop.start) && utils.is.number(player.config.loop.end) && player.media.currentTime >= player.config.loop.end) { + player.seek(player.config.loop.start); } setProgress(progress, value); @@ -3741,9 +3259,10 @@ var secs = parseInt(time % 60); var mins = parseInt((time / 60) % 60); var hours = parseInt(((time / 60) / 60) % 60); + var duration = player.getDuration(); // Do we need to display hours? - var displayHours = (parseInt(((getDuration() / 60) / 60) % 60) > 0); + var displayHours = (parseInt(((duration / 60) / 60) % 60) > 0); // Ensure it's two digits. For example, 03 rather than 3. secs = ('0' + secs).slice(-2); @@ -3766,10 +3285,10 @@ } // Determine duration - var duration = getDuration() || 0; + var duration = player.getDuration() || 0; // If there's only one time display, display duration there - if (!player.elements.display.duration && config.displayDuration && player.elements.media.paused) { + if (!player.elements.display.duration && player.config.displayDuration && player.media.paused) { updateTimeDisplay(duration, player.elements.display.currentTime); } @@ -3785,10 +3304,10 @@ // Handle time change event function timeUpdate(event) { // Duration - updateTimeDisplay(player.elements.media.currentTime, player.elements.display.currentTime); + updateTimeDisplay(player.media.currentTime, player.elements.display.currentTime); // Ignore updates while seeking - if (event && event.type === 'timeupdate' && player.elements.media.seeking) { + if (event && event.type === 'timeupdate' && player.media.seeking) { return; } @@ -3803,8 +3322,8 @@ time = 0; } - var duration = getDuration(), - value = utils.getPercentage(time, duration); + var duration = player.getDuration(); + var value = utils.getPercentage(time, duration); // Update progress if (player.elements.progress && player.elements.display.played) { @@ -3819,17 +3338,17 @@ // Update hover tooltip for seeking function updateSeekTooltip(event) { - var duration = getDuration(); + var duration = player.getDuration(); // Bail if setting not true - if (!config.tooltips.seek || !utils.is.htmlElement(player.elements.inputs.seek) || !utils.is.htmlElement(player.elements.display.seekTooltip) || duration === 0) { + if (!player.config.tooltips.seek || !utils.is.htmlElement(player.elements.inputs.seek) || !utils.is.htmlElement(player.elements.display.seekTooltip) || duration === 0) { return; } // Calculate percentage var clientRect = player.elements.inputs.seek.getBoundingClientRect(); var percent = 0; - var visible = config.classes.tooltip + '--visible'; + var visible = player.config.classes.tooltip + '--visible'; // Determine percentage, if already visible if (utils.is.event(event)) { @@ -3862,108 +3381,6 @@ } } - // 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 - if (!config.hideControls || player.type === 'audio') { - return; - } - - var delay = 0; - var isEnterFullscreen = false; - var show = toggle; - var loading = utils.hasClass(player.elements.container, config.classes.loading); - - // Default to false if no boolean - if (!utils.is.boolean(toggle)) { - if (toggle && toggle.type) { - // Is the enter fullscreen event - isEnterFullscreen = (toggle.type === 'enterfullscreen'); - - // Whether to show controls - show = utils.inArray(['mousemove', 'touchstart', 'mouseenter', 'focus'], toggle.type); - - // Delay hiding on move events - if (utils.inArray(['mousemove', 'touchmove'], toggle.type)) { - delay = 2000; - } - - // Delay a little more for keyboard users - if (toggle.type === 'focus') { - delay = 3000; - } - } else { - show = utils.hasClass(player.elements.container, config.classes.hideControls); - } - } - - // Clear timer every movement - window.clearTimeout(timers.hover); - - // If the mouse is not over the controls, set a timeout to hide them - if (show || player.elements.media.paused || loading) { - utils.toggleClass(player.elements.container, config.classes.hideControls, false); - - // Always show controls when paused or if touch - if (player.elements.media.paused || loading) { - return; - } - - // Delay for hiding on touch - if (support.touch) { - delay = 3000; - } - } - - // If toggle is false or if we're playing (regardless of toggle), - // then set the timer to hide the controls - if (!show || !player.elements.media.paused) { - timers.hover = window.setTimeout(function() { - // If the mouse is over the controls (and not entering fullscreen), bail - if ((player.elements.controls.pressed || player.elements.controls.hover) && !isEnterFullscreen) { - return; - } - - utils.toggleClass(player.elements.container, config.classes.hideControls, true); - }, delay); - } - } - - // Add common function to retrieve media source - function source(source) { - // If not null or undefined, parse it - if (!utils.is.undefined(source)) { - updateSource(source); - return; - } - - // Return the current source - var url; - switch (player.type) { - case 'youtube': - url = player.embed.getVideoUrl(); - break; - - case 'vimeo': - player.embed.getVideoUrl.then(function(value) { - url = value; - }); - break; - - case 'soundcloud': - player.embed.getCurrentSound(function(object) { - url = object.permalink_url; - }); - break; - - default: - url = player.elements.media.currentSrc; - break; - } - - return url || ''; - } - // Update source // Sources are not checked for support so be careful function updateSource(source) { @@ -3973,10 +3390,10 @@ } // Remove ready class hook - utils.toggleClass(player.elements.container, config.classes.ready, false); + player.ready = false; - // Pause playback - pause(); + // Stop playback + player.stop(); // Update seek range and progress updateSeekDisplay(); @@ -4021,56 +3438,56 @@ } // Check for support - player.supported = utils.checkSupport(player.type, config.inline); + player.supported = utils.checkSupport(player.type, player.config.inline); // Create new markup switch (player.type) { case 'video': - player.elements.media = utils.createElement('video'); + player.media = utils.createElement('video'); break; case 'audio': - player.elements.media = utils.createElement('audio'); + player.media = utils.createElement('audio'); break; case 'youtube': case 'vimeo': case 'soundcloud': - player.elements.media = utils.createElement('div'); + player.media = utils.createElement('div'); player.embedId = source.sources[0].src; break; } // Inject the new element - utils.prependChild(player.elements.container, player.elements.media); + utils.prependChild(player.elements.container, player.media); // Autoplay the new source? if (utils.is.boolean(source.autoplay)) { - config.autoplay = source.autoplay; + player.config.autoplay = source.autoplay; } // Set attributes for audio and video if (utils.inArray(types.html5, player.type)) { - if (config.crossorigin) { - player.elements.media.setAttribute('crossorigin', ''); + if (player.config.crossorigin) { + player.media.setAttribute('crossorigin', ''); } - if (config.autoplay) { - player.elements.media.setAttribute('autoplay', ''); + if (player.config.autoplay) { + player.media.setAttribute('autoplay', ''); } if ('poster' in source) { - player.elements.media.setAttribute('poster', source.poster); + player.media.setAttribute('poster', source.poster); } - if (config.loop.active) { - player.elements.media.setAttribute('loop', ''); + if (player.config.loop.active) { + player.media.setAttribute('loop', ''); } - if (config.inline) { - player.elements.media.setAttribute('playsinline', ''); + if (player.config.inline) { + player.media.setAttribute('playsinline', ''); } } // Restore class hooks - utils.toggleClass(player.elements.container, config.classes.fullscreen.active, player.fullscreen.active); - utils.toggleClass(player.elements.container, config.classes.captions.active, player.captions.enabled); + utils.toggleClass(player.elements.container, player.config.classes.fullscreen.active, player.fullscreen.active); + utils.toggleClass(player.elements.container, player.config.classes.captions.active, player.captions.enabled); toggleStyleHook(); // Set new sources for html5 @@ -4089,7 +3506,7 @@ } // Load HTML5 sources - player.elements.media.load(); + player.media.load(); } // If HTML5 or embed but not fully supported, setupInterface and call ready now @@ -4102,20 +3519,13 @@ } // Set aria title and iframe title - config.title = source.title; + player.config.title = source.title; setTitle(); } // Destroy instance adn wait for callback // Vimeo throws a wobbly if you don't wait - destroy(setup, false); - } - - // Update poster - function updatePoster(source) { - if (player.type === 'video') { - player.elements.media.setAttribute('poster', source); - } + player.destroy(setup, false); } // Listen for control events @@ -4124,8 +3534,8 @@ var inputEvent = (player.browser.isIE ? 'change' : 'input'); // Click play/pause helper - function _togglePlay() { - var play = togglePlay(); + function togglePlay() { + var play = player.togglePlay(); // Determine which buttons var trigger = player.elements.buttons[play ? 'play' : 'pause']; @@ -4133,7 +3543,7 @@ // Setup focus and tab focus if (target) { - var hadTabFocus = utils.hasClass(trigger, config.classes.tabFocus); + var hadTabFocus = utils.hasClass(trigger, player.config.classes.tabFocus); setTimeout(function() { if (utils.is.htmlElement(target)) { @@ -4141,8 +3551,8 @@ } if (hadTabFocus) { - utils.toggleClass(trigger, config.classes.tabFocus, false); - utils.toggleClass(target, config.classes.tabFocus, true); + utils.toggleClass(trigger, player.config.classes.tabFocus, false); + utils.toggleClass(target, player.config.classes.tabFocus, true); } }, 100); } @@ -4155,19 +3565,19 @@ // Detect tab focus function checkTabFocus(focused) { - utils.toggleClass(getElements('.' + config.classes.tabFocus), config.classes.tabFocus, false); + utils.toggleClass(getElements('.' + player.config.classes.tabFocus), player.config.classes.tabFocus, false); if (player.elements.container.contains(focused)) { - utils.toggleClass(focused, config.classes.tabFocus, true); + utils.toggleClass(focused, player.config.classes.tabFocus, true); } } // Keyboard shortcuts - if (config.keyboardShortcuts.focused) { + if (player.config.keyboardShortcuts.focused) { var last = null; // Handle global presses - if (config.keyboardShortcuts.global) { + if (player.config.keyboardShortcuts.global) { utils.on(window, 'keydown keyup', function(event) { var code = getKeyCode(event); var focused = utils.getFocusElement(); @@ -4176,7 +3586,7 @@ // Only handle global key press if key is in the allowed keys // and if the focused element is not editable (e.g. text input) // and any that accept key input http://webaim.org/techniques/keyboard/ - if (utils.inArray(allowed, code) && (!utils.is.htmlElement(focused) || !utils.matches(focused, config.selectors.editable))) { + if (utils.inArray(allowed, code) && (!utils.is.htmlElement(focused) || !utils.matches(focused, player.config.selectors.editable))) { handleKey(event); } }, false); @@ -4200,7 +3610,7 @@ // Seek by the number keys function seekByKey() { // Get current duration - var duration = player.elements.media.duration; + var duration = player.media.duration; // Bail if we have no duration set if (!utils.is.number(duration)) { @@ -4208,7 +3618,7 @@ } // Divide the max duration into 10th's and times by the number value - seek((duration / 10) * (code - 48)); + player.seek((duration / 10) * (code - 48)); } // Handle the key on keydown @@ -4253,66 +3663,66 @@ case 75: // Space and K key if (!held) { - _togglePlay(); + togglePlay(); } break; case 38: // Arrow up - increaseVolume(); + player.increaseVolume(); break; case 40: // Arrow down - decreaseVolume(); + player.decreaseVolume(); break; case 77: // M key if (!held) { - toggleMute(); + player.toggleMute(); } break; case 39: // Arrow forward - forward(); + player.forward(); break; case 37: // Arrow back - rewind(); + player.rewind(); break; case 70: // F key - toggleFullscreen(); + player.toggleFullscreen(); break; case 67: // C key if (!held) { - toggleCaptions(); + player.toggleCaptions(); } break; case 73: - toggleLoop('start'); + player.loop('start'); break; case 76: - toggleLoop(); + player.loop(); break; case 79: - toggleLoop('end'); + player.loop('end'); break; } // Escape is handle natively when in full screen // So we only need to worry about non native if (!support.fullscreen && player.fullscreen.active && code === 27) { - toggleFullscreen(); + player.toggleFullscreen(); } // Store last code for next cycle @@ -4332,7 +3742,7 @@ } }); utils.on(document.body, 'click', function() { - utils.toggleClass(getElement('.' + config.classes.tabFocus), config.classes.tabFocus, false); + utils.toggleClass(getElement('.' + player.config.classes.tabFocus), player.config.classes.tabFocus, false); }); for (var button in player.elements.buttons) { var element = player.elements.buttons[button]; @@ -4353,44 +3763,50 @@ } // Play - utils.proxy(player.elements.buttons.play, 'click', config.listeners.play, _togglePlay); - utils.proxy(player.elements.buttons.playLarge, 'click', config.listeners.play, _togglePlay); + utils.proxy(player.elements.buttons.play, 'click', player.config.listeners.play, togglePlay); + utils.proxy(player.elements.buttons.playLarge, 'click', player.config.listeners.play, togglePlay); // Pause - utils.proxy(player.elements.buttons.pause, 'click', config.listeners.pause, _togglePlay); + utils.proxy(player.elements.buttons.pause, 'click', player.config.listeners.pause, togglePlay); // Pause - utils.proxy(player.elements.buttons.restart, 'click', config.listeners.restart, seek); + utils.proxy(player.elements.buttons.restart, 'click', player.config.listeners.restart, function() { + player.restart(); + }); // Rewind - utils.proxy(player.elements.buttons.rewind, 'click', config.listeners.rewind, rewind); + utils.proxy(player.elements.buttons.rewind, 'click', player.config.listeners.rewind, function() { + player.rewind(); + }); // Rewind - utils.proxy(player.elements.buttons.forward, 'click', config.listeners.forward, forward); + utils.proxy(player.elements.buttons.forward, 'click', player.config.listeners.forward, function() { + player.forward(); + }); // Mute - utils.proxy(player.elements.buttons.mute, 'click', config.listeners.mute, toggleMute); + utils.proxy(player.elements.buttons.mute, 'click', player.config.listeners.mute, function() { + player.toggleMute(); + }); // Captions - utils.proxy(player.elements.buttons.captions, 'click', config.listeners.captions, toggleCaptions); + utils.proxy(player.elements.buttons.captions, 'click', player.config.listeners.captions, function() { + player.toggleCaptions(); + }); // Fullscreen - utils.proxy(player.elements.buttons.fullscreen, 'click', config.listeners.fullscreen, toggleFullscreen); + utils.proxy(player.elements.buttons.fullscreen, 'click', player.config.listeners.fullscreen, function(event) { + player.toggleFullscreen(event); + }); // Picture-in-Picture - utils.proxy(player.elements.buttons.pip, 'click', config.listeners.pip, function(event) { - if (!support.pip) { - return; - } - player.elements.media.webkitSetPresentationMode(player.elements.media.webkitPresentationMode === 'picture-in-picture' ? 'inline' : 'picture-in-picture'); + utils.proxy(player.elements.buttons.pip, 'click', player.config.listeners.pip, function() { + player.togglePictureInPicture(); }); // Airplay - utils.proxy(player.elements.buttons.airplay, 'click', config.listeners.airplay, function(event) { - if (!support.airplay) { - return; - } - player.elements.media.webkitShowPlaybackTargetPicker(); + utils.proxy(player.elements.buttons.airplay, 'click', player.config.listeners.airplay, function() { + player.airPlay(); }); // Settings menu @@ -4405,49 +3821,60 @@ // Settings menu items - use event delegation as items are added/removed utils.on(player.elements.settings.form, 'click', function(event) { // Settings - Language - if (utils.matches(event.target, config.selectors.inputs.language)) { - handlerProxy.call(this, event, config.listeners.language, setLanguage); + if (utils.matches(event.target, player.config.selectors.inputs.language)) { + handlerProxy.call(this, event, player.config.listeners.language, function() { + player.language(event.target.value.toLowerCase()); + }); } // Settings - Quality - else if (utils.matches(event.target, config.selectors.inputs.quality)) { - handlerProxy.call(this, event, config.listeners.quality, function() { + else if (utils.matches(event.target, player.config.selectors.inputs.quality)) { + handlerProxy.call(this, event, player.config.listeners.quality, function() { warn("Set quality"); }); } // Settings - Speed - else if (utils.matches(event.target, config.selectors.inputs.speed)) { - handlerProxy.call(this, event, config.listeners.speed, setSpeed); + else if (utils.matches(event.target, player.config.selectors.inputs.speed)) { + handlerProxy.call(this, event, player.config.listeners.speed, function() { + player.setSpeed(parseFloat(event.target.value)); + }); } // Settings - Looping // TODO: use toggle buttons - else if (utils.matches(event.target, config.selectors.buttons.loop)) { - handlerProxy.call(this, event, config.listeners.loop, function() { + else if (utils.matches(event.target, player.config.selectors.buttons.loop)) { + handlerProxy.call(this, event, player.config.listeners.loop, function() { // TODO: This should be done in the method itself I think var value = event.target.getAttribute('data-loop__value') || event.target.getAttribute('data-loop__type'); if (utils.inArray(['start', 'end', 'all', 'none'], value)) { - toggleLoop(value); + player.loop(value); } }); } }); // Seek - utils.proxy(player.elements.inputs.seek, inputEvent, config.listeners.seek, seek); + utils.proxy(player.elements.inputs.seek, inputEvent, player.config.listeners.seek, function(event) { + var duration = player.getDuration(); + player.seek((event.target.value / event.target.max) * duration); + }); // Seek - utils.proxy(player.elements.inputs.volume, inputEvent, config.listeners.volume, setVolume); + utils.proxy(player.elements.inputs.volume, inputEvent, player.config.listeners.volume, function() { + player.setVolume(event.target.value); + }); // Seek tooltip utils.on(player.elements.progress, 'mouseenter mouseleave mousemove', updateSeekTooltip); // Toggle controls visibility based on mouse movement - if (config.hideControls) { + if (player.config.hideControls) { // Toggle controls on mouse events and entering fullscreen - utils.on(player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen', toggleControls); + utils.on(player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen', function(event) { + player.toggleControls(event); + }); // Watch for cursor over controls so they don't hide when trying to interact utils.on(player.elements.controls, 'mouseenter mouseleave', function(event) { @@ -4461,11 +3888,13 @@ // Focus in/out on controls // TODO: Check we need capture here - utils.on(player.elements.controls, 'focus blur', toggleControls, true, true); + utils.on(player.elements.controls, 'focus blur', function(event) { + player.toggleControls(event); + }, true, true); } // Mouse wheel for volume - utils.proxy(player.elements.inputs.volume, 'wheel', config.listeners.volume, function(event) { + utils.proxy(player.elements.inputs.volume, 'wheel', player.config.listeners.volume, function(event) { // Detect "natural" scroll - suppored on OS X Safari only // Other browsers on OS X will be inverted until support improves var inverted = event.webkitDirectionInvertedFromDevice; @@ -4475,10 +3904,10 @@ // Scroll down (or up on natural) to decrease if (event.deltaY < 0 || event.deltaX > 0) { if (inverted) { - decreaseVolume(step); + player.decreaseVolume(step); direction = -1; } else { - increaseVolume(step); + player.increaseVolume(step); direction = 1; } } @@ -4486,68 +3915,70 @@ // Scroll up (or down on natural) to increase if (event.deltaY > 0 || event.deltaX < 0) { if (inverted) { - increaseVolume(step); + player.increaseVolume(step); direction = 1; } else { - decreaseVolume(step); + player.decreaseVolume(step); direction = -1; } } // Don't break page scrolling at max and min - if ((direction === 1 && player.elements.media.volume < 1) || - (direction === -1 && player.elements.media.volume > 0)) { + if ((direction === 1 && player.media.volume < 1) || + (direction === -1 && player.media.volume > 0)) { event.preventDefault(); } }, false); // Handle user exiting fullscreen by escaping etc if (support.fullscreen) { - utils.on(document, fullscreen.eventType, toggleFullscreen); + utils.on(document, fullscreen.eventType, function(event) { + player.toggleFullscreen(event); + }); } } // Listen for media events function mediaListeners() { // Time change on media - utils.on(player.elements.media, 'timeupdate seeking', timeUpdate); + utils.on(player.media, 'timeupdate seeking', timeUpdate); // Display duration - utils.on(player.elements.media, 'durationchange loadedmetadata', displayDuration); + utils.on(player.media, 'durationchange loadedmetadata', displayDuration); // Handle the media finishing - utils.on(player.elements.media, 'ended', function() { + utils.on(player.media, 'ended', function() { // Show poster on end - if (player.type === 'video' && config.showPosterOnEnd) { + if (player.type === 'video' && player.config.showPosterOnEnd) { // Clear if (player.type === 'video') { setCaption(); } // Restart - seek(); + player.restart(); // Re-load media - player.elements.media.load(); + player.media.load(); } }); // Check for buffer progress - utils.on(player.elements.media, 'progress playing', updateProgress); + utils.on(player.media, 'progress playing', updateProgress); // Handle native mute - utils.on(player.elements.media, 'volumechange', updateVolume); + utils.on(player.media, 'volumechange', updateVolume); // Handle native play/pause - utils.on(player.elements.media, 'play pause ended', checkPlaying); + utils.on(player.media, 'play pause ended', checkPlaying); // Loading - utils.on(player.elements.media, 'waiting canplay seeked', checkLoading); + utils.on(player.media, 'waiting canplay seeked', checkLoading); // Click video - if (config.clickToPlay && player.type !== 'audio') { + if (player.config.clickToPlay && player.type !== 'audio') { // Re-fetch the wrapper - var wrapper = getElement('.' + config.classes.videoWrapper); + var wrapper = getElement('.' + player.config.classes.video); // Bail if there's no wrapper (this should never happen) if (!wrapper) { @@ -4560,31 +3991,31 @@ // On click play, pause ore restart utils.on(wrapper, 'click', function() { // Touch devices will just show controls (if we're hiding controls) - if (config.hideControls && support.touch && !player.elements.media.paused) { + if (player.config.hideControls && support.touch && !player.media.paused) { return; } - if (player.elements.media.paused) { - play(); - } else if (player.elements.media.ended) { - seek(); - play(); + if (player.media.paused) { + player.play(); + } else if (player.media.ended) { + player.restart(); + player.play(); } else { - pause(); + player.pause(); } }); } // Disable right click - if (config.disableContextMenu) { - utils.on(player.elements.media, 'contextmenu', function(event) { + if (player.config.disableContextMenu) { + utils.on(player.media, 'contextmenu', function(event) { event.preventDefault(); }, false); } // Proxy events to container // Bubble up key events for Edge - utils.on(player.elements.media, config.events.concat(['keyup', 'keydown']).join(' '), function(event) { + utils.on(player.media, player.config.events.concat(['keyup', 'keydown']).join(' '), function(event) { trigger(player.elements.container, event.type, true); }); } @@ -4597,7 +4028,7 @@ } // Remove child sources - var sources = player.elements.media.querySelectorAll('source'); + var sources = player.media.querySelectorAll('source'); for (var i = 0; i < sources.length; i++) { utils.removeElement(sources[i]); } @@ -4605,84 +4036,17 @@ // Set blank video src attribute // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection - player.elements.media.setAttribute('src', 'https://cdn.selz.com/plyr/blank.mp4'); + player.media.setAttribute('src', 'https://cdn.selz.com/plyr/blank.mp4'); // Load the new empty source // This will cancel existing requests // See https://github.com/sampotts/plyr/issues/174 - player.elements.media.load(); + player.media.load(); // Debugging log('Cancelled network requests'); } - // Destroy an instance - // Event listeners are removed when elements are removed - // http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory - function destroy(callback, restore) { - // Type specific stuff - switch (player.type) { - case 'youtube': - // Clear timers - window.clearInterval(timers.buffering); - window.clearInterval(timers.playing); - - // Destroy YouTube API - player.embed.destroy(); - - // Clean up - cleanUp(); - - break; - - case 'vimeo': - // Destroy Vimeo API - // then clean up (wait, to prevent postmessage errors) - player.embed.unload().then(cleanUp); - - // Vimeo does not always return - window.setTimeout(cleanUp, 200); - - break; - - case 'video': - case 'audio': - // Restore native video controls - toggleNativeControls(true); - - // Clean up - cleanUp(); - - break; - } - - function cleanUp() { - // Default to restore original element - if (!utils.is.boolean(restore)) { - restore = true; - } - - // Callback - if (utils.is.function(callback)) { - callback.call(player.original); - } - - // Bail if we don't need to restore the original element - if (!restore) { - return; - } - - // Replace the container with the original element provided - player.elements.container.parentNode.replaceChild(player.original, player.elements.container); - - // Reset overflow (incase destroyed while fullscreen) - document.body.style.overflow = ''; - - // Event - trigger(player.original, 'destroyed', true); - } - } - // Setup the UI function setupInterface() { // Don't setup interface if no support @@ -4729,14 +4093,14 @@ setupCaptions(); // Set volume - setVolume(); + player.setVolume(); updateVolume(); // Set playback speed - setSpeed(); + player.setSpeed(); // Set loop - toggleLoop(); + player.loop(); // Reset time display timeUpdate(); @@ -4747,46 +4111,40 @@ // Everything done function ready() { - // Set class hook on media element - // utils.toggleClass(player.elements.media, defaults.classes.setup, true); - - // Set container class for ready - // utils.toggleClass(player.elements.container, config.classes.ready, true); - - // Store a refernce to instance - // player.elements.media.plyr = api; - // Ready event at end of execution stack trigger(player.elements.container, 'ready', true); // Autoplay - if (config.autoplay) { - play(); + if (player.config.autoplay) { + player.play(); } } // Setup a player function setup(target) { // We need an element to setup - if (!utils.is.htmlElement(target)) { - error('Setup failed. No suitable element passed.'); - return false; + if (target === null || utils.is.undefined(target) || !utils.is.htmlElement(target)) { + error('Setup failed: no suitable element passed'); + return; } // Bail if not enabled - if (!config.enabled) { - return false; + if (!player.config.enabled) { + error('Setup failed: disabled by config'); + return; } // Bail if disabled or no basic support // You may want to disable certain UAs etc if (!utils.checkSupport().basic) { - return false; + error('Setup failed: no support'); + return; } // Bail if the element is initialized if (target.plyr) { - return false; + log('Target already setup'); + return target.plyr; } // Set media type based on tag or data attribute @@ -4799,8 +4157,14 @@ player.type = target.getAttribute('data-type'); player.embedId = target.getAttribute('data-video-id'); - if (utils.is.empty(player.type) || utils.is.empty(player.embedId)) { - return false; + if (utils.is.empty(player.type)) { + error('Setup failed: embed type missing'); + return; + } + + if (utils.is.empty(player.embedId)) { + error('Setup failed: video id missing'); + return; } // Clean up @@ -4815,13 +4179,14 @@ case 'video': case 'audio': player.type = type; - config.crossorigin = target.getAttribute('crossorigin') !== null; - config.autoplay = config.autoplay || (target.getAttribute('autoplay') !== null); - config.inline = target.getAttribute('playsinline') !== null; - config.loop.active = config.loop || (target.getAttribute('loop') !== null); + player.config.crossorigin = target.getAttribute('crossorigin') !== null; + player.config.autoplay = player.config.autoplay || (target.getAttribute('autoplay') !== null); + player.config.inline = target.getAttribute('playsinline') !== null; + player.config.loop.active = player.config.loop || (target.getAttribute('loop') !== null); break; default: + error('Setup failed: unsupported type'); return false; } @@ -4831,11 +4196,12 @@ // Load saved settings from localStorage setupStorage(); - // Check for support - player.supported = utils.checkSupport(player.type, config.inline); + // Check for support again but with type + player.supported = utils.checkSupport(player.type, player.config.inline); // If no native support, bail if (!player.supported.basic) { + error('Setup failed: no support'); return false; } @@ -4858,8 +4224,8 @@ setupMedia(); // Listen for events if debugging - if (config.debug) { - var events = config.events.concat(['setup', 'statechange', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled']); + if (player.config.debug) { + var events = player.config.events.concat(['setup', 'statechange', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled']); utils.on(player.elements.container, events.join(' '), function(event) { log(['event:', event.type].join(' ').trim()); @@ -4878,90 +4244,853 @@ // Set title on button and frame setTitle(); } - - // Successful setup - return true; } - // Expose prototypes - api = { - getOriginal: function() { - return player.original; - }, - getContainer: function() { - return player.elements.container - }, - getEmbed: function() { - return player.embed; - }, - getMedia: function() { - return player.elements.media; - }, - getType: function() { - return player.type; - }, - getDuration: getDuration, - getCurrentTime: function() { - return player.elements.media.currentTime; - }, - getVolume: function() { - return player.elements.media.volume; - }, - isMuted: function() { - return player.elements.media.muted; - }, - isReady: function() { - return utils.hasClass(player.elements.container, config.classes.ready); - }, - isLoading: function() { - return utils.hasClass(player.elements.container, config.classes.loading); - }, - isPaused: function() { - return player.elements.media.paused; - }, - isLooping: function() { - return config.loop.active; - }, - on: function(event, callback) { - utils.on(player.elements.container, event, callback); - return this; - }, - play: play, - pause: pause, - loop: toggleLoop, - stop: function() { - pause(); - seek(); - }, - restart: seek, - rewind: rewind, - forward: forward, - seek: seek, - source: source, - poster: updatePoster, - setVolume: setVolume, - setSpeed: setSpeed, - togglePlay: togglePlay, - toggleMute: toggleMute, - toggleCaptions: toggleCaptions, - toggleFullscreen: toggleFullscreen, - toggleControls: toggleControls, - setLanguage: setLanguage, - isFullscreen: player.fullscreen.active, - support: function(mimeType) { - return support.mime(player, mimeType); - }, - destroy: destroy + // Expose some core functions + player.core = { + getElement: getElement, + getElements: getElements, + trigger: trigger, + setCaption: setCaption, + setupCaptions: setupCaptions, + toggleNativeControls: toggleNativeControls, + updateTimeDisplay: updateTimeDisplay, + updateSeekDisplay: updateSeekDisplay, + updateSource: updateSource, + updateStorage: updateStorage, + timers: timers, + support: support, + + // Debugging + log: log, + warn: warn, + error: error }; // Initialize instance - if (!setup(player.elements.media)) { - return null; + setup(player.media); + } + + // API + // --------------------------------------- + // Play + Player.prototype.play = function() { + var player = this; + + if ('play' in player.media) { + player.media.play(); } - // Expose API - return api; - } + // Allow chaining + return player; + }; + + // Pause + Player.prototype.pause = function() { + var player = this; + + if ('pause' in player.media) { + player.media.pause(); + } + + // Allow chaining + return player; + }; + + // Toggle playback + Player.prototype.togglePlay = function(toggle) { + var player = this; + + // True toggle if nothing passed + if (!utils.is.boolean(toggle)) { + toggle = player.media.paused; + } + + if (toggle) { + player.play(); + } else { + player.pause(); + } + + return toggle; + }; + + // Stop + Player.prototype.stop = function() { + var player = this; + + player.restart(); + player.pause(); + + // Allow chaining + return player; + }; + + // Restart + Player.prototype.restart = function() { + var player = this; + + // Seek to 0 + player.seek(); + + // Allow chaining + return player; + }; + + // Rewind + Player.prototype.rewind = function(seekTime) { + var player = this; + + // Use default if needed + if (!utils.is.number(seekTime)) { + seekTime = player.config.seekTime; + } + + player.seek(player.media.currentTime - seekTime); + + // Allow chaining + return player; + }; + + // Fast forward + Player.prototype.forward = function(seekTime) { + var player = this; + + // Use default if needed + if (!utils.is.number(seekTime)) { + seekTime = player.config.seekTime; + } + + player.seek(player.media.currentTime + seekTime); + + // Allow chaining + return player; + }; + + // Seek to time + // The input parameter can be an event or a number + Player.prototype.seek = function(input) { + var player = this; + var targetTime = 0; + var paused = player.media.paused; + var duration = player.getDuration(); + + if (utils.is.number(input)) { + targetTime = input; + } + + // Normalise targetTime + if (targetTime < 0) { + targetTime = 0; + } else if (targetTime > duration) { + targetTime = duration; + } + + // Update seek range and progress + player.core.updateSeekDisplay(targetTime); + + // Set the current time + // Try/catch incase the media isn't set and we're calling seek() from source() and IE moans + try { + player.media.currentTime = targetTime.toFixed(4); + } catch (e) {} + + // Embeds + if (utils.inArray(types.embed, player.type)) { + switch (player.type) { + case 'youtube': + player.embed.seekTo(targetTime); + break; + + case 'vimeo': + // Round to nearest second for vimeo + player.embed.setCurrentTime(targetTime.toFixed(0)); + break; + + case 'soundcloud': + player.embed.seekTo(targetTime * 1000); + break; + } + + if (paused) { + player.pause(); + } + + // Trigger timeupdate + player.core.trigger(player.media, 'timeupdate'); + + // Set seeking flag + player.media.seeking = true; + + // Trigger seeking + player.core.trigger(player.media, 'seeking'); + } + + // Logging + player.core.log('Seeking to ' + player.media.currentTime + ' seconds'); + + // Allow chaining + return player; + }; + + // Set volume + Player.prototype.setVolume = function(volume) { + var player = this; + var max = 10; + var min = 0; + + // Load volume from storage if no value specified + if (utils.is.undefined(volume)) { + volume = player.storage.volume; + } + + // Use config if all else fails + if (volume === null || isNaN(volume)) { + volume = player.config.volume; + } + + // Maximum is volumeMax + if (volume > max) { + volume = max; + } + // Minimum is volumeMin + if (volume < min) { + volume = min; + } + + // Set the player volume + player.media.volume = parseFloat(volume / max); + + // Set the display + if (player.elements.display.volume) { + player.elements.display.volume.value = volume; + } + + // Embeds + if (utils.inArray(types.embed, player.type)) { + switch (player.type) { + case 'youtube': + player.embed.setVolume(player.media.volume * 100); + break; + + case 'vimeo': + case 'soundcloud': + player.embed.setVolume(player.media.volume); + break; + } + + // Trigger volumechange for embeds + player.core.trigger(player.media, 'volumechange'); + } + + // Toggle muted state + if (volume === 0) { + player.media.muted = true; + } else if (player.media.muted && volume > 0) { + player.toggleMute(); + } + + // Allow chaining + return player; + }; + + // Increase volume + Player.prototype.increaseVolume = function(step) { + var player = this; + var volume = player.media.muted ? 0 : (player.media.volume * 10); + + if (!utils.is.number(step)) { + step = 1; + } + + player.setVolume(volume + step); + + // Allow chaining + return player; + }; + + // Decrease volume + Player.prototype.decreaseVolume = function(step) { + var player = this; + var volume = player.media.muted ? 0 : (player.media.volume * 10); + + if (!utils.is.number(step)) { + step = 1; + } + + player.setVolume(volume - step); + + // Allow chaining + return player; + }; + + // Toggle mute + Player.prototype.toggleMute = function(muted) { + var player = this; + + // If the method is called without parameter, toggle based on current value + if (!utils.is.boolean(muted)) { + muted = !player.media.muted; + } + + // Set button state + utils.toggleState(player.elements.buttons.mute, muted); + + // Set mute on the player + player.media.muted = muted; + + // If volume is 0 after unmuting, set to default + if (player.media.volume === 0) { + player.volume(player.config.volume); + } + + // Embeds + if (utils.inArray(types.embed, player.type)) { + // YouTube + switch (player.type) { + case 'youtube': + player.embed[player.media.muted ? 'mute' : 'unMute'](); + break; + + case 'vimeo': + case 'soundcloud': + player.embed.setVolume(player.media.muted ? 0 : parseFloat(player.config.volume / 10)); + break; + } + + // Trigger volumechange for embeds + player.core.trigger(player.media, 'volumechange'); + } + + // Allow chaining + return player; + }; + + // Set playback speed + Player.prototype.setSpeed = function(speed) { + var player = this; + + // Load speed from storage or default value + if (!utils.is.number(speed)) { + speed = parseFloat(player.storage.speed || player.config.speed.selected); + } + + // Set min/max + if (speed < 0.1) { + speed = 0.1; + } + if (speed > 2.0) { + speed = 2.0; + } + + if (!utils.is.array(player.config.speed.options)) { + player.core.warn('Invalid speeds format'); + return; + } + + // Store current speed + player.config.speed.selected = speed; + + // Set HTML5 speed + // TODO: set YouTube + player.media.playbackRate = speed; + + // Save speed to localStorage + player.core.updateStorage({ + speed: speed + }); + + // Allow chaining + return player; + }; + + // Toggle loop + // TODO: Finish logic + // TODO: Set the indicator on load as user may pass loop as config + Player.prototype.loop = function(type) { + var player = this; + + // Set default to be a true toggle + if (!utils.inArray(['start', 'end', 'all', 'none', 'toggle'], type)) { + type = 'toggle'; + } + + var currentTime = Number(player.media.currentTime); + + switch (type) { + case 'start': + if (player.config.loop.end && player.config.loop.end <= currentTime) { + player.config.loop.end = null; + } + player.config.loop.start = currentTime; + player.config.loop.indicator.start = player.elements.display.played.value; + break; + + case 'end': + if (player.config.loop.start >= currentTime) { + return; + } + player.config.loop.end = currentTime; + player.config.loop.indicator.end = player.elements.display.played.value; + break; + + case 'all': + player.config.loop.start = 0; + player.config.loop.end = player.media.duration - 2; + player.config.loop.indicator.start = 0; + player.config.loop.indicator.end = 100; + break; + + case 'toggle': + if (player.config.loop.active) { + player.config.loop.start = 0; + player.config.loop.end = null; + } else { + player.config.loop.start = 0; + player.config.loop.end = player.media.duration - 2; + } + break; + + default: + player.config.loop.start = 0; + player.config.loop.end = null; + break; + } + + // Check if can loop + player.config.loop.active = utils.is.number(player.config.loop.start) && utils.is.number(player.config.loop.end); + var start = player.core.updateTimeDisplay(player.config.loop.start, player.core.getElement('[data-plyr-loop="start"]')); + var end = null; + + if (utils.is.number(player.config.loop.end)) { + // Find the <span> inside button + end = player.core.updateTimeDisplay(player.config.loop.end, player.core.getElement('[data-loop__value="loopout"]')); + } else { + // Find the <span> inside button + //end = document.querySelector('[data-loop__value="loopout"]').innerHTML = ''; + } + + if (player.config.loop.active) { + // TODO: Improve the design of the loop indicator and put styling in CSS where it's meant to be + //getElement('[data-menu="loop"]').innerHTML = start + ' - ' + end; + //getElement(player.config.selectors.progress.looped).style.position = 'absolute'; + //getElement(player.config.selectors.progress.looped).style.left = player.config.loopinPositionPercentage + '%'; + //getElement(player.config.selectors.progress.looped).style.width = (player.config.loopoutPositionPercentage - player.config.loopinPositionPercentage) + '%'; + //getElement(player.config.selectors.progress.looped).style.background = '#ffbb00'; + //getElement(player.config.selectors.progress.looped).style.height = '3px'; + //getElement(player.config.selectors.progress.looped).style.top = '3px'; + //getElement(player.config.selectors.progress.looped).style['border-radius'] = '100px'; + } else { + //getElement('[data-menu="loop"]').innerHTML = player.config.i18n.loopNone; + //getElement(player.config.selectors.progress.looped).style.width = '0px'; + } + + // Allow chaining + return player; + }; + + // Add common function to retrieve media source + Player.prototype.source = function(source) { + var player = this; + + // If object or string, parse it + if (utils.is.object(source)) { + player.core.updateSource(source); + return player; + } + + // Return the current source + var url; + + switch (player.type) { + case 'youtube': + url = player.embed.getVideoUrl(); + break; + + case 'vimeo': + player.embed.getVideoUrl.then(function(value) { + url = value; + }); + break; + + case 'soundcloud': + player.embed.getCurrentSound(function(object) { + url = object.permalink_url; + }); + break; + + default: + url = player.media.currentSrc; + break; + } + + return url; + }; + + // Set or get poster + Player.prototype.poster = function(source) { + var player = this; + + if (!utils.is.string(source)) { + return player.media.getAttribute('poster'); + } else if (player.type === 'video') { + player.media.setAttribute('poster', source); + } else { + player.core.warn('Poster can only be set on HTML5 video'); + } + + // Allow chaining + return player; + }; + + // Toggle captions + Player.prototype.toggleCaptions = function(show) { + var player = this; + + // If there's no full support, or there's no caption toggle + if (!player.supported.full || !player.elements.buttons.captions) { + return; + } + + // If the method is called without parameter, toggle based on current value + if (!utils.is.boolean(show)) { + show = (player.elements.container.className.indexOf(player.config.classes.captions.active) === -1); + } + + // Set global + player.captions.enabled = show; + + // Toggle state + utils.toggleState(player.elements.buttons.captions, player.captions.enabled); + + // Add class hook + utils.toggleClass(player.elements.container, player.config.classes.captions.active, player.captions.enabled); + + // Trigger an event + player.core.trigger(player.elements.container, player.captions.enabled ? 'captionsenabled' : 'captionsdisabled', true); + + // Save captions state to localStorage + player.core.updateStorage({ + captions: player.captions.enabled + }); + + // Allow chaining + return player; + }; + + // Select active caption + Player.prototype.language = function(language) { + var player = this; + + if (utils.is.string(language)) { + // Update config + player.config.captions.language = language.toLowerCase(); + } else { + // If no language passed, return current language + return player.config.captions.language; + } + + // Clear caption + player.core.setCaption(); + + // Re-run setup + player.core.setupCaptions(); + + // Allow chaining + return player; + }; + + // Toggle fullscreen + // Requires user input event + Player.prototype.toggleFullscreen = function(event) { + var player = this; + + // Save scroll position + function saveScrollPosition() { + scroll = { + x: window.pageXOffset || 0, + y: window.pageYOffset || 0 + }; + } + + // Restore scroll position + function restoreScrollPosition() { + window.scrollTo(scroll.x, scroll.y); + } + + // Check for native support + var nativeSupport = support.fullscreen; + + if (nativeSupport) { + // If it's a fullscreen change event, update the UI + if (utils.is.event(event) && event.type === fullscreen.eventType) { + player.fullscreen.active = fullscreen.isFullScreen(player.elements.container); + } else { + // Else it's a user request to enter or exit + if (!fullscreen.isFullScreen(player.elements.container)) { + // Save scroll position + saveScrollPosition(); + + // Request full screen + fullscreen.requestFullScreen(player.elements.container); + } else { + // Bail from fullscreen + fullscreen.cancelFullScreen(); + } + + // Check if we're actually full screen (it could fail) + player.fullscreen.active = fullscreen.isFullScreen(player.elements.container); + + return; + } + } else { + // Otherwise, it's a simple toggle + player.fullscreen.active = !player.fullscreen.active; + + // Bind/unbind escape key + document.body.style.overflow = player.fullscreen.active ? 'hidden' : ''; + } + + // Set class hook + utils.toggleClass(player.elements.container, player.config.classes.fullscreen.active, player.fullscreen.active); + + // Set button state + if (player.elements.buttons && player.elements.buttons.fullscreen) { + utils.toggleState(player.elements.buttons.fullscreen, player.fullscreen.active); + } + + // Trigger an event + player.core.trigger(player.elements.container, player.fullscreen.active ? 'enterfullscreen' : 'exitfullscreen', true); + + // Restore scroll position + if (!player.fullscreen.active && nativeSupport) { + restoreScrollPosition(); + } + + // Allow chaining + return player; + }; + + // Toggle picture-in-picture + // TODO: update player with state, support, enabled + // TODO: detect outside changes + Player.prototype.togglePictureInPicture = function(toggle) { + var player = this; + var states = { + pip: 'picture-in-picture', + inline: 'inline' + }; + + // Bail if no support + if (!player.core.support.pip) { + return; + } + + // Toggle based on current state if not passed + if (!utils.is.boolean(toggle)) { + toggle = player.media.webkitPresentationMode === states.inline; + } + + // Toggle based on current state + player.media.webkitSetPresentationMode(toggle ? states.pip : states.inline); + + // Allow chaining + return player; + }; + + // Trigger airplay + // TODO: update player with state, support, enabled + Player.prototype.airPlay = function() { + var player = this; + + // Bail if no support + if (!player.core.support.airplay) { + return; + } + + // Show dialog + player.media.webkitShowPlaybackTargetPicker(); + + // Allow chaining + return player; + }; + + // Show the player controls in fullscreen mode + Player.prototype.toggleControls = function(toggle) { + var player = this; + + // Don't hide if config says not to, it's audio, or not ready or loading + if (!player.config.hideControls || player.type === 'audio') { + return; + } + + var delay = 0; + var show = toggle; + var isEnterFullscreen = false; + var loading = utils.hasClass(player.elements.container, player.config.classes.loading); + + // Default to false if no boolean + if (!utils.is.boolean(toggle)) { + if (utils.is.event(toggle)) { + // Is the enter fullscreen event + isEnterFullscreen = (toggle.type === 'enterfullscreen'); + + // Whether to show controls + show = utils.inArray(['mousemove', 'touchstart', 'mouseenter', 'focus'], toggle.type); + + // Delay hiding on move events + if (utils.inArray(['mousemove', 'touchmove'], toggle.type)) { + delay = 2000; + } + + // Delay a little more for keyboard users + if (toggle.type === 'focus') { + delay = 3000; + } + } else { + show = utils.hasClass(player.elements.container, player.config.classes.hideControls); + } + } + + // Clear timer every movement + window.clearTimeout(player.core.timers.hover); + + // If the mouse is not over the controls, set a timeout to hide them + if (show || player.media.paused || loading) { + utils.toggleClass(player.elements.container, player.config.classes.hideControls, false); + + // Always show controls when paused or if touch + if (player.media.paused || loading) { + return; + } + + // Delay for hiding on touch + if (support.touch) { + delay = 3000; + } + } + + // If toggle is false or if we're playing (regardless of toggle), + // then set the timer to hide the controls + if (!show || !player.media.paused) { + player.core.timers.hover = window.setTimeout(function() { + // If the mouse is over the controls (and not entering fullscreen), bail + if ((player.elements.controls.pressed || player.elements.controls.hover) && !isEnterFullscreen) { + return; + } + + utils.toggleClass(player.elements.container, player.config.classes.hideControls, true); + }, delay); + } + + // Allow chaining + return player; + }; + + // Event listener + Player.prototype.on = function(event, callback) { + var player = this; + + utils.on(player.elements.container, event, callback); + + // Allow chaining + return player; + }; + + // Check for support + Player.prototype.supports = function(mimeType) { + return support.mime(this, mimeType); + }; + + // Destroy an instance + // Event listeners are removed when elements are removed + // http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory + Player.prototype.destroy = function(callback, restore) { + var player = this; + // Type specific stuff + switch (player.type) { + case 'youtube': + // Clear timers + window.clearInterval(player.timers.buffering); + window.clearInterval(player.timers.playing); + + // Destroy YouTube API + player.embed.destroy(); + + // Clean up + cleanUp(); + + break; + + case 'vimeo': + // Destroy Vimeo API + // then clean up (wait, to prevent postmessage errors) + player.embed.unload().then(cleanUp); + + // Vimeo does not always return + window.setTimeout(cleanUp, 200); + + break; + + case 'video': + case 'audio': + // Restore native video controls + player.core.toggleNativeControls(true); + + // Clean up + cleanUp(); + + break; + } + + function cleanUp() { + // Default to restore original element + if (!utils.is.boolean(restore)) { + restore = true; + } + + // Callback + if (utils.is.function(callback)) { + callback.call(player.original); + } + + // Bail if we don't need to restore the original element + if (!restore) { + return; + } + + // Replace the container with the original element provided + player.elements.container.parentNode.replaceChild(player.original, player.elements.container); + + // Reset overflow (incase destroyed while fullscreen) + document.body.style.overflow = ''; + + // Event + player.core.trigger(player.original, 'destroyed', true); + } + + // Allow chaining + return player; + }; + + // Get the duration (or custom if set) + Player.prototype.getDuration = function() { + var player = this; + + // It should be a number, but parse it just incase + var duration = parseInt(player.config.duration); + + // True duration + var mediaDuration = 0; + + // Only if duration available + if (player.media.duration !== null && !isNaN(player.media.duration)) { + mediaDuration = player.media.duration; + } + + // If custom duration is funky, use regular duration + return (isNaN(duration) ? mediaDuration : duration); + }; return Player; -})); +}); |