diff options
author | Danielh112 <Daniel@sbgsportssoftware.com> | 2020-08-18 11:29:25 +0100 |
---|---|---|
committer | Danielh112 <Daniel@sbgsportssoftware.com> | 2020-08-18 11:29:25 +0100 |
commit | f7e9ee56d2ed5447f59e5548f005fabdab2f0a72 (patch) | |
tree | a16300fa62e68b3310ae96e36dba65981f0024ef /src/js/plugins/youtube.js | |
parent | 22af7f16ea4a4269321d29242d63ec23718c92da (diff) | |
parent | 423b7b276f1572eb666de32094a9aacd32e87d18 (diff) | |
download | plyr-f7e9ee56d2ed5447f59e5548f005fabdab2f0a72.tar.lz plyr-f7e9ee56d2ed5447f59e5548f005fabdab2f0a72.tar.xz plyr-f7e9ee56d2ed5447f59e5548f005fabdab2f0a72.zip |
Fix merge conflicts
Diffstat (limited to 'src/js/plugins/youtube.js')
-rw-r--r-- | src/js/plugins/youtube.js | 790 |
1 files changed, 395 insertions, 395 deletions
diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 8c65b1dc..89a75d89 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -15,426 +15,426 @@ import { setAspectRatio } from '../utils/style'; // Parse YouTube ID from URL function parseId(url) { - if (is.empty(url)) { - return null; - } + if (is.empty(url)) { + return null; + } - const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; - return url.match(regex) ? RegExp.$2 : url; + const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; + return url.match(regex) ? RegExp.$2 : url; } // Set playback state and trigger change (only on actual change) function assurePlaybackState(play) { - if (play && !this.embed.hasPlayed) { - this.embed.hasPlayed = true; - } - if (this.media.paused === play) { - this.media.paused = !play; - triggerEvent.call(this, this.media, play ? 'play' : 'pause'); - } + if (play && !this.embed.hasPlayed) { + this.embed.hasPlayed = true; + } + if (this.media.paused === play) { + this.media.paused = !play; + triggerEvent.call(this, this.media, play ? 'play' : 'pause'); + } } function getHost(config) { - if (config.noCookie) { - return 'https://www.youtube-nocookie.com'; - } + if (config.noCookie) { + return 'https://www.youtube-nocookie.com'; + } - if (window.location.protocol === 'http:') { - return 'http://www.youtube.com'; - } + if (window.location.protocol === 'http:') { + return 'http://www.youtube.com'; + } - // Use YouTube's default - return undefined; + // Use YouTube's default + return undefined; } const youtube = { - setup() { - // Add embed class for responsive - toggleClass(this.elements.wrapper, this.config.classNames.embed, true); - - // Setup API - if (is.object(window.YT) && is.function(window.YT.Player)) { - youtube.ready.call(this); - } else { - // Reference current global callback - const callback = window.onYouTubeIframeAPIReady; - - // Set callback to process queue - window.onYouTubeIframeAPIReady = () => { - // Call global callback if set - if (is.function(callback)) { - callback(); - } - - youtube.ready.call(this); - }; - - // Load the SDK - loadScript(this.config.urls.youtube.sdk).catch(error => { - this.debug.warn('YouTube API failed to load', error); - }); + setup() { + // Add embed class for responsive + toggleClass(this.elements.wrapper, this.config.classNames.embed, true); + + // Setup API + if (is.object(window.YT) && is.function(window.YT.Player)) { + youtube.ready.call(this); + } else { + // Reference current global callback + const callback = window.onYouTubeIframeAPIReady; + + // Set callback to process queue + window.onYouTubeIframeAPIReady = () => { + // Call global callback if set + if (is.function(callback)) { + callback(); } - }, - // Get the media title - getTitle(videoId) { - const url = format(this.config.urls.youtube.api, videoId); + youtube.ready.call(this); + }; - fetch(url) - .then(data => { - if (is.object(data)) { - const { title, height, width } = data; + // Load the SDK + loadScript(this.config.urls.youtube.sdk).catch(error => { + this.debug.warn('YouTube API failed to load', error); + }); + } + }, - // Set title - this.config.title = title; - ui.setTitle.call(this); + // Get the media title + getTitle(videoId) { + const url = format(this.config.urls.youtube.api, videoId); - // Set aspect ratio - this.embed.ratio = [width, height]; - } + fetch(url) + .then(data => { + if (is.object(data)) { + const { title, height, width } = data; - setAspectRatio.call(this); - }) - .catch(() => { - // Set aspect ratio - setAspectRatio.call(this); - }); - }, - - // API ready - ready() { - const player = this; - // Ignore already setup (race condition) - const currentId = player.media && player.media.getAttribute('id'); - if (!is.empty(currentId) && currentId.startsWith('youtube-')) { - return; + // Set title + this.config.title = title; + ui.setTitle.call(this); + + // Set aspect ratio + this.embed.ratio = [width, height]; } - // Get the source URL or ID - let source = player.media.getAttribute('src'); + setAspectRatio.call(this); + }) + .catch(() => { + // Set aspect ratio + setAspectRatio.call(this); + }); + }, + + // API ready + ready() { + const player = this; + // Ignore already setup (race condition) + const currentId = player.media && player.media.getAttribute('id'); + if (!is.empty(currentId) && currentId.startsWith('youtube-')) { + return; + } + + // Get the source URL or ID + let source = player.media.getAttribute('src'); + + // Get from <div> if needed + if (is.empty(source)) { + source = player.media.getAttribute(this.config.attributes.embed.id); + } - // Get from <div> if needed - 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 = parseId(source); + const id = generateId(player.provider); + // Get poster, if already set + const { poster } = player; + // Replace media element + const container = createElement('div', { id, 'data-poster': poster }); + player.media = replaceElement(container, player.media); + + // Id to poster wrapper + const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`; + + // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide) + 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(src => { + // If the image is padded, use background-size "cover" instead (like youtube does too with their posters) + if (!src.includes('maxres')) { + player.elements.poster.style.backgroundSize = 'cover'; } + }) + .catch(() => {}); + + const config = player.config.youtube; + + // Setup instance + // https://developers.google.com/youtube/iframe_api_reference + player.embed = new window.YT.Player(id, { + videoId, + host: getHost(config), + playerVars: extend( + {}, + { + autoplay: player.config.autoplay ? 1 : 0, // Autoplay + hl: player.config.hl, // iframe interface language + controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported + disablekb: 1, // Disable keyboard as we handle it + playsinline: !player.config.fullscreen.iosNative ? 1 : 0, // Allow iOS inline playback + // Captions are flaky on YouTube + cc_load_policy: player.captions.active ? 1 : 0, + cc_lang_pref: player.config.captions.language, + // Tracking for stats + widget_referrer: window ? window.location.href : null, + }, + config, + ), + events: { + onError(event) { + // 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'); + } + }, + onPlaybackRateChange(event) { + // Get the instance + const instance = event.target; + + // Get current speed + player.media.playbackRate = instance.getPlaybackRate(); + + triggerEvent.call(player, player.media, 'ratechange'); + }, + onReady(event) { + // Bail if onReady has already been called. See issue #1108 + if (is.function(player.media.play)) { + return; + } + // Get the instance + const instance = event.target; + + // Get the title + youtube.getTitle.call(player, videoId); + + // Create a faux HTML5 API using the YouTube API + player.media.play = () => { + assurePlaybackState.call(player, true); + instance.playVideo(); + }; + + player.media.pause = () => { + assurePlaybackState.call(player, false); + instance.pauseVideo(); + }; + + player.media.stop = () => { + instance.stopVideo(); + }; + + player.media.duration = instance.getDuration(); + player.media.paused = true; + + // Seeking + player.media.currentTime = 0; + Object.defineProperty(player.media, 'currentTime', { + get() { + return Number(instance.getCurrentTime()); + }, + set(time) { + // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet). + if (player.paused && !player.embed.hasPlayed) { + player.embed.mute(); + } + + // Set seeking state and trigger event + player.media.seeking = true; + triggerEvent.call(player, player.media, 'seeking'); + + // Seek after events sent + instance.seekTo(time); + }, + }); - // Replace the <iframe> with a <div> due to YouTube API issues - const videoId = parseId(source); - const id = generateId(player.provider); - // Get poster, if already set - const { poster } = player; - // Replace media element - const container = createElement('div', { id, poster }); - player.media = replaceElement(container, player.media); - - // Id to poster wrapper - const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`; - - // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide) - 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(src => { - // If the image is padded, use background-size "cover" instead (like youtube does too with their posters) - if (!src.includes('maxres')) { - player.elements.poster.style.backgroundSize = 'cover'; - } - }) - .catch(() => {}); - - const config = player.config.youtube; - - // Setup instance - // https://developers.google.com/youtube/iframe_api_reference - player.embed = new window.YT.Player(id, { - videoId, - host: getHost(config), - playerVars: extend( - {}, - { - autoplay: player.config.autoplay ? 1 : 0, // Autoplay - hl: player.config.hl, // iframe interface language - controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported - disablekb: 1, // Disable keyboard as we handle it - playsinline: !player.config.fullscreen.iosNative ? 1 : 0, // Allow iOS inline playback - // Captions are flaky on YouTube - cc_load_policy: player.captions.active ? 1 : 0, - cc_lang_pref: player.config.captions.language, - // Tracking for stats - widget_referrer: window ? window.location.href : null, - }, - config, - ), - events: { - onError(event) { - // 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'); - } - }, - onPlaybackRateChange(event) { - // Get the instance - const instance = event.target; - - // Get current speed - player.media.playbackRate = instance.getPlaybackRate(); - - triggerEvent.call(player, player.media, 'ratechange'); - }, - onReady(event) { - // Bail if onReady has already been called. See issue #1108 - if (is.function(player.media.play)) { - return; - } - // Get the instance - const instance = event.target; - - // Get the title - youtube.getTitle.call(player, videoId); - - // Create a faux HTML5 API using the YouTube API - player.media.play = () => { - assurePlaybackState.call(player, true); - instance.playVideo(); - }; - - player.media.pause = () => { - assurePlaybackState.call(player, false); - instance.pauseVideo(); - }; - - player.media.stop = () => { - instance.stopVideo(); - }; - - player.media.duration = instance.getDuration(); - player.media.paused = true; - - // Seeking - player.media.currentTime = 0; - Object.defineProperty(player.media, 'currentTime', { - get() { - return Number(instance.getCurrentTime()); - }, - set(time) { - // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet). - if (player.paused && !player.embed.hasPlayed) { - player.embed.mute(); - } - - // Set seeking state and trigger event - player.media.seeking = true; - triggerEvent.call(player, player.media, 'seeking'); - - // Seek after events sent - instance.seekTo(time); - }, - }); - - // Playback speed - Object.defineProperty(player.media, 'playbackRate', { - get() { - return instance.getPlaybackRate(); - }, - set(input) { - instance.setPlaybackRate(input); - }, - }); - - // Volume - let { volume } = player.config; - Object.defineProperty(player.media, 'volume', { - get() { - return volume; - }, - set(input) { - volume = input; - instance.setVolume(volume * 100); - triggerEvent.call(player, player.media, 'volumechange'); - }, - }); - - // Muted - let { muted } = player.config; - Object.defineProperty(player.media, 'muted', { - get() { - return muted; - }, - set(input) { - const toggle = is.boolean(input) ? input : muted; - muted = toggle; - instance[toggle ? 'mute' : 'unMute'](); - triggerEvent.call(player, player.media, 'volumechange'); - }, - }); - - // Source - Object.defineProperty(player.media, 'currentSrc', { - get() { - return instance.getVideoUrl(); - }, - }); - - // Ended - Object.defineProperty(player.media, 'ended', { - get() { - return player.currentTime === player.duration; - }, - }); - - // Get available speeds - const speeds = instance.getAvailablePlaybackRates(); - // Filter based on config - player.options.speed = speeds.filter(s => player.config.speed.options.includes(s)); - - // Set the tabindex to avoid focus entering iframe - if (player.supported.ui) { - player.media.setAttribute('tabindex', -1); - } - - triggerEvent.call(player, player.media, 'timeupdate'); - triggerEvent.call(player, player.media, 'durationchange'); - - // Reset timer - clearInterval(player.timers.buffering); - - // Setup buffering - player.timers.buffering = setInterval(() => { - // Get loaded % from YouTube - player.media.buffered = instance.getVideoLoadedFraction(); - - // Trigger progress only when we actually buffer something - if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) { - triggerEvent.call(player, player.media, 'progress'); - } - - // Set last buffer point - player.media.lastBuffered = player.media.buffered; - - // Bail if we're at 100% - if (player.media.buffered === 1) { - clearInterval(player.timers.buffering); - - // Trigger event - triggerEvent.call(player, player.media, 'canplaythrough'); - } - }, 200); - - // Rebuild UI - setTimeout(() => ui.build.call(player), 50); - }, - onStateChange(event) { - // Get the instance - const instance = event.target; - - // Reset timer - clearInterval(player.timers.playing); - - const seeked = player.media.seeking && [1, 2].includes(event.data); - - if (seeked) { - // Unset seeking and fire seeked event - player.media.seeking = false; - triggerEvent.call(player, player.media, 'seeked'); - } - - // Handle events - // -1 Unstarted - // 0 Ended - // 1 Playing - // 2 Paused - // 3 Buffering - // 5 Video cued - switch (event.data) { - case -1: - // Update scrubber - triggerEvent.call(player, player.media, 'timeupdate'); - - // Get loaded % from YouTube - player.media.buffered = instance.getVideoLoadedFraction(); - triggerEvent.call(player, player.media, 'progress'); - - break; - - case 0: - assurePlaybackState.call(player, false); - - // YouTube doesn't support loop for a single video, so mimick it. - if (player.media.loop) { - // YouTube needs a call to `stopVideo` before playing again - instance.stopVideo(); - instance.playVideo(); - } else { - triggerEvent.call(player, player.media, 'ended'); - } - - break; - - case 1: - // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) - if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) { - player.media.pause(); - } else { - assurePlaybackState.call(player, true); - - triggerEvent.call(player, player.media, 'playing'); - - // Poll to get playback progress - player.timers.playing = setInterval(() => { - triggerEvent.call(player, player.media, 'timeupdate'); - }, 50); - - // 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.media.duration !== instance.getDuration()) { - player.media.duration = instance.getDuration(); - triggerEvent.call(player, player.media, 'durationchange'); - } - } - - break; - - case 2: - // Restore audio (YouTube starts playing on seek if the video hasn't been played yet) - if (!player.muted) { - player.embed.unMute(); - } - assurePlaybackState.call(player, false); - - break; - - case 3: - // Trigger waiting event to add loading classes to container as the video buffers. - triggerEvent.call(player, player.media, 'waiting'); - - break; - - default: - break; - } - - triggerEvent.call(player, player.elements.container, 'statechange', false, { - code: event.data, - }); - }, + // Playback speed + Object.defineProperty(player.media, 'playbackRate', { + get() { + return instance.getPlaybackRate(); }, - }); - }, + set(input) { + instance.setPlaybackRate(input); + }, + }); + + // Volume + let { volume } = player.config; + Object.defineProperty(player.media, 'volume', { + get() { + return volume; + }, + set(input) { + volume = input; + instance.setVolume(volume * 100); + triggerEvent.call(player, player.media, 'volumechange'); + }, + }); + + // Muted + let { muted } = player.config; + Object.defineProperty(player.media, 'muted', { + get() { + return muted; + }, + set(input) { + const toggle = is.boolean(input) ? input : muted; + muted = toggle; + instance[toggle ? 'mute' : 'unMute'](); + triggerEvent.call(player, player.media, 'volumechange'); + }, + }); + + // Source + Object.defineProperty(player.media, 'currentSrc', { + get() { + return instance.getVideoUrl(); + }, + }); + + // Ended + Object.defineProperty(player.media, 'ended', { + get() { + return player.currentTime === player.duration; + }, + }); + + // Get available speeds + const speeds = instance.getAvailablePlaybackRates(); + // Filter based on config + player.options.speed = speeds.filter(s => player.config.speed.options.includes(s)); + + // Set the tabindex to avoid focus entering iframe + if (player.supported.ui) { + player.media.setAttribute('tabindex', -1); + } + + triggerEvent.call(player, player.media, 'timeupdate'); + triggerEvent.call(player, player.media, 'durationchange'); + + // Reset timer + clearInterval(player.timers.buffering); + + // Setup buffering + player.timers.buffering = setInterval(() => { + // Get loaded % from YouTube + player.media.buffered = instance.getVideoLoadedFraction(); + + // Trigger progress only when we actually buffer something + if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) { + triggerEvent.call(player, player.media, 'progress'); + } + + // Set last buffer point + player.media.lastBuffered = player.media.buffered; + + // Bail if we're at 100% + if (player.media.buffered === 1) { + clearInterval(player.timers.buffering); + + // Trigger event + triggerEvent.call(player, player.media, 'canplaythrough'); + } + }, 200); + + // Rebuild UI + setTimeout(() => ui.build.call(player), 50); + }, + onStateChange(event) { + // Get the instance + const instance = event.target; + + // Reset timer + clearInterval(player.timers.playing); + + const seeked = player.media.seeking && [1, 2].includes(event.data); + + if (seeked) { + // Unset seeking and fire seeked event + player.media.seeking = false; + triggerEvent.call(player, player.media, 'seeked'); + } + + // Handle events + // -1 Unstarted + // 0 Ended + // 1 Playing + // 2 Paused + // 3 Buffering + // 5 Video cued + switch (event.data) { + case -1: + // Update scrubber + triggerEvent.call(player, player.media, 'timeupdate'); + + // Get loaded % from YouTube + player.media.buffered = instance.getVideoLoadedFraction(); + triggerEvent.call(player, player.media, 'progress'); + + break; + + case 0: + assurePlaybackState.call(player, false); + + // YouTube doesn't support loop for a single video, so mimick it. + if (player.media.loop) { + // YouTube needs a call to `stopVideo` before playing again + instance.stopVideo(); + instance.playVideo(); + } else { + triggerEvent.call(player, player.media, 'ended'); + } + + break; + + case 1: + // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) + if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) { + player.media.pause(); + } else { + assurePlaybackState.call(player, true); + + triggerEvent.call(player, player.media, 'playing'); + + // Poll to get playback progress + player.timers.playing = setInterval(() => { + triggerEvent.call(player, player.media, 'timeupdate'); + }, 50); + + // 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.media.duration !== instance.getDuration()) { + player.media.duration = instance.getDuration(); + triggerEvent.call(player, player.media, 'durationchange'); + } + } + + break; + + case 2: + // Restore audio (YouTube starts playing on seek if the video hasn't been played yet) + if (!player.muted) { + player.embed.unMute(); + } + assurePlaybackState.call(player, false); + + break; + + case 3: + // Trigger waiting event to add loading classes to container as the video buffers. + triggerEvent.call(player, player.media, 'waiting'); + + break; + + default: + break; + } + + triggerEvent.call(player, player.elements.container, 'statechange', false, { + code: event.data, + }); + }, + }, + }); + }, }; export default youtube; |