diff options
Diffstat (limited to 'src/js/plugins')
-rw-r--r-- | src/js/plugins/ads.js | 109 | ||||
-rw-r--r-- | src/js/plugins/preview-thumbnails.js (renamed from src/js/plugins/previewThumbnails.js) | 38 | ||||
-rw-r--r-- | src/js/plugins/vimeo.js | 14 | ||||
-rw-r--r-- | src/js/plugins/youtube.js | 106 |
4 files changed, 146 insertions, 121 deletions
diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 6efd3295..db55e499 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -10,14 +10,28 @@ import { createElement } from '../utils/elements'; import { triggerEvent } from '../utils/events'; import i18n from '../utils/i18n'; import is from '../utils/is'; -import loadScript from '../utils/loadScript'; +import loadScript from '../utils/load-script'; import { formatTime } from '../utils/time'; import { buildUrlParams } from '../utils/urls'; +const destroy = instance => { + // Destroy our adsManager + if (instance.manager) { + instance.manager.destroy(); + } + + // Destroy our adsManager + if (instance.elements.displayContainer) { + instance.elements.displayContainer.destroy(); + } + + instance.elements.container.remove(); +}; + class Ads { /** * Ads constructor. - * @param {object} player + * @param {Object} player * @return {Ads} */ constructor(player) { @@ -63,20 +77,22 @@ class Ads { * Load the IMA SDK */ load() { - if (this.enabled) { - // Check if the Google IMA3 SDK is loaded or load it ourselves - if (!is.object(window.google) || !is.object(window.google.ima)) { - loadScript(this.player.config.urls.googleIMA.sdk) - .then(() => { - this.ready(); - }) - .catch(() => { - // Script failed to load or is blocked - this.trigger('error', new Error('Google IMA SDK failed to load')); - }); - } else { - this.ready(); - } + if (!this.enabled) { + return; + } + + // Check if the Google IMA3 SDK is loaded or load it ourselves + if (!is.object(window.google) || !is.object(window.google.ima)) { + loadScript(this.player.config.urls.googleIMA.sdk) + .then(() => { + this.ready(); + }) + .catch(() => { + // Script failed to load or is blocked + this.trigger('error', new Error('Google IMA SDK failed to load')); + }); + } else { + this.ready(); } } @@ -84,6 +100,11 @@ class Ads { * Get the ads instance ready */ ready() { + // Double check we're enabled + if (!this.enabled) { + destroy(this); + } + // Start ticking our safety timer. If the whole advertisement // thing doesn't resolve within our set time; we bail this.startSafetyTimer(12000, 'ready()'); @@ -198,7 +219,7 @@ class Ads { /** * Update the ad countdown - * @param {boolean} start + * @param {Boolean} start */ pollCountdown(start = false) { if (!start) { @@ -240,16 +261,13 @@ class Ads { // Get the cue points for any mid-rolls by filtering out the pre- and post-roll this.cuePoints = this.manager.getCuePoints(); - // Set volume to match player - this.manager.setVolume(this.player.volume); - // Add listeners to the required events // Advertisement error events this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error)); // Advertisement regular events Object.keys(google.ima.AdEvent.Type).forEach(type => { - this.manager.addEventListener(google.ima.AdEvent.Type[type], event => this.onAdEvent(event)); + this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e)); }); // Resolve our adsManager @@ -285,7 +303,6 @@ class Ads { */ onAdEvent(event) { const { container } = this.player.elements; - // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED) // don't have ad object associated const ad = event.getAd(); @@ -293,19 +310,18 @@ class Ads { // Proxy event const dispatchEvent = type => { - const event = `ads${type.replace(/_/g, '').toLowerCase()}`; - triggerEvent.call(this.player, this.player.media, event); + triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`); }; + // Bubble the event + dispatchEvent(event.type); + switch (event.type) { case google.ima.AdEvent.Type.LOADED: // This is the first event sent for an ad - it is possible to determine whether the // ad is a video ad or an overlay this.trigger('loaded'); - // Bubble event - dispatchEvent(event.type); - // Start countdown this.pollCountdown(true); @@ -317,15 +333,19 @@ class Ads { // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex()); // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset()); + + break; + + case google.ima.AdEvent.Type.STARTED: + // Set volume to match player + this.manager.setVolume(this.player.volume); + break; case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: // All ads for the current videos are done. We can now request new advertisements // in case the video is re-played - // Fire event - dispatchEvent(event.type); - // TODO: Example for what happens when a next video in a playlist would be loaded. // So here we load a new video when all ads are done. // Then we load new ads within a new adsManager. When the video @@ -350,6 +370,7 @@ class Ads { // playing when the IMA SDK is ready or has failed this.loadAds(); + break; case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED: @@ -357,8 +378,6 @@ class Ads { // for example display a pause button and remaining time. Fired when content should // be paused. This usually happens right before an ad is about to cover the content - dispatchEvent(event.type); - this.pauseContent(); break; @@ -369,26 +388,17 @@ class Ads { // Fired when content should be resumed. This usually happens when an ad finishes // or collapses - dispatchEvent(event.type); - this.pollCountdown(); this.resumeContent(); break; - case google.ima.AdEvent.Type.STARTED: - case google.ima.AdEvent.Type.MIDPOINT: - case google.ima.AdEvent.Type.COMPLETE: - case google.ima.AdEvent.Type.IMPRESSION: - case google.ima.AdEvent.Type.CLICK: - dispatchEvent(event.type); - break; - case google.ima.AdEvent.Type.LOG: if (adData.adError) { this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`); } + break; default: @@ -463,6 +473,9 @@ class Ads { // Play the requested advertisement whenever the adsManager is ready this.managerPromise .then(() => { + // Set volume to match player + this.manager.setVolume(this.player.volume); + // Initialize the container. Must be done via a user action on mobile devices this.elements.displayContainer.initialize(); @@ -559,7 +572,7 @@ class Ads { /** * Handles callbacks after an ad event was invoked - * @param {string} event - Event type + * @param {String} event - Event type */ trigger(event, ...args) { const handlers = this.events[event]; @@ -575,8 +588,8 @@ class Ads { /** * Add event listeners - * @param {string} event - Event type - * @param {function} callback - Callback for when event occurs + * @param {String} event - Event type + * @param {Function} callback - Callback for when event occurs * @return {Ads} */ on(event, callback) { @@ -594,8 +607,8 @@ class Ads { * The advertisement has 12 seconds to get its things together. We stop this timer when the * advertisement is playing, or when a user action is required to start, then we clear the * timer on ad ready - * @param {number} time - * @param {string} from + * @param {Number} time + * @param {String} from */ startSafetyTimer(time, from) { this.player.debug.log(`Safety timer invoked from: ${from}`); @@ -608,7 +621,7 @@ class Ads { /** * Clear our safety timer(s) - * @param {string} from + * @param {String} from */ clearSafetyTimer(from) { if (!is.nullOrUndefined(this.safetyTimer)) { diff --git a/src/js/plugins/previewThumbnails.js b/src/js/plugins/preview-thumbnails.js index 834d16f2..61021d64 100644 --- a/src/js/plugins/previewThumbnails.js +++ b/src/js/plugins/preview-thumbnails.js @@ -17,17 +17,17 @@ const parseVtt = vttDataString => { if (!is.number(result.startTime)) { // The line with start and end times on it is the first line of interest const matchTimes = line.match( - /([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2,3})/, + /([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/, ); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT if (matchTimes) { result.startTime = - Number(matchTimes[1]) * 60 * 60 + + Number(matchTimes[1] || 0) * 60 * 60 + Number(matchTimes[2]) * 60 + Number(matchTimes[3]) + Number(`0.${matchTimes[4]}`); result.endTime = - Number(matchTimes[6]) * 60 * 60 + + Number(matchTimes[6] || 0) * 60 * 60 + Number(matchTimes[7]) * 60 + Number(matchTimes[8]) + Number(`0.${matchTimes[9]}`); @@ -100,6 +100,10 @@ class PreviewThumbnails { } this.getThumbnails().then(() => { + if (!this.enabled) { + return; + } + // Render DOM elements this.render(); @@ -121,7 +125,6 @@ class PreviewThumbnails { // If string, convert into single-element list const urls = is.string(src) ? [src] : src; - // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails const promises = urls.map(u => this.getThumbnail(u)); @@ -148,7 +151,12 @@ class PreviewThumbnails { // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank - if (!thumbnail.frames[0].text.startsWith('/')) { + // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file + if ( + !thumbnail.frames[0].text.startsWith('/') && + !thumbnail.frames[0].text.startsWith('http://') && + !thumbnail.frames[0].text.startsWith('https://') + ) { thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1); } @@ -220,6 +228,7 @@ class PreviewThumbnails { // Only act on left mouse button (0), or touch device (event.button is false) if (event.button === false || event.button === 0) { this.mouseDown = true; + // Wait until media has a duration if (this.player.media.duration) { this.toggleScrubbingContainer(true); @@ -293,7 +302,9 @@ class PreviewThumbnails { this.elements.thumb.container.appendChild(timeContainer); // Inject the whole thumb - this.player.elements.progress.appendChild(this.elements.thumb.container); + if (is.element(this.player.elements.progress)) { + this.player.elements.progress.appendChild(this.elements.thumb.container); + } // Create HTML element: plyr__preview-scrubbing-container this.elements.scrubbing.container = createElement('div', { @@ -307,7 +318,6 @@ class PreviewThumbnails { if (this.mouseDown) { this.setScrubbingContainerSize(); } else { - this.toggleThumbContainer(true); this.setThumbContainerSizeAndPos(); } @@ -319,7 +329,10 @@ class PreviewThumbnails { const hasThumb = thumbNum >= 0; let qualityIndex = 0; - this.toggleThumbContainer(hasThumb); + // Show the thumb container if we're not scrubbing + if (!this.mouseDown) { + this.toggleThumbContainer(hasThumb); + } // No matching thumb found if (!hasThumb) { @@ -416,7 +429,9 @@ class PreviewThumbnails { if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) { // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function + // eslint-disable-next-line no-param-reassign image.dataset.deleting = true; + // This has to be set before the timeout - to prevent issues switching between hover and scrub const { currentImageContainer } = this; @@ -457,7 +472,6 @@ class PreviewThumbnails { const { urlPrefix } = this.thumbnails[0]; const thumbURL = urlPrefix + newThumbFilename; - const previewImage = new Image(); previewImage.src = thumbURL; previewImage.onload = () => { @@ -591,11 +605,9 @@ class PreviewThumbnails { const seekbarRect = this.player.elements.progress.getBoundingClientRect(); const plyrRect = this.player.elements.container.getBoundingClientRect(); const { container } = this.elements.thumb; - // Find the lowest and highest desired left-position, so we don't slide out the side of the video container const minVal = plyrRect.left - seekbarRect.left + 10; const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10; - // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2; @@ -626,9 +638,13 @@ class PreviewThumbnails { // Find difference between height and preview container height const multiplier = this.thumbContainerHeight / frame.h; + // eslint-disable-next-line no-param-reassign previewImage.style.height = `${Math.floor(previewImage.naturalHeight * multiplier)}px`; + // eslint-disable-next-line no-param-reassign previewImage.style.width = `${Math.floor(previewImage.naturalWidth * multiplier)}px`; + // eslint-disable-next-line no-param-reassign previewImage.style.left = `-${frame.x * multiplier}px`; + // eslint-disable-next-line no-param-reassign previewImage.style.top = `-${frame.y * multiplier}px`; } } diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index a7664e73..91019abf 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -9,7 +9,7 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements'; import { triggerEvent } from '../utils/events'; import fetch from '../utils/fetch'; import is from '../utils/is'; -import loadScript from '../utils/loadScript'; +import loadScript from '../utils/load-script'; import { extend } from '../utils/objects'; import { format, stripHTML } from '../utils/strings'; import { setAspectRatio } from '../utils/style'; @@ -48,14 +48,14 @@ const vimeo = { // Set intial ratio setAspectRatio.call(this); - // Load the API if not already + // Load the SDK if not already if (!is.object(window.Vimeo)) { loadScript(this.config.urls.vimeo.sdk) .then(() => { vimeo.ready.call(this); }) .catch(error => { - this.debug.warn('Vimeo API failed to load', error); + this.debug.warn('Vimeo SDK (player.js) failed to load', error); }); } else { vimeo.ready.call(this); @@ -91,7 +91,6 @@ const vimeo = { } const id = parseId(source); - // Build an iframe const iframe = createElement('iframe'); const src = format(player.config.urls.vimeo.iframe, id, params); @@ -102,7 +101,6 @@ const vimeo = { // Get poster, if already set const { poster } = player; - // Inject the package const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer }); wrapper.appendChild(iframe); @@ -259,7 +257,7 @@ const vimeo = { .getVideoUrl() .then(value => { currentSrc = value; - controls.setDownloadLink.call(player); + controls.setDownloadUrl.call(player); }) .catch(error => { this.debug.warn(error); @@ -281,8 +279,8 @@ const vimeo = { // Set aspect ratio based on video size Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => { const [width, height] = dimensions; - player.embed.ratio = `${width}:${height}`; - setAspectRatio.call(this, player.embed.ratio); + player.embed.ratio = [width, height]; + setAspectRatio.call(this); }); // Set autopause diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 0bd232e0..31d22bb4 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -7,8 +7,8 @@ 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 loadImage from '../utils/load-image'; +import loadScript from '../utils/load-script'; import { extend } from '../utils/objects'; import { format, generateId } from '../utils/strings'; import { setAspectRatio } from '../utils/style'; @@ -34,78 +34,78 @@ function assurePlaybackState(play) { } } +function getHost(config) { + if (config.noCookie) { + return 'https://www.youtube-nocookie.com'; + } + + if (window.location.protocol === 'http:') { + return 'http://www.youtube.com'; + } + + // Use YouTube's default + return undefined; +} + const youtube = { setup() { // Add embed class for responsive toggleClass(this.elements.wrapper, this.config.classNames.embed, true); - // Set aspect ratio - setAspectRatio.call(this); - // Setup API if (is.object(window.YT) && is.function(window.YT.Player)) { youtube.ready.call(this); } else { - // Load the API - loadScript(this.config.urls.youtube.sdk).catch(error => { - this.debug.warn('YouTube API failed to load', error); - }); - - // Setup callback for the API - // YouTube has it's own system of course... - window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || []; - - // Add to queue - window.onYouTubeReadyCallbacks.push(() => { - youtube.ready.call(this); - }); + // Reference current global callback + const callback = window.onYouTubeIframeAPIReady; // Set callback to process queue window.onYouTubeIframeAPIReady = () => { - window.onYouTubeReadyCallbacks.forEach(callback => { + // 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); + }); } }, // Get the media title getTitle(videoId) { - // Try via undocumented API method first - // This method disappears now and then though... - // https://github.com/sampotts/plyr/issues/709 - if (is.function(this.embed.getVideoData)) { - const { title } = this.embed.getVideoData(); - - if (is.empty(title)) { - this.config.title = title; - ui.setTitle.call(this); - return; - } - } + const url = format(this.config.urls.youtube.api, videoId); - // Or via Google API - const key = this.config.keys.google; - if (is.string(key) && !is.empty(key)) { - const url = format(this.config.urls.youtube.api, videoId, key); + fetch(url) + .then(data => { + if (is.object(data)) { + const { title, height, width } = data; - fetch(url) - .then(result => { - if (is.object(result)) { - this.config.title = result.items[0].snippet.title; - ui.setTitle.call(this); - } - }) - .catch(() => {}); - } + // Set title + this.config.title = title; + ui.setTitle.call(this); + + // Set aspect ratio + this.embed.ratio = [width, height]; + } + + 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.getAttribute('id'); + const currentId = player.media && player.media.getAttribute('id'); if (!is.empty(currentId) && currentId.startsWith('youtube-')) { return; } @@ -121,25 +121,23 @@ const youtube = { // 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 = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`; + 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(posterSrc => { + .then(src => { // If the image is padded, use background-size "cover" instead (like youtube does too with their posters) - if (!posterSrc.includes('maxres')) { + if (!src.includes('maxres')) { player.elements.poster.style.backgroundSize = 'cover'; } }) @@ -151,7 +149,7 @@ const youtube = { // https://developers.google.com/youtube/iframe_api_reference player.embed = new window.YT.Player(id, { videoId, - host: config.noCookie ? 'https://www.youtube-nocookie.com' : undefined, + host: getHost(config), playerVars: extend( {}, { @@ -386,7 +384,7 @@ const youtube = { case 1: // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) - if (player.media.paused && !player.embed.hasPlayed) { + if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) { player.media.pause(); } else { assurePlaybackState.call(player, true); |