La carga de un objeto de tipo vídeo es una operación realmente costosa y pesada respecto a rendimiento de una página. Máxime si estamos haciendo una galería de vídeos donde se muestran varios vídeos a la vez.
Una primera aproximación para aligerar esa carga es lo que se ha hecho toda la vida mediante la utilización de thumbnails de los vídeos, pero yo al menos siempre me quedo con la sensación de que quiero algo más... un triste fotograma de un vídeo no me saca de pobre y más si es un fotograma automatizado que puede que coincida con una pantalla en negro.
Pero... y si tuviéramos varios fotogramas de un mismo vídeo. Se podrían poner uno a continuación del otro para hacer una secuencia, pero es algo que se antoja "artificial"... lo ideal sería que dichos fotogramas se reprodujeran en sucesión antes de la carga real del vídeo de modo que solo se muestre el vídeo en sí bajo petición por parte del usuario.
Ahí es donde entra FRoll, un plugin 100% hecho por mí, haciéndonos la vida un poco más sencilla y más si el vídeo en cuestión está alojado en youtube, que hace el trabajo de sacar fotogramas de los vídeos por nosotros (sólo 3, pero qué le vamos a hacer).
Una vez incluido este plugin, tendremos disponible el método .froll() que podremos llamar sobre las imágenes que deseemos convertir en previsualizaciones de vídeos. El nombre de los fotogramas se deriva a partir del nombre de la imagen original mediante sencillas reglas de transformación y ya se proporcionan reglas para los escenarios más comunes que son el de imágenes alojadas en youtube, thumbnail en local y fotogramas en remoto en youtube e imagen + fotogramas en local. Usando dichos ejemplos es fácil adaptar el comportamiento a las necesidades de cada uno.
También soporta el la delegación del evento de click para permitirnos completar acciones sobre la imagen original y que el uso del plugin sea totalmente transparente tanto para el usuario como para el desarrollador.
Para ver el plugin en acción, como es costumbre, podéis hacerlo en mi fiddle o en la página del plugin en el
sitio de jQuery.
Y ahora, el código del plugin, que está comentado hasta el aburrimiento para que no se pierda nadie, y ya sabéis, si lo usáis, mejorais, arregláis, hacédmelo saber ;)
/**
* jquery.froll-0.1.js - Fancy Image Roll
* ==========================================================
* (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
*
* http://3nibbles.blogspot.com/2011/07/plugin-jquery-sliding-buttons.html
* http://plugins.jquery.com/project/froll
*
* FRoll is a jQuery plugin to simplify the task of providing a simple and
* efficient way to expand the image information of a picture using a sequence
* of images.
*
* The direct use of this plugin is to provide a preview of a youtube video
* using the Google API. In that case it presents a sucession of three
* different moments of the video in a smooth sucession when mouse enters into
* the image.
*
* Everything is self-contained. No need of extra CSS or complex controls,
* just call the method over the image and you are ready.
*
* INSTANTIATION
* Just call the ".froll()" method over the images selector.
*
* $('.videoCaptionImg').froll( [ options_object ] );
*
* OPTIONS
* - transform $.froll.youtube Array that defines [0] as the regex to apply
* to src and [1] as the resulting string with
* {number} placeholder for the animation images.
* - frames [1, 2, 3], Array with the {number} of each frame of the animation
* - width null, Width of the animation frames. Null = automatic
* - height null, Height of the animation frames. Null = automatic
* - speed 750, Fade animation speed
* - time 1500, Time between frames
* - click function(taget) Click callback function. Defaults to trigger click
* event over the container <a> of the img.
*
* PUBLIC API
* - $.froll.stop() Stops current animation and hides the preview
*
* HELPER CONSTANTS
* $.froll.youtube Transform array that converts origin src stored in youtube
* (http://img.youtube.com/vi/<video_ID>/0.jpg) into youtube previews.
* $.froll.youtubeLocal Transform array that converts local stored captions with name the
* id of the video, into youtube previews.
* $.froll.local Transform array that converts local stored captions into local
* stored previews with the same name but ended with "_<number>"
* in the same directory than caption.
*
* CSS CLASSES
* - Container: #froll-overlay
* - Frames: .froll-frame
*
* Legal stuff
* You are free to use this code, but you must give credit and/or keep header intact.
* Please, tell me if you find it useful. An email will be enough.
* If you enhance this code or correct a bug, please, tell me.
*/
(function( $ ) {
///////////////////////////////////////////////////////////////////////////////
// Private members
///////////////////////////////////////////////////////////////////////////////
var busy = false,
overlay = null,
frames = [],
frame = -1,
lframe = 0,
options = {},
target = null,
src = "",
tickTimer = null,
imgPreloader = new Image(),
//isIE6 = $.browser.msie && $.browser.version < 7 && !window.XMLHttpRequest,
// ========================================================================
// Starts the animation
_start = function() {
_stop(); // Hides current animation (if any)
if( !target.attr('src') ) return; // No image src
// Gets the options and src transform
options = target.data('froll');
src = target.attr('src').replace( options.transform[0], options.transform[1] );
// Moves the overlay to the target position
var pos = _getPos(target);
overlay.css({
'position' : 'absolute',
'left' : pos.left,
'top' : pos.top,
'width' : pos.width,
'height' : pos.height,
'zIndex' : 9999,
'background': 'transparent'
//,'border': '1px solid red'
}).show();
// ========================================================================
// Starts the frames preload chain loading first frame
lframe = 0; // Frame being loaded
if(!imgPreloader) imgPreloader = new Image();
imgPreloader.onerror = function() { _error(); };
imgPreloader.onload = _preloadCompleted;
imgPreloader.src = src.replace( /\{number\}/, ""+options.frames[lframe] );
if(imgPreloader.complete) _preloadCompleted(); // Cached images don't fire onload events
},
// ========================================================================
// Gracefully stops the animation
_stop = function() {
clearInterval(tickTimer); // Disables the timer
imgPreloader.onerror = imgPreloader.onload = null;
if( target && overlay.is( ':visible' ) )
overlay.hide().empty(); // Hides the overlay and deletes the frames
frames = [];
frame = -1;
//target = options = null;
busy = false;
},
// ========================================================================
// Frame image load error
_error = function() {
alert( "Error loading image at " + src );
},
// ========================================================================
// Function called on animation click
_click = function(e) {
if( typeof options.click !== 'undefined' )
options.click(target);
},
// ========================================================================
// Image preload complete event
_preloadCompleted = function() {
// Gets default image dimensions
if( !options.width ) options.width = overlay.width(); //imgPreloader.width;
if( !options.height ) options.height = overlay.height(); //imgPreloader.height;
// Creates frame ima
$("<img />").attr({
'id' : 'froll-frame-' + lframe,
'class': 'froll-frame',
'src' : imgPreloader.src
}).css({
'position' : 'absolute',
'display' : 'block',
'left' : '0px',
'top' : '0px',
'width' : options.width+'px',
'height' : options.height+'px',
'zIndex' : lframe+1,
'opacity' : 0
//,'visibility': 'hidden'
}).appendTo( overlay );
// Shows first frame
if( lframe == 0 )
{
_tick();
tickTimer = setInterval( _tick, options.time );
}
// Preloads next frame
frames[ lframe++ ] = 1; // Marks frame as done
if( lframe < options.frames.length )
{
// Intermediate frame
imgPreloader.src = src.replace( /\{number\}/i, options.frames[lframe] );
if(imgPreloader.complete) _preloadCompleted(); // Cached images don't fire onload events
}
else
imgPreloader.onerror = imgPreloader.onload = null; // Last frame
},
// ========================================================================
// Shows next frame
_tick = function() {
var l = options.frames.length - 1;
var children = overlay.children();
// Animates next frame
if( frame == -1 )
{
// First run
children.eq( 0 ).css('opacity', 0).stop( true, true ).animate( { 'opacity': 1 }, options.speed );
frame = 0;
}
else if( frame == 0 )
{
// First frame (after a full run)
for(var i = 1; i < l; i++) children.eq( i ).css( 'opacity' , 0); // Hides all but first and last frames
children.eq( 0 ).css( 'opacity', 1 ).show(); // Shows first frame (bellow last frame)
children.eq( l ).stop( true, true ).animate( { 'opacity': 0 }, options.speed );
}
else if( frame <= l )
{
// Intermediate frame
var next = children.eq( frame );
if(next)
next.css('opacity', 0).stop( true, true ).animate( { 'opacity': 1 }, options.speed );
}
else
{
// The last frame. Resets animation to show first frame and hide the last one
children.eq( 0 ).css( 'opacity', 1 );
children.eq( l ).stop( true, true ).animate( { 'opacity': 0 }, options.speed );
}
frame = (frame+1) % (l+1);
},
// ========================================================================
// Helper function to get the exact obj position in the page
_getPos = function(obj) {
var pos = obj.offset();
pos.top += parseInt( obj.css( 'paddingTop' ) , 10 ) || 0;
pos.left += parseInt( obj.css( 'paddingLeft' ) , 10 ) || 0;
pos.top += parseInt( obj.css( 'border-top-width' ) , 10 ) || 0;
pos.left += parseInt( obj.css( 'border-left-width' ), 10 ) || 0;
pos.width = obj.width();
pos.height = obj.height();
return pos;
};
///////////////////////////////////////////////////////////////////////////////
// Public members
///////////////////////////////////////////////////////////////////////////////
// ========================================================================
// Instantiation. Called on every object of the supplied selector
$.fn.froll = function( obj ) {
if (!$(this).length) {
return this;
}
if( $(this).data( 'froll' ) )
{
// Object already initialized. Starts the animation over it simulating a click
if($(this).click) $(this).click();
}
else
{
// Object not initialized. Sets data and binds events
$(this)
.data( 'froll', $.extend( $.fn.froll.defaults, obj ) )
.unbind( 'mouseenter' )
.bind( 'mouseenter', function(e) {
var self = $(this);
e.preventDefault();
if (busy && self !== target) _stop(); // Stops current animation
busy = true;
//var rel = self.attr('rel') || '';
target = self;
_start(); // Starts the animation over target element
return;
});
}
return this;
};
// ========================================================================
// Container class for the public interface
$.froll = function(obj) { };
// ========================================================================
// Inits the components needed for the animation overlay
$.froll.init = function() {
if ( $( '#froll-overlay' ).length ) {
return;
}
// Components
$('body').append(
overlay = $( '<div id="froll-overlay"></div>' )
);
// Animation controls events
overlay.mouseleave( _stop );
overlay.click( _click );
return this;
};
// ========================================================================
// Stops current animation and hides overlay
$.froll.stop = function() { _stop(); };
// ========================================================================
// Sample transformation arrays
$.froll.youtube = [ /.*\/(.*)\/0\.(jpg|gif|png|bmp|jpeg)(.*)?/i, 'http://img.youtube.com/vi/$1/{number}.$2' ]; // Caption image is located in youtube (http://img.youtube.com/vi/<video_ID>/0.jpg)
$.froll.youtubeLocal = [ /.*\/(.*)\.(jpg|gif|png|bmp|jpeg)(.*)?/i , 'http://img.youtube.com/vi/$1/{number}.$2' ]; // Caption image is located elsewhere but caption image name is the youtube video_ID
$.froll.local = [ /(.*)\/(.*)\.(jpg|gif|png|bmp|jpeg)(.*)?/i , '$1/$2_{number}.$3$4' ]; // Caption image is located elsewhere and frames are in format "originalImage_<frame>.jpg" in the same directory
// ========================================================================
// Froll options defaults
$.fn.froll.defaults = {
transform : $.froll.youtube, // Array that defines [0] as the regex to apply to src and [1] as the resulting string with {number} placeholder for the animation images
frames : [1, 2, 3], // Array with the {number} of each frame of the animation
width : null, // Width of the animation frames. Null = automatic frame image width
height : null, // Height of the animation frames. Null = automatic frame image height
speed : 750, // Fade animation speed
time : 1500, // Time between frames
click : function(taget) { $(target).closest('a').click(); } // Click callback function
};
// ========================================================================
// Inits the animation overlay on DOM ready
$(document).ready(function() {
$.froll.init();
});
})( jQuery );
Y ahora, la versión minimizada, que se queda en 2.5Kb (aún menos gzipeada, claro)
/**
* jquery.froll-0.1.js - Fancy Image Roll - (C) 2011 José Ramón Díaz - jrdiazweb@gmail.com
* http://3nibbles.blogspot.com/2011/07/jquery-froll-plugin-preview-de-videos.html
* http://plugins.jquery.com/project/froll
* You are free to use this code, but you must give credit and/or keep header intact.
*/
(function(a){var k=!1,d=null,g=-1,h=0,b={},e=null,i="",m=null,c=new Image,p=function(){j();if(e.attr("src")){b=e.data("froll");i=e.attr("src").replace(b.transform[0],b.transform[1]);var a=o(e);d.css({position:"absolute",left:a.left,top:a.top,width:a.width,height:a.height,zIndex:9999,background:"transparent"}).show();h=0;c||(c=new Image);c.onerror=function(){alert("Error loading image at "+i)};c.onload=l;c.src=i.replace(/\{number\}/,""+b.frames[h]);c.complete&&l()}},j=function(){clearInterval(m);c.onerror= c.onload=null;e&&d.is(":visible")&&d.hide().empty();g=-1;k=!1},q=function(){typeof b.click!=="undefined"&&b.click(e)},l=function(){if(!b.width)b.width=d.width();if(!b.height)b.height=d.height();a("<img />").attr({id:"froll-frame-"+h,"class":"froll-frame",src:c.src}).css({position:"absolute",display:"block",left:"0px",top:"0px",width:b.width+"px",height:b.height+"px",zIndex:h+1,opacity:0}).appendTo(d);h==0&&(n(),m=setInterval(n,b.time));h++;h<b.frames.length?(c.src=i.replace(/\{number\}/i,b.frames[h]), c.complete&&l()):c.onerror=c.onload=null},n=function(){var a=b.frames.length-1,f=d.children();if(g==-1)f.eq(0).css("opacity",0).animate({opacity:1},b.speed),g=0;else if(g==0){for(var c=1;c<a;c++)f.eq(c).css("opacity",0);f.eq(0).css("opacity",1).show();f.eq(a).animate({opacity:0},b.speed)}else g<=a?(f=f.eq(g))&&f.css("opacity",0).animate({opacity:1},b.speed):(f.eq(0).css("opacity",1),f.eq(a).animate({opacity:0},b.speed));g=(g+1)%(a+1)},o=function(a){var b=a.offset();b.top+=parseInt(a.css("paddingTop"), 10)||0;b.left+=parseInt(a.css("paddingLeft"),10)||0;b.top+=parseInt(a.css("border-top-width"),10)||0;b.left+=parseInt(a.css("border-left-width"),10)||0;b.width=a.width();b.height=a.height();return b};a.fn.froll=function(b){if(!a(this).length)return this;a(this).data("froll")?a(this).click&&a(this).click():a(this).data("froll",a.extend(a.fn.froll.defaults,b)).unbind("mouseenter").bind("mouseenter",function(b){var c=a(this);b.preventDefault();k&&c!==e&&j();k=!0;e=c;p()});return this};a.froll=function(){}; a.froll.init=function(){if(!a("#froll-overlay").length)return a("body").append(d=a('<div id="froll-overlay"></div>')),d.mouseleave(j),d.click(q),this};a.froll.stop=function(){j()};a.froll.youtube=[/.*\/(.*)\/0\.(jpg|gif|png|bmp|jpeg)(.*)?/i,"http://img.youtube.com/vi/$1/{number}.$2"];a.froll.youtubeLocal=[/.*\/(.*)\.(jpg|gif|png|bmp|jpeg)(.*)?/i,"http://img.youtube.com/vi/$1/{number}.$2"];a.froll.local=[/(.*)\/(.*)\.(jpg|gif|png|bmp|jpeg)(.*)?/i,"$1/$2_{number}.$3$4"];a.fn.froll.defaults={transform:a.froll.youtube, frames:[1,2,3],width:null,height:null,speed:750,time:1500,click:function(){a(e).closest("a").click()}};a(document).ready(function(){a.froll.init()})})(jQuery);
Actualización. Añadida la clase CSS para cambiar el cursor a tipo "mano" al hacer hover. Ver en el jsfiddle.