diff options
Diffstat (limited to 'src/js')
-rw-r--r-- | src/js/controls.js | 89 | ||||
-rw-r--r-- | src/js/media.js | 19 | ||||
-rw-r--r-- | src/js/plugins/vimeo.js | 7 | ||||
-rw-r--r-- | src/js/plugins/youtube.js | 66 | ||||
-rw-r--r-- | src/js/plyr.js | 73 | ||||
-rw-r--r-- | src/js/source.js | 44 | ||||
-rw-r--r-- | src/js/support.js | 100 | ||||
-rw-r--r-- | src/js/ui.js | 63 | ||||
-rw-r--r-- | src/js/utils/elements.js | 8 | ||||
-rw-r--r-- | src/js/utils/events.js | 6 |
10 files changed, 191 insertions, 284 deletions
diff --git a/src/js/controls.js b/src/js/controls.js index f091555f..19c531af 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -119,28 +119,17 @@ const controls = { }, // Create hidden text label - createLabel(type, attr) { - let text = i18n.get(type, this.config); - const attributes = Object.assign({}, attr); - - switch (type) { - case 'pip': - text = 'PIP'; - break; - - case 'airplay': - text = 'AirPlay'; - break; - - default: - break; - } + createLabel(type, attr = {}) { + // Skip i18n for abbreviations and brand names + const universals = { + pip: 'PIP', + airplay: 'AirPlay', + }; - if ('class' in attributes) { - attributes.class += ` ${this.config.classNames.hidden}`; - } else { - attributes.class = this.config.classNames.hidden; - } + const text = universals[type] || i18n.get(type, this.config); + const attributes = Object.assign({}, attr, { + class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' '), + }); return createElement('span', attributes, text); }, @@ -342,19 +331,12 @@ const controls = { if (type !== 'volume') { progress.appendChild(createElement('span', null, '0')); - let suffix = ''; - switch (type) { - case 'played': - suffix = i18n.get('played', this.config); - break; + const suffixKey = ({ + played: 'played', + buffer: 'buffered', + })[type]; - case 'buffer': - suffix = i18n.get('buffered', this.config); - break; - - default: - break; - } + const suffix = suffixKey ? i18n.get(suffixKey, this.config) : ''; progress.innerText = `% ${suffix.toLowerCase()}`; } @@ -724,32 +706,27 @@ const controls = { let value = null; let list = container; - switch (setting) { - case 'captions': - value = this.currentTrack; - break; - - default: - value = !is.empty(input) ? input : this[setting]; - - // Get default - if (is.empty(value)) { - value = this.config[setting].default; - } + if (setting === 'captions') { + value = this.currentTrack; + } else { + value = !is.empty(input) ? input : this[setting]; - // Unsupported value - if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) { - this.debug.warn(`Unsupported value of '${value}' for ${setting}`); - return; - } + // Get default + if (is.empty(value)) { + value = this.config[setting].default; + } - // Disabled value - if (!this.config[setting].options.includes(value)) { - this.debug.warn(`Disabled value of '${value}' for ${setting}`); - return; - } + // Unsupported value + if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) { + this.debug.warn(`Unsupported value of '${value}' for ${setting}`); + return; + } - break; + // Disabled value + if (!this.config[setting].options.includes(value)) { + this.debug.warn(`Disabled value of '${value}' for ${setting}`); + return; + } } // Get the list if we need to diff --git a/src/js/media.js b/src/js/media.js index 189112a1..eb37d441 100644 --- a/src/js/media.js +++ b/src/js/media.js @@ -46,21 +46,12 @@ const media = { this.elements.wrapper.appendChild(this.elements.poster); } - if (this.isEmbed) { - switch (this.provider) { - case 'youtube': - youtube.setup.call(this); - break; - - case 'vimeo': - vimeo.setup.call(this); - break; - - default: - break; - } - } else if (this.isHTML5) { + if (this.isHTML5) { html5.extend.call(this); + } else if (this.isYouTube) { + youtube.setup.call(this); + } else if (this.isVimeo) { + vimeo.setup.call(this); } }, }; diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 312d53cf..09339229 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -119,8 +119,11 @@ const vimeo = { iframe.setAttribute('allowtransparency', ''); iframe.setAttribute('allow', 'autoplay'); + // Get poster, if already set + const { poster } = player; + // Inject the package - const wrapper = createElement('div', { class: player.config.classNames.embedContainer }); + const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer }); wrapper.appendChild(iframe); player.media = replaceElement(wrapper, player.media); @@ -137,7 +140,7 @@ const vimeo = { url.pathname = `${url.pathname.split('_')[0]}.jpg`; // Set and show poster - ui.setPoster.call(player, url.href); + ui.setPoster.call(player, url.href).catch(() => {}); }); // Setup instance diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index a1e52b48..65b6db75 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -158,10 +158,15 @@ const youtube = { // Replace the <iframe> with a <div> due to YouTube API issues const videoId = parseId(source); const id = generateId(player.provider); - const container = createElement('div', { id }); + + // Get poster, if already set + const { poster } = player; + + // Replace media element + const container = createElement('div', { id, poster }); player.media = replaceElement(container, player.media); - // Set poster image + // Id to poster wrapper const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`; // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide) @@ -174,7 +179,8 @@ const youtube = { if (!posterSrc.includes('maxres')) { player.elements.poster.style.backgroundSize = 'cover'; } - }); + }) + .catch(() => {}); // Setup instance // https://developers.google.com/youtube/iframe_api_reference @@ -200,46 +206,22 @@ const youtube = { }, events: { onError(event) { - // If we've already fired an error, don't do it again - // YouTube fires onError twice - if (is.object(player.media.error)) { - return; - } - - const detail = { - code: event.data, - }; - - // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError - switch (event.data) { - case 2: - detail.message = - 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.'; - break; - - case 5: - detail.message = - 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.'; - break; - - case 100: - detail.message = - 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.'; - break; - - case 101: - case 150: - detail.message = 'The owner of the requested video does not allow it to be played in embedded players.'; - break; - - default: - detail.message = 'An unknown error occured'; - break; + // YouTube may fire onError twice, so only handle it once + if (!player.media.error) { + const code = event.data; + // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError + const message = ({ + 2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.', + 5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.', + 100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.', + 101: 'The owner of the requested video does not allow it to be played in embedded players.', + 150: 'The owner of the requested video does not allow it to be played in embedded players.', + }[code]) || 'An unknown error occured'; + + player.media.error = { code, message }; + + triggerEvent.call(player, player.media, 'error'); } - - player.media.error = detail; - - triggerEvent.call(player, player.media, 'error'); }, onPlaybackQualityChange() { triggerEvent.call(player, player.media, 'qualitychange', false, { diff --git a/src/js/plyr.js b/src/js/plyr.js index 753db775..dcbe384b 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -790,7 +790,7 @@ class Plyr { return; } - ui.setPoster.call(this, input); + ui.setPoster.call(this, input, false).catch(() => {}); } /** @@ -1028,50 +1028,37 @@ class Plyr { // Stop playback this.stop(); - // Type specific stuff - switch (`${this.provider}:${this.type}`) { - case 'html5:video': - case 'html5:audio': - // Clear timeout - clearTimeout(this.timers.loading); - - // Restore native video controls - ui.toggleNativeControls.call(this, true); - - // Clean up - done(); - - break; - - case 'youtube:video': - // Clear timers - clearInterval(this.timers.buffering); - clearInterval(this.timers.playing); - - // Destroy YouTube API - if (this.embed !== null && is.function(this.embed.destroy)) { - this.embed.destroy(); - } - - // Clean up - done(); - - break; - - case 'vimeo:video': - // Destroy Vimeo API - // then clean up (wait, to prevent postmessage errors) - if (this.embed !== null) { - this.embed.unload().then(done); - } - - // Vimeo does not always return - setTimeout(done, 200); + // Provider specific stuff + if (this.isHTML5) { + // Clear timeout + clearTimeout(this.timers.loading); + + // Restore native video controls + ui.toggleNativeControls.call(this, true); + + // Clean up + done(); + } else if (this.isYouTube) { + // Clear timers + clearInterval(this.timers.buffering); + clearInterval(this.timers.playing); + + // Destroy YouTube API + if (this.embed !== null && is.function(this.embed.destroy)) { + this.embed.destroy(); + } - break; + // Clean up + done(); + } else if (this.isVimeo) { + // Destroy Vimeo API + // then clean up (wait, to prevent postmessage errors) + if (this.embed !== null) { + this.embed.unload().then(done); + } - default: - break; + // Vimeo does not always return + setTimeout(done, 200); } } diff --git a/src/js/source.js b/src/js/source.js index d4a66963..c62db15a 100644 --- a/src/js/source.js +++ b/src/js/source.js @@ -8,6 +8,7 @@ import media from './media'; import support from './support'; import ui from './ui'; import { createElement, insertElement, removeElement } from './utils/elements'; +import { getDeep } from './utils/objects'; import is from './utils/is'; const source = { @@ -27,7 +28,7 @@ const source = { // Update source // Sources are not checked for support so be careful change(input) { - if (!is.object(input) || !('sources' in input) || !input.sources.length) { + if (!getDeep(input, 'sources.length')) { this.debug.warn('Invalid source format'); return; } @@ -52,32 +53,19 @@ const source = { } // Set the type and provider - this.type = input.type; - this.provider = !is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5; - - // Check for support - this.supported = support.check(this.type, this.provider, this.config.playsinline); - - // Create new markup - switch (`${this.provider}:${this.type}`) { - case 'html5:video': - this.media = createElement('video'); - break; - - case 'html5:audio': - this.media = createElement('audio'); - break; - - case 'youtube:video': - case 'vimeo:video': - this.media = createElement('div', { - src: input.sources[0].src, - }); - break; - - default: - break; - } + const { sources, type } = input; + const [{ provider = providers.html5, src }] = sources; + const tagName = provider === 'html5' ? type : 'div'; + const attributes = provider === 'html5' ? {} : { src }; + + Object.assign(this, { + provider, + type, + // Check for support + supported: support.check(type, provider, this.config.playsinline), + // Create new element + media: createElement(tagName, attributes), + }); // Inject the new element this.elements.container.appendChild(this.media); @@ -114,7 +102,7 @@ const source = { // Set new sources for html5 if (this.isHTML5) { - source.insertElements.call(this, 'source', input.sources); + source.insertElements.call(this, 'source', sources); } // Set video title diff --git a/src/js/support.js b/src/js/support.js index 7eabae3c..6395293f 100644 --- a/src/js/support.js +++ b/src/js/support.js @@ -7,6 +7,15 @@ import browser from './utils/browser'; import { createElement } from './utils/elements'; import is from './utils/is'; +// Default codecs for checking mimetype support +const defaultCodecs = { + 'audio/ogg': 'vorbis', + 'audio/wav': '1', + 'video/webm': 'vp8, vorbis', + 'video/mp4': 'avc1.42E01E, mp4a.40.2', + 'video/ogg': 'theora', +}; + // Check for feature support const support = { // Basic support @@ -16,31 +25,9 @@ const support = { // Check for support // Basic functionality vs full UI check(type, provider, playsinline) { - let api = false; - let ui = false; const canPlayInline = browser.isIPhone && playsinline && support.playsinline; - - switch (`${provider}:${type}`) { - case 'html5:video': - api = support.video; - ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline); - break; - - case 'html5:audio': - api = support.audio; - ui = api && support.rangeInput; - break; - - case 'youtube:video': - case 'vimeo:video': - api = true; - ui = support.rangeInput && (!browser.isIPhone || canPlayInline); - break; - - default: - api = support.audio && support.video; - ui = api && support.rangeInput; - } + const api = support[type] || provider !== 'html5'; + const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline); return { api, @@ -63,56 +50,29 @@ const support = { // Check for mime type support against a player instance // Credits: http://diveintohtml5.info/everything.html // Related: http://www.leanbackplayer.com/test/h5mt.html - mime(type) { - const { media } = this; + mime(inputType) { + const [mediaType] = inputType.split('/'); + if (!this.isHTML5 || mediaType !== this.type) { + return false; + } + + let type; + if (inputType && inputType.includes('codecs=')) { + // Use input directly + type = inputType; + } else if (inputType === 'audio/mpeg') { + // Skip codec + type = 'audio/mpeg;'; + } else if (inputType in defaultCodecs) { + // Use codec + type = `${inputType}; codecs="${defaultCodecs[inputType]}"`; + } try { - // Bail if no checking function - if (!this.isHTML5 || !is.function(media.canPlayType)) { - return false; - } - - // Check directly if codecs specified - if (type.includes('codecs=')) { - return media.canPlayType(type).replace(/no/, ''); - } - - // Type specific checks - if (this.isVideo) { - switch (type) { - case 'video/webm': - return media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, ''); - - case 'video/mp4': - return media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''); - - case 'video/ogg': - return media.canPlayType('video/ogg; codecs="theora"').replace(/no/, ''); - - default: - return false; - } - } else if (this.isAudio) { - switch (type) { - case 'audio/mpeg': - return media.canPlayType('audio/mpeg;').replace(/no/, ''); - - case 'audio/ogg': - return media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, ''); - - case 'audio/wav': - return media.canPlayType('audio/wav; codecs="1"').replace(/no/, ''); - - default: - return false; - } - } - } catch (e) { + return Boolean(type && this.media.canPlayType(type).replace(/no/, '')); + } catch (err) { return false; } - - // If we got this far, we're stuffed - return false; }, // Check for textTracks support diff --git a/src/js/ui.js b/src/js/ui.js index 285739a7..5d7a6ae3 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -8,7 +8,7 @@ import i18n from './i18n'; import support from './support'; import browser from './utils/browser'; import { getElement, toggleClass, toggleState } from './utils/elements'; -import { triggerEvent } from './utils/events'; +import { ready, triggerEvent } from './utils/events'; import is from './utils/is'; import loadImage from './utils/loadImage'; @@ -109,8 +109,8 @@ const ui = { ui.setTitle.call(this); // Assure the poster image is set, if the property was added before the element was created - if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) { - ui.setPoster.call(this, this.poster); + if (this.poster) { + ui.setPoster.call(this, this.poster, false).catch(() => {}); } // Manually set the duration if user has overridden it. @@ -163,32 +163,43 @@ const ui = { }, // Set the poster image (async) - setPoster(poster) { - // Set property regardless of validity - this.media.setAttribute('poster', poster); - - // Bail if element is missing - if (!is.element(this.elements.poster)) { - return Promise.reject(); + // Used internally for the poster setter, with the passive option forced to false + setPoster(poster, passive = true) { + // Don't override if call is passive + if (passive && this.poster) { + return Promise.reject(new Error('Poster already set')); } - // Load the image, and set poster if successful - const loadPromise = loadImage(poster).then(() => { - this.elements.poster.style.backgroundImage = `url('${poster}')`; - Object.assign(this.elements.poster.style, { - backgroundImage: `url('${poster}')`, - // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) - backgroundSize: '', - }); - ui.togglePoster.call(this, true); - return poster; - }); - - // Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video) - loadPromise.catch(() => ui.togglePoster.call(this, false)); + // Set property synchronously to respect the call order + this.media.setAttribute('poster', poster); - // Return the promise so the caller can use it as well - return loadPromise; + // Wait until ui is ready + return ready.call(this) + // Load image + .then(() => loadImage(poster)) + .catch(err => { + // Hide poster on error unless it's been set by another call + if (poster === this.poster) { + ui.togglePoster.call(this, false); + } + // Rethrow + throw err; + }) + .then(() => { + // Prevent race conditions + if (poster !== this.poster) { + throw new Error('setPoster cancelled by later call to setPoster'); + } + }) + .then(() => { + Object.assign(this.elements.poster.style, { + backgroundImage: `url('${poster}')`, + // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) + backgroundSize: '', + }); + ui.togglePoster.call(this, true); + return poster; + }); }, // Check playing state diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 39b944d2..2d314ed8 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -42,9 +42,11 @@ export function setAttributes(element, attributes) { return; } - Object.entries(attributes).forEach(([key, value]) => { - element.setAttribute(key, value); - }); + // Assume null and undefined attributes should be left out, + // Setting them would otherwise convert them to "null" and "undefined" + Object.entries(attributes) + .filter(([, value]) => !is.nullOrUndefined(value)) + .forEach(([key, value]) => element.setAttribute(key, value)); } // Create a DocumentFragment diff --git a/src/js/utils/events.js b/src/js/utils/events.js index 1e940c71..9009d1cc 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -111,3 +111,9 @@ export function unbindListeners() { this.eventListeners = []; } } + +// Run method when / if player is ready +export function ready () { + return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)) + .then(() => {}); +} |