diff options
Diffstat (limited to 'src/js/plugins/youtube.js')
-rw-r--r-- | src/js/plugins/youtube.js | 246 |
1 files changed, 105 insertions, 141 deletions
diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 9b067c8a..64b6fff7 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -4,64 +4,54 @@ import controls from './../controls'; import ui from './../ui'; -import utils from './../utils'; +import { dedupe } from './../utils/arrays'; +import { createElement, replaceElement, toggleClass } from './../utils/elements'; +import { triggerEvent } from './../utils/events'; +import fetch from './../utils/fetch'; +import is from './../utils/is'; +import loadImage from './../utils/loadImage'; +import loadScript from './../utils/loadScript'; +import { format, generateId } from './../utils/strings'; + +// Parse YouTube ID from URL +function parseId(url) { + if (is.empty(url)) { + return null; + } + + const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; + return url.match(regex) ? RegExp.$2 : url; +} // Standardise YouTube quality unit function mapQualityUnit(input) { - switch (input) { - case 'hd2160': - return 2160; - - case 2160: - return 'hd2160'; - - case 'hd1440': - return 1440; - - case 1440: - return 'hd1440'; - - case 'hd1080': - return 1080; - - case 1080: - return 'hd1080'; - - case 'hd720': - return 720; - - case 720: - return 'hd720'; - - case 'large': - return 480; - - case 480: - return 'large'; - - case 'medium': - return 360; - - case 360: - return 'medium'; - - case 'small': - return 240; - - case 240: - return 'small'; - - default: - return 'default'; + const qualities = { + hd2160: 2160, + hd1440: 1440, + hd1080: 1080, + hd720: 720, + large: 480, + medium: 360, + small: 240, + tiny: 144, + }; + + const entry = Object.entries(qualities).find(entry => entry.includes(input)); + + if (entry) { + // Get the match corresponding to the input + return entry.find(value => value !== input); } + + return 'default'; } function mapQualityUnits(levels) { - if (utils.is.empty(levels)) { + if (is.empty(levels)) { return levels; } - return utils.dedupe(levels.map(level => mapQualityUnit(level))); + return dedupe(levels.map(level => mapQualityUnit(level))); } // Set playback state and trigger change (only on actual change) @@ -71,24 +61,24 @@ function assurePlaybackState(play) { } if (this.media.paused === play) { this.media.paused = !play; - utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); + triggerEvent.call(this, this.media, play ? 'play' : 'pause'); } } const youtube = { setup() { // Add embed class for responsive - utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); + toggleClass(this.elements.wrapper, this.config.classNames.embed, true); // Set aspect ratio youtube.setAspectRatio.call(this); // Setup API - if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) { + if (is.object(window.YT) && is.function(window.YT.Player)) { youtube.ready.call(this); } else { // Load the API - utils.loadScript(this.config.urls.youtube.sdk).catch(error => { + loadScript(this.config.urls.youtube.sdk).catch(error => { this.debug.warn('YouTube API failed to load', error); }); @@ -115,10 +105,10 @@ const youtube = { // Try via undocumented API method first // This method disappears now and then though... // https://github.com/sampotts/plyr/issues/709 - if (utils.is.function(this.embed.getVideoData)) { + if (is.function(this.embed.getVideoData)) { const { title } = this.embed.getVideoData(); - if (utils.is.empty(title)) { + if (is.empty(title)) { this.config.title = title; ui.setTitle.call(this); return; @@ -127,13 +117,12 @@ const youtube = { // Or via Google API const key = this.config.keys.google; - if (utils.is.string(key) && !utils.is.empty(key)) { - const url = utils.format(this.config.urls.youtube.api, videoId, key); + if (is.string(key) && !is.empty(key)) { + const url = format(this.config.urls.youtube.api, videoId, key); - utils - .fetch(url) + fetch(url) .then(result => { - if (utils.is.object(result)) { + if (is.object(result)) { this.config.title = result.items[0].snippet.title; ui.setTitle.call(this); } @@ -154,7 +143,7 @@ const youtube = { // Ignore already setup (race condition) const currentId = player.media.getAttribute('id'); - if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) { + if (!is.empty(currentId) && currentId.startsWith('youtube-')) { return; } @@ -162,30 +151,36 @@ const youtube = { let source = player.media.getAttribute('src'); // Get from <div> if needed - if (utils.is.empty(source)) { + if (is.empty(source)) { source = player.media.getAttribute(this.config.attributes.embed.id); } // Replace the <iframe> with a <div> due to YouTube API issues - const videoId = utils.parseYouTubeId(source); - const id = utils.generateId(player.provider); - const container = utils.createElement('div', { id }); - player.media = utils.replaceElement(container, player.media); + const videoId = parseId(source); + const id = generateId(player.provider); + + // Get poster, if already set + const { poster } = player; - // Set poster image + // Replace media element + const container = createElement('div', { id, poster }); + player.media = replaceElement(container, player.media); + + // 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) - utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded - .catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 - .catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists + loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded + .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 + .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists .then(image => ui.setPoster.call(player, image.src)) .then(posterSrc => { // If the image is padded, use background-size "cover" instead (like youtube does too with their posters) if (!posterSrc.includes('maxres')) { player.elements.poster.style.backgroundSize = 'cover'; } - }); + }) + .catch(() => {}); // Setup instance // https://developers.google.com/youtube/iframe_api_reference @@ -211,49 +206,26 @@ const youtube = { }, events: { onError(event) { - // If we've already fired an error, don't do it again - // YouTube fires onError twice - if (utils.is.object(player.media.error)) { - return; + // 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'); } - - 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; - } - - player.media.error = detail; - - utils.dispatchEvent.call(player, player.media, 'error'); }, onPlaybackQualityChange() { - utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { + triggerEvent.call(player, player.media, 'qualitychange', false, { quality: player.media.quality, }); }, @@ -264,7 +236,7 @@ const youtube = { // Get current speed player.media.playbackRate = instance.getPlaybackRate(); - utils.dispatchEvent.call(player, player.media, 'ratechange'); + triggerEvent.call(player, player.media, 'ratechange'); }, onReady(event) { // Get the instance @@ -305,7 +277,7 @@ const youtube = { // Set seeking state and trigger event player.media.seeking = true; - utils.dispatchEvent.call(player, player.media, 'seeking'); + triggerEvent.call(player, player.media, 'seeking'); // Seek after events sent instance.seekTo(time); @@ -328,15 +300,7 @@ const youtube = { return mapQualityUnit(instance.getPlaybackQuality()); }, set(input) { - const quality = input; - - // Set via API - instance.setPlaybackQuality(mapQualityUnit(quality)); - - // Trigger request event - utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, { - quality, - }); + instance.setPlaybackQuality(mapQualityUnit(input)); }, }); @@ -349,7 +313,7 @@ const youtube = { set(input) { volume = input; instance.setVolume(volume * 100); - utils.dispatchEvent.call(player, player.media, 'volumechange'); + triggerEvent.call(player, player.media, 'volumechange'); }, }); @@ -360,10 +324,10 @@ const youtube = { return muted; }, set(input) { - const toggle = utils.is.boolean(input) ? input : muted; + const toggle = is.boolean(input) ? input : muted; muted = toggle; instance[toggle ? 'mute' : 'unMute'](); - utils.dispatchEvent.call(player, player.media, 'volumechange'); + triggerEvent.call(player, player.media, 'volumechange'); }, }); @@ -389,8 +353,8 @@ const youtube = { player.media.setAttribute('tabindex', -1); } - utils.dispatchEvent.call(player, player.media, 'timeupdate'); - utils.dispatchEvent.call(player, player.media, 'durationchange'); + triggerEvent.call(player, player.media, 'timeupdate'); + triggerEvent.call(player, player.media, 'durationchange'); // Reset timer clearInterval(player.timers.buffering); @@ -402,7 +366,7 @@ const youtube = { // Trigger progress only when we actually buffer something if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) { - utils.dispatchEvent.call(player, player.media, 'progress'); + triggerEvent.call(player, player.media, 'progress'); } // Set last buffer point @@ -413,7 +377,7 @@ const youtube = { clearInterval(player.timers.buffering); // Trigger event - utils.dispatchEvent.call(player, player.media, 'canplaythrough'); + triggerEvent.call(player, player.media, 'canplaythrough'); } }, 200); @@ -427,15 +391,12 @@ const youtube = { // Reset timer clearInterval(player.timers.playing); - const seeked = player.media.seeking && [ - 1, - 2, - ].includes(event.data); + const seeked = player.media.seeking && [1, 2].includes(event.data); if (seeked) { // Unset seeking and fire seeked event player.media.seeking = false; - utils.dispatchEvent.call(player, player.media, 'seeked'); + triggerEvent.call(player, player.media, 'seeked'); } // Handle events @@ -448,11 +409,11 @@ const youtube = { switch (event.data) { case -1: // Update scrubber - utils.dispatchEvent.call(player, player.media, 'timeupdate'); + triggerEvent.call(player, player.media, 'timeupdate'); // Get loaded % from YouTube player.media.buffered = instance.getVideoLoadedFraction(); - utils.dispatchEvent.call(player, player.media, 'progress'); + triggerEvent.call(player, player.media, 'progress'); break; @@ -465,7 +426,7 @@ const youtube = { instance.stopVideo(); instance.playVideo(); } else { - utils.dispatchEvent.call(player, player.media, 'ended'); + triggerEvent.call(player, player.media, 'ended'); } break; @@ -477,11 +438,11 @@ const youtube = { } else { assurePlaybackState.call(player, true); - utils.dispatchEvent.call(player, player.media, 'playing'); + triggerEvent.call(player, player.media, 'playing'); // Poll to get playback progress player.timers.playing = setInterval(() => { - utils.dispatchEvent.call(player, player.media, 'timeupdate'); + triggerEvent.call(player, player.media, 'timeupdate'); }, 50); // Check duration again due to YouTube bug @@ -489,11 +450,14 @@ const youtube = { // https://code.google.com/p/gdata-issues/issues/detail?id=8690 if (player.media.duration !== instance.getDuration()) { player.media.duration = instance.getDuration(); - utils.dispatchEvent.call(player, player.media, 'durationchange'); + triggerEvent.call(player, player.media, 'durationchange'); } // Get quality - controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels())); + controls.setQualityMenu.call( + player, + mapQualityUnits(instance.getAvailableQualityLevels()), + ); } break; @@ -511,7 +475,7 @@ const youtube = { break; } - utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, { + triggerEvent.call(player, player.elements.container, 'statechange', false, { code: event.data, }); }, |