/* * Backstretch * http://srobbin.com/jquery-plugins/backstretch/ * * Copyright (c) 2013 Scott Robbin * Licensed under the MIT license. */ ;(function ($, window, undefined) { 'use strict'; /** @const */ var YOUTUBE_REGEXP = /^.*(youtu\.be\/|youtube\.com\/v\/|youtube\.com\/embed\/|youtube\.com\/watch\?v=|youtube\.com\/watch\?.*\&v=)([^#\&\?]*).*/i; /* PLUGIN DEFINITION * ========================= */ $.fn.backstretch = function (images, options) { var args = arguments; /* * Scroll the page one pixel to get the right window height on iOS * Pretty harmless for everyone else */ if ($(window).scrollTop() === 0) { window.scrollTo(0, 0); } var returnValues; this.each(function (eachIndex) { var $this = $(this) , obj = $this.data('backstretch'); // Do we already have an instance attached to this element? if (obj) { // Is this a method they're trying to execute? if (typeof args[0] === 'string' && typeof obj[args[0]] === 'function') { // Call the method var returnValue = obj[args[0]].apply(obj, Array.prototype.slice.call(args, 1)); if (returnValue === obj) { // If a method is chaining returnValue = undefined; } if (returnValue !== undefined) { returnValues = returnValues || []; returnValues[eachIndex] = returnValue; } return; // Nothing further to do } // Merge the old options with the new options = $.extend(obj.options, options); // Remove the old instance if (typeof obj === 'object' && 'destroy' in obj) { obj.destroy(true); } } // We need at least one image if (!images || (images && images.length === 0)) { var cssBackgroundImage = $this.css('background-image'); if (cssBackgroundImage && cssBackgroundImage !== 'none') { images = [{url: $this.css('backgroundImage').replace(/url\(|\)|"|'/g, "")}]; } else { $.error('No images were supplied for Backstretch, or element must have a CSS-defined background image.'); } } obj = new Backstretch(this, images, options || {}); $this.data('backstretch', obj); }); return returnValues ? returnValues.length === 1 ? returnValues[0] : returnValues : this; }; // If no element is supplied, we'll attach to body $.backstretch = function (images, options) { // Return the instance return $('body') .backstretch(images, options) .data('backstretch'); }; // Custom selector $.expr[':'].backstretch = function (elem) { return $(elem).data('backstretch') !== undefined; }; /* DEFAULTS * ========================= */ $.fn.backstretch.defaults = { duration: 5000 // Amount of time in between slides (if slideshow) , transition: 'fade' // Type of transition between slides , transitionDuration: 0 // Duration of transition between slides , animateFirst: true // Animate the transition of first image of slideshow in? , alignX: 0.5 // The x-alignment for the image, can be 'left'|'center'|'right' or any number between 0.0 and 1.0 , alignY: 0.5 // The y-alignment for the image, can be 'top'|'center'|'bottom' or any number between 0.0 and 1.0 , paused: false // Whether the images should slide after given duration , start: 0 // Index of the first image to show , preload: 2 // How many images preload at a time? , preloadSize: 1 // How many images can we preload in parallel? , resolutionRefreshRate: 2500 // How long to wait before switching resolution? , resolutionChangeRatioThreshold: 0.1 // How much a change should it be before switching resolution? }; /* STYLES * * Baked-in styles that we'll apply to our elements. * In an effort to keep the plugin simple, these are not exposed as options. * That said, anyone can override these in their own stylesheet. * ========================= */ var styles = { wrap: { left: 0 , top: 0 , overflow: 'hidden' , margin: 0 , padding: 0 , height: '100%' , width: '100%' , zIndex: -999999 } , itemWrapper: { position: 'absolute' , display: 'none' , margin: 0 , padding: 0 , border: 'none' , width: '100%' , height: '100%' , zIndex: -999999 } , item: { position: 'absolute' , margin: 0 , padding: 0 , border: 'none' , width: '100%' , height: '100%' , maxWidth: 'none' } }; /* Given an array of different options for an image, * choose the optimal image for the container size. * * Given an image template (a string with {{ width }} and/or * {{height}} inside) and a container object, returns the * image url with the exact values for the size of that * container. * * Returns an array of urls optimized for the specified resolution. * */ var optimalSizeImages = (function () { /* Sorts the array of image sizes based on width */ var widthInsertSort = function (arr) { for (var i = 1; i < arr.length; i++) { var tmp = arr[i], j = i; while (arr[j - 1] && parseInt(arr[j - 1].width, 10) > parseInt(tmp.width, 10)) { arr[j] = arr[j - 1]; --j; } arr[j] = tmp; } return arr; }; /* Given an array of various sizes of the same image and a container width, * return the best image. */ var selectBest = function (containerWidth, containerHeight, imageSizes) { var devicePixelRatio = window.devicePixelRatio || 1; var deviceOrientation = getDeviceOrientation(); var windowOrientation = getWindowOrientation(); var wrapperOrientation = (containerHeight > containerWidth) ? 'portrait' : (containerWidth > containerHeight ? 'landscape' : 'square'); var lastAllowedImage = 0; var testWidth; for (var j = 0, image; j < imageSizes.length; j++) { image = imageSizes[j]; // In case a new image was pushed in, process it: if (typeof image === 'string') { image = imageSizes[j] = {url: image}; } if (image.pixelRatio && image.pixelRatio !== 'auto' && parseFloat(image.pixelRatio) !== devicePixelRatio) { // We disallowed choosing this image for current device pixel ratio, // So skip this one. continue; } if (image.deviceOrientation && image.deviceOrientation !== deviceOrientation) { // We disallowed choosing this image for current device orientation, // So skip this one. continue; } if (image.windowOrientation && image.windowOrientation !== deviceOrientation) { // We disallowed choosing this image for current window orientation, // So skip this one. continue; } if (image.orientation && image.orientation !== wrapperOrientation) { // We disallowed choosing this image for current element's orientation, // So skip this one. continue; } // Mark this one as the last one we investigated // which does not violate device pixel ratio rules. // We may choose this one later if there's no match. lastAllowedImage = j; // For most images, we match the specified width against element width, // And enforcing a limit depending on the "pixelRatio" property if specified. // But if a pixelRatio="auto", then we consider the width as the physical width of the image, // And match it while considering the device's pixel ratio. testWidth = containerWidth; if (image.pixelRatio === 'auto') { containerWidth *= devicePixelRatio; } // Stop when the width of the image is larger or equal to the container width if (image.width >= testWidth) { break; } } // Use the image located at where we stopped return imageSizes[Math.min(j, lastAllowedImage)]; }; var replaceTagsInUrl = function (url, templateReplacer) { if (typeof url === 'string') { url = url.replace(/{{(width|height)}}/g, templateReplacer); } else if (url instanceof Array) { for (var i = 0; i < url.length; i++) { if (url[i].src) { url[i].src = replaceTagsInUrl(url[i].src, templateReplacer); } else { url[i] = replaceTagsInUrl(url[i], templateReplacer); } } } return url; }; return function ($container, images) { var containerWidth = $container.width(), containerHeight = $container.height(); var chosenImages = []; var templateReplacer = function (match, key) { if (key === 'width') { return containerWidth; } if (key === 'height') { return containerHeight; } return match; }; for (var i = 0; i < images.length; i++) { if ($.isArray(images[i])) { images[i] = widthInsertSort(images[i]); var chosen = selectBest(containerWidth, containerHeight, images[i]); chosenImages.push(chosen); } else { // In case a new image was pushed in, process it: if (typeof images[i] === 'string') { images[i] = {url: images[i]}; } var item = $.extend({}, images[i]); item.url = replaceTagsInUrl(item.url, templateReplacer); chosenImages.push(item); } } return chosenImages; }; })(); var isVideoSource = function (source) { return YOUTUBE_REGEXP.test(source.url) || source.isVideo; }; /* Preload images */ var preload = (function (sources, startAt, count, batchSize, callback) { // Plugin cache var cache = []; // Wrapper for cache var caching = function (image) { for (var i = 0; i < cache.length; i++) { if (cache[i].src === image.src) { return cache[i]; } } cache.push(image); return image; }; // Execute callback var exec = function (sources, callback, last) { if (typeof callback === 'function') { callback.call(sources, last); } }; // Closure to hide cache return function preload(sources, startAt, count, batchSize, callback) { // Check input data if (typeof sources === 'undefined') { return; } if (!$.isArray(sources)) { sources = [sources]; } if (arguments.length < 5 && typeof arguments[arguments.length - 1] === 'function') { callback = arguments[arguments.length - 1]; } startAt = (typeof startAt === 'function' || !startAt) ? 0 : startAt; count = (typeof count === 'function' || !count || count < 0) ? sources.length : Math.min(count, sources.length); batchSize = (typeof batchSize === 'function' || !batchSize) ? 1 : batchSize; if (startAt >= sources.length) { startAt = 0; count = 0; } if (batchSize < 0) { batchSize = count; } batchSize = Math.min(batchSize, count); var next = sources.slice(startAt + batchSize, count - batchSize); sources = sources.slice(startAt, batchSize); count = sources.length; // If sources array is empty if (!count) { exec(sources, callback, true); return; } // Image loading callback var countLoaded = 0; var loaded = function () { countLoaded++; if (countLoaded !== count) { return; } exec(sources, callback, !next); preload(next, 0, 0, batchSize, callback); }; // Loop sources to preload var image; for (var i = 0; i < sources.length; i++) { if (isVideoSource(sources[i])) { // Do not preload videos. There are issues with that. // First - we need to keep an instance of the preloaded and use that exactly, not a copy. // Second - there are memory issues. // If there will be a requirement from users - I'll try to implement this. continue; } else { image = new Image(); image.src = sources[i].url; image = caching(image); if (image.complete) { loaded(); } else { $(image).on('load error', loaded); } } } }; })(); /* Process images array */ var processImagesArray = function (images) { var processed = []; for (var i = 0; i < images.length; i++) { if (typeof images[i] === 'string') { processed.push({url: images[i]}); } else if ($.isArray(images[i])) { processed.push(processImagesArray(images[i])); } else { processed.push(processOptions(images[i])); } } return processed; }; /* Process options */ var processOptions = function (options, required) { // Convert old options // centeredX/centeredY are deprecated if (options.centeredX || options.centeredY) { if (window.console && window.console.log) { window.console.log('jquery.backstretch: `centeredX`/`centeredY` is deprecated, please use `alignX`/`alignY`'); } if (options.centeredX) { options.alignX = 0.5; } if (options.centeredY) { options.alignY = 0.5; } } // Deprecated spec if (options.speed !== undefined) { if (window.console && window.console.log) { window.console.log('jquery.backstretch: `speed` is deprecated, please use `transitionDuration`'); } options.transitionDuration = options.speed; options.transition = 'fade'; } // Typo if (options.resolutionChangeRatioTreshold !== undefined) { window.console.log('jquery.backstretch: `treshold` is a typo!'); options.resolutionChangeRatioThreshold = options.resolutionChangeRatioTreshold; } // Current spec that needs processing if (options.fadeFirst !== undefined) { options.animateFirst = options.fadeFirst; } if (options.fade !== undefined) { options.transitionDuration = options.fade; options.transition = 'fade'; } if (options.scale) { options.scale = validScale(options.scale); } return processAlignOptions(options); }; /* Process align options */ var processAlignOptions = function (options, required) { if (options.alignX === 'left') { options.alignX = 0.0; } else if (options.alignX === 'center') { options.alignX = 0.5; } else if (options.alignX === 'right') { options.alignX = 1.0; } else { if (options.alignX !== undefined || required) { options.alignX = parseFloat(options.alignX); if (isNaN(options.alignX)) { options.alignX = 0.5; } } } if (options.alignY === 'top') { options.alignY = 0.0; } else if (options.alignY === 'center') { options.alignY = 0.5; } else if (options.alignY === 'bottom') { options.alignY = 1.0; } else { if (options.alignX !== undefined || required) { options.alignY = parseFloat(options.alignY); if (isNaN(options.alignY)) { options.alignY = 0.5; } } } return options; }; var SUPPORTED_SCALE_OPTIONS = { 'cover': 'cover', 'fit': 'fit', 'fit-smaller': 'fit-smaller', 'fill': 'fill' }; function validScale(scale) { if (!SUPPORTED_SCALE_OPTIONS.hasOwnProperty(scale)) { return 'cover'; } return scale; } /* CLASS DEFINITION * ========================= */ var Backstretch = function (container, images, options) { this.options = $.extend({}, $.fn.backstretch.defaults, options || {}); this.firstShow = true; // Process options processOptions(this.options, true); /* In its simplest form, we allow Backstretch to be called on an image path. * e.g. $.backstretch('/path/to/image.jpg') * So, we need to turn this back into an array. */ this.images = processImagesArray($.isArray(images) ? images : [images]); /** * Paused-Option */ if (this.options.paused) { this.paused = true; } /** * Start-Option (Index) */ if (this.options.start >= this.images.length) { this.options.start = this.images.length - 1; } if (this.options.start < 0) { this.options.start = 0; } // Convenience reference to know if the container is body. this.isBody = container === document.body; /* We're keeping track of a few different elements * * Container: the element that Backstretch was called on. * Wrap: a DIV that we place the image into, so we can hide the overflow. * Root: Convenience reference to help calculate the correct height. */ var $window = $(window); this.$container = $(container); this.$root = this.isBody ? supportsFixedPosition ? $window : $(document) : this.$container; this.originalImages = this.images; this.images = optimalSizeImages( this.options.alwaysTestWindowResolution ? $window : this.$root, this.originalImages); /** * Pre-Loading. * This is the first image, so we will preload a minimum of 1 images. */ preload(this.images, this.options.start || 0, this.options.preload || 1); // Don't create a new wrap if one already exists (from a previous instance of Backstretch) var $existing = this.$container.children(".backstretch").first(); this.$wrap = $existing.length ? $existing : $('
') .css(this.options.bypassCss ? {} : styles.wrap) .appendTo(this.$container); if (!this.options.bypassCss) { // Non-body elements need some style adjustments if (!this.isBody) { // If the container is statically positioned, we need to make it relative, // and if no zIndex is defined, we should set it to zero. var position = this.$container.css('position') , zIndex = this.$container.css('zIndex'); this.$container.css({ position: position === 'static' ? 'relative' : position , zIndex: zIndex === 'auto' ? 0 : zIndex }); // Needs a higher z-index this.$wrap.css({zIndex: -999998}); } // Fixed or absolute positioning? this.$wrap.css({ position: this.isBody && supportsFixedPosition ? 'fixed' : 'absolute' }); } // Set the first image this.index = this.options.start; this.show(this.index); // Listen for resize $window.on('resize.backstretch', $.proxy(this.resize, this)) .on('orientationchange.backstretch', $.proxy(function () { // Need to do this in order to get the right window height if (this.isBody && window.pageYOffset === 0) { window.scrollTo(0, 1); this.resize(); } }, this)); }; var performTransition = function (options) { var transition = options.transition || 'fade'; // Look for multiple options if (typeof transition === 'string' && transition.indexOf('|') > -1) { transition = transition.split('|'); } if (transition instanceof Array) { transition = transition[Math.round(Math.random() * (transition.length - 1))]; } var $new = options['new']; var $old = options['old'] ? options['old'] : $([]); switch (transition.toString().toLowerCase()) { default: case 'fade': $new.fadeIn({ duration: options.duration, complete: options.complete, easing: options.easing || undefined }); break; case 'fadeinout': case 'fade_in_out': var fadeInNew = function () { $new.fadeIn({ duration: options.duration / 2, complete: options.complete, easing: options.easing || undefined }); }; if ($old.length) { $old.fadeOut({ duration: options.duration / 2, complete: fadeInNew, easing: options.easing || undefined }); } else { fadeInNew(); } break; case 'pushleft': case 'push_left': case 'pushright': case 'push_right': case 'pushup': case 'push_up': case 'pushdown': case 'push_down': case 'coverleft': case 'cover_left': case 'coverright': case 'cover_right': case 'coverup': case 'cover_up': case 'coverdown': case 'cover_down': var transitionParts = transition.match(/^(cover|push)_?(.*)$/); var animProp = transitionParts[2] === 'left' ? 'right' : transitionParts[2] === 'right' ? 'left' : transitionParts[2] === 'down' ? 'top' : transitionParts[2] === 'up' ? 'bottom' : 'right'; var newCssStart = { 'display': '' }, newCssAnim = {}; newCssStart[animProp] = '-100%'; newCssAnim[animProp] = 0; $new .css(newCssStart) .animate(newCssAnim, { duration: options.duration, complete: function () { $new.css(animProp, ''); options.complete.apply(this, arguments); }, easing: options.easing || undefined }); if (transitionParts[1] === 'push' && $old.length) { var oldCssAnim = {}; oldCssAnim[animProp] = '100%'; $old .animate(oldCssAnim, { duration: options.duration, complete: function () { $old.css('display', 'none'); }, easing: options.easing || undefined }); } break; } }; /* PUBLIC METHODS * ========================= */ Backstretch.prototype = { resize: function () { try { // Check for a better suited image after the resize var $resTest = this.options.alwaysTestWindowResolution ? $(window) : this.$root; var newContainerWidth = $resTest.width(); var newContainerHeight = $resTest.height(); var changeRatioW = newContainerWidth / (this._lastResizeContainerWidth || 0); var changeRatioH = newContainerHeight / (this._lastResizeContainerHeight || 0); var resolutionChangeRatioThreshold = this.options.resolutionChangeRatioThreshold || 0.0; // check for big changes in container size if ((newContainerWidth !== this._lastResizeContainerWidth || newContainerHeight !== this._lastResizeContainerHeight) && ((Math.abs(changeRatioW - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioW)) || (Math.abs(changeRatioH - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioH)))) { this._lastResizeContainerWidth = newContainerWidth; this._lastResizeContainerHeight = newContainerHeight; // Big change: rebuild the entire images array this.images = optimalSizeImages($resTest, this.originalImages); // Preload them (they will be automatically inserted on the next cycle) if (this.options.preload) { preload(this.images, (this.index + 1) % this.images.length, this.options.preload); } // In case there is no cycle and the new source is different than the current if (this.images.length === 1 && this._currentImage.url !== this.images[0].url) { // Wait a little an update the image being showed var that = this; clearTimeout(that._selectAnotherResolutionTimeout); that._selectAnotherResolutionTimeout = setTimeout(function () { that.show(0); }, this.options.resolutionRefreshRate); } } var bgCSS = {left: 0, top: 0, right: 'auto', bottom: 'auto'} , boxWidth = this.isBody ? this.$root.width() : this.$root.innerWidth() , boxHeight = this.isBody ? (window.innerHeight ? window.innerHeight : this.$root.height()) : this.$root.innerHeight() , naturalWidth = this.$itemWrapper.data('width') , naturalHeight = this.$itemWrapper.data('height') , ratio = (naturalWidth / naturalHeight) || 1 , alignX = this._currentImage.alignX === undefined ? this.options.alignX : this._currentImage.alignX , alignY = this._currentImage.alignY === undefined ? this.options.alignY : this._currentImage.alignY , scale = validScale(this._currentImage.scale || this.options.scale); var width, height; if (scale === 'fit' || scale === 'fit-smaller') { width = naturalWidth; height = naturalHeight; if (width > boxWidth || height > boxHeight || scale === 'fit-smaller') { var boxRatio = boxWidth / boxHeight; if (boxRatio > ratio) { width = Math.floor(boxHeight * ratio); height = boxHeight; } else if (boxRatio < ratio) { width = boxWidth; height = Math.floor(boxWidth / ratio); } else { width = boxWidth; height = boxHeight; } } } else if (scale === 'fill') { width = boxWidth; height = boxHeight; } else { // 'cover' width = Math.max(boxHeight * ratio, boxWidth); height = Math.max(width / ratio, boxHeight); } // Make adjustments based on image ratio bgCSS.top = -(height - boxHeight) * alignY; bgCSS.left = -(width - boxWidth) * alignX; bgCSS.width = width; bgCSS.height = height; if (!this.options.bypassCss) { this.$wrap .css({width: boxWidth, height: boxHeight}) .find('>.backstretch-item').not('.deleteable') .each(function () { var $wrapper = $(this); $wrapper.find('img,video,iframe') .css(bgCSS); }); } var evt = $.Event('backstretch.resize', { relatedTarget: this.$container[0] }); this.$container.trigger(evt, this); } catch (err) { // IE7 seems to trigger resize before the image is loaded. // This try/catch block is a hack to let it fail gracefully. } return this; } // Show the slide at a certain position , show: function (newIndex, overrideOptions) { // Validate index if (Math.abs(newIndex) > this.images.length - 1) { return; } // Vars var that = this , $oldItemWrapper = that.$wrap.find('>.backstretch-item').addClass('deleteable') , oldVideoWrapper = that.videoWrapper , evtOptions = {relatedTarget: that.$container[0]}; // Trigger the "before" event that.$container.trigger($.Event('backstretch.before', evtOptions), [that, newIndex]); // Set the new frame index this.index = newIndex; var selectedImage = that.images[newIndex]; // Pause the slideshow clearTimeout(that._cycleTimeout); // New image delete that.videoWrapper; // Current item may not be a video var isVideo = isVideoSource(selectedImage); if (isVideo) { that.videoWrapper = new VideoWrapper(selectedImage); that.$item = that.videoWrapper.$video.css('pointer-events', 'none'); } else { that.$item = $(''); } that.$itemWrapper = $('
') .append(that.$item); if (this.options.bypassCss) { that.$itemWrapper.css({ 'display': 'none' }); } else { that.$itemWrapper.css(styles.itemWrapper); that.$item.css(styles.item); } that.$item.bind(isVideo ? 'canplay' : 'load', function (e) { var $this = $(this) , $wrapper = $this.parent() , options = $wrapper.data('options'); if (overrideOptions) { options = $.extend({}, options, overrideOptions); } var imgWidth = this.naturalWidth || this.videoWidth || this.width , imgHeight = this.naturalHeight || this.videoHeight || this.height; // Save the natural dimensions $wrapper .data('width', imgWidth) .data('height', imgHeight); var getOption = function (opt) { return options[opt] !== undefined ? options[opt] : that.options[opt]; }; var transition = getOption('transition'); var transitionEasing = getOption('transitionEasing'); var transitionDuration = getOption('transitionDuration'); // Show the image, then delete the old one var bringInNextImage = function () { if (oldVideoWrapper) { oldVideoWrapper.stop(); oldVideoWrapper.destroy(); } $oldItemWrapper.remove(); // Resume the slideshow if (!that.paused && that.images.length > 1) { that.cycle(); } // Now we can clear the background on the element, to spare memory if (!that.options.bypassCss && !that.isBody) { that.$container.css('background-image', 'none'); } // Trigger the "after" and "show" events // "show" is being deprecated $(['after', 'show']).each(function () { that.$container.trigger($.Event('backstretch.' + this, evtOptions), [that, newIndex]); }); if (isVideo) { that.videoWrapper.play(); } }; if ((that.firstShow && !that.options.animateFirst) || !transitionDuration || !transition) { // Avoid transition on first show or if there's no transitionDuration value $wrapper.show(); bringInNextImage(); } else { performTransition({ 'new': $wrapper, old: $oldItemWrapper, transition: transition, duration: transitionDuration, easing: transitionEasing, complete: bringInNextImage }); } that.firstShow = false; // Resize that.resize(); }); that.$itemWrapper.appendTo(that.$wrap); that.$item.attr('alt', selectedImage.alt || ''); that.$itemWrapper.data('options', selectedImage); if (!isVideo) { that.$item.attr('src', selectedImage.url); } that._currentImage = selectedImage; return that; } , current: function () { return this.index; } , next: function () { var args = Array.prototype.slice.call(arguments, 0); args.unshift(this.index < this.images.length - 1 ? this.index + 1 : 0); return this.show.apply(this, args); } , prev: function () { var args = Array.prototype.slice.call(arguments, 0); args.unshift(this.index === 0 ? this.images.length - 1 : this.index - 1); return this.show.apply(this, args); } , pause: function () { // Pause the slideshow this.paused = true; if (this.videoWrapper) { this.videoWrapper.pause(); } return this; } , resume: function () { // Resume the slideshow this.paused = false; if (this.videoWrapper) { this.videoWrapper.play(); } this.cycle(); return this; } , cycle: function () { // Start/resume the slideshow if (this.images.length > 1) { // Clear the timeout, just in case clearTimeout(this._cycleTimeout); var duration = (this._currentImage && this._currentImage.duration) || this.options.duration; var isVideo = isVideoSource(this._currentImage); var callNext = function () { this.$item.off('.cycle'); // Check for paused slideshow if (!this.paused) { this.next(); } }; // Special video handling if (isVideo) { // Leave video at last frame if (!this._currentImage.loop) { var lastFrameTimeout = 0; this.$item .on('playing.cycle', function () { var player = $(this).data('player'); clearTimeout(lastFrameTimeout); lastFrameTimeout = setTimeout(function () { player.pause(); player.$video.trigger('ended'); }, (player.getDuration() - player.getCurrentTime()) * 1000); }) .on('ended.cycle', function () { clearTimeout(lastFrameTimeout); }); } // On error go to next this.$item.on('error.cycle initerror.cycle', $.proxy(callNext, this)); } if (isVideo && !this._currentImage.duration) { // It's a video - playing until end this.$item.on('ended.cycle', $.proxy(callNext, this)); } else { // Cycling according to specified duration this._cycleTimeout = setTimeout($.proxy(callNext, this), duration); } } return this; } , destroy: function (preserveBackground) { // Stop the resize events $(window).off('resize.backstretch orientationchange.backstretch'); // Stop any videos if (this.videoWrapper) { this.videoWrapper.destroy(); } // Clear the timeout clearTimeout(this._cycleTimeout); // Remove Backstretch if (!preserveBackground) { this.$wrap.remove(); } this.$container.removeData('backstretch'); } }; /** * Video Abstraction Layer * * Static methods: * > VideoWrapper.loadYoutubeAPI() -> Call in order to load the Youtube API. * An 'youtube_api_load' event will be triggered on $(window) when the API is loaded. * * Generic: * > player.type -> type of the video * > player.video / player.$video -> contains the element holding the video * > player.play() -> plays the video * > player.pause() -> pauses the video * > player.setCurrentTime(position) -> seeks to a position by seconds * * Youtube: * > player.ytId will contain the youtube ID if the source is a youtube url * > player.ytReady is a flag telling whether the youtube source is ready for playback * */ var VideoWrapper = function () { this.init.apply(this, arguments); }; /** * @param {Object} options * @param {String|Array|Array<{{src: String, type: String?}}>} options.url * @param {Boolean} options.loop=false * @param {Boolean?} options.mute=true * @param {String?} options.poster * loop, mute, poster */ VideoWrapper.prototype.init = function (options) { var that = this; var $video; var setVideoElement = function () { that.$video = $video; that.video = $video[0]; }; // Determine video type var videoType = 'video'; if (!(options.url instanceof Array) && YOUTUBE_REGEXP.test(options.url)) { videoType = 'youtube'; } that.type = videoType; if (videoType === 'youtube') { // Try to load the API in the meantime VideoWrapper.loadYoutubeAPI(); that.ytId = options.url.match(YOUTUBE_REGEXP)[2]; var src = 'https://www.youtube.com/embed/' + that.ytId + '?rel=0&autoplay=0&showinfo=0&controls=0&modestbranding=1' + '&cc_load_policy=0&disablekb=1&iv_load_policy=3&loop=0' + '&enablejsapi=1&origin=' + encodeURIComponent(window.location.origin); that.__ytStartMuted = !!options.mute || options.mute === undefined; $video = $('