diff -r ec4f33084f8d -r f6232b308fbd web/static/res/js/popcorn.sequence.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/static/res/js/popcorn.sequence.js Thu Oct 04 20:39:57 2012 +0200 @@ -0,0 +1,592 @@ +/*! + * Popcorn.sequence + * + * Copyright 2011, Rick Waldron + * Licensed under MIT license. + * + */ + +/* jslint forin: true, maxerr: 50, indent: 4, es5: true */ +/* global Popcorn: true */ + +// Requires Popcorn.js +(function( global, Popcorn ) { + + // TODO: as support increases, migrate to element.dataset + var doc = global.document, + location = global.location, + rprotocol = /:\/\//, + // TODO: better solution to this sucky stop-gap + lochref = location.href.replace( location.href.split("/").slice(-1)[0], "" ), + // privately held + range = function(start, stop, step) { + + start = start || 0; + stop = ( stop || start || 0 ) + 1; + step = step || 1; + + var len = Math.ceil((stop - start) / step) || 0, + idx = 0, + range = []; + + range.length = len; + + while (idx < len) { + range[idx++] = start; + start += step; + } + return range; + }; + + Popcorn.sequence = function( parent, list ) { + return new Popcorn.sequence.init( parent, list ); + }; + + Popcorn.sequence.init = function( parent, list ) { + + // Video element + this.parent = doc.getElementById( parent ); + + // Store ref to a special ID + this.seqId = Popcorn.guid( "__sequenced" ); + + // List of HTMLVideoElements + this.queue = []; + + // List of Popcorn objects + this.playlist = []; + + // Lists of in/out points + this.inOuts = { + + // Stores the video in/out times for each video in sequence + ofVideos: [], + + // Stores the clip in/out times for each clip in sequences + ofClips: [] + + }; + + // Store first video dimensions + this.dims = { + width: 0, //this.video.videoWidth, + height: 0 //this.video.videoHeight + }; + + this.active = 0; + this.cycling = false; + this.playing = false; + + this.times = { + last: 0 + }; + + // Store event pointers and queues + this.events = { + + }; + + var self = this, + clipOffset = 0; + + // Create `video` elements + Popcorn.forEach( list, function( media, idx ) { + + var video = doc.createElement( "video" ); + + video.preload = "auto"; + + // Setup newly created video element + video.controls = true; + + // If the first, show it, if the after, hide it + video.style.display = ( idx && "none" ) || "" ; + + // Seta registered sequence id + video.id = self.seqId + "-" + idx ; + + // Push this video into the sequence queue + self.queue.push( video ); + + var //satisfy lint + mIn = media["in"], + mOut = media["out"]; + + // Push the in/out points into sequence ioVideos + self.inOuts.ofVideos.push({ + "in": mIn !== undefined && typeof mIn === "number" ? mIn : 1, + "out": mOut !== undefined && typeof mOut === "number" ? mOut : 0 + }); + + self.inOuts.ofVideos[ idx ]["out"] = self.inOuts.ofVideos[ idx ]["out"] || self.inOuts.ofVideos[ idx ]["in"] + 2; + + // Set the sources + video.src = !rprotocol.test( media.src ) ? lochref + media.src : media.src; + + // Set some squence specific data vars + video.setAttribute("data-sequence-owner", parent ); + video.setAttribute("data-sequence-guid", self.seqId ); + video.setAttribute("data-sequence-id", idx ); + video.setAttribute("data-sequence-clip", [ self.inOuts.ofVideos[ idx ]["in"], self.inOuts.ofVideos[ idx ]["out"] ].join(":") ); + + // Append the video to the parent element + self.parent.appendChild( video ); + + + self.playlist.push( Popcorn("#" + video.id ) ); + + }); + + self.inOuts.ofVideos.forEach(function( obj ) { + + var clipDuration = obj["out"] - obj["in"], + offs = { + "in": clipOffset, + "out": clipOffset + clipDuration + }; + + self.inOuts.ofClips.push( offs ); + + clipOffset = offs["out"]; + }); + + Popcorn.forEach( this.queue, function( media, idx ) { + + function canPlayThrough( event ) { + + // If this is idx zero, use it as dimension for all + if ( !idx ) { + self.dims.width = media.videoWidth; + self.dims.height = media.videoHeight; + } + + // -0.2 prevents trackEvents from firing when they trigger on a clips in value + media.currentTime = self.inOuts.ofVideos[ idx ]["in"] - 0.2; + + media.removeEventListener( "canplaythrough", canPlayThrough, false ); + + return true; + } + + // Hook up event oners for managing special playback + media.addEventListener( "canplaythrough", canPlayThrough, false ); + + // TODO: consolidate & DRY + media.addEventListener( "play", function( event ) { + + self.playing = true; + + }, false ); + + media.addEventListener( "pause", function( event ) { + + self.playing = false; + + }, false ); + + media.addEventListener( "timeupdate", function( event ) { + + var target = event.srcElement || event.target, + seqIdx = +( (target.dataset && target.dataset.sequenceId) || target.getAttribute("data-sequence-id") ), + floor = Math.floor( media.currentTime ); + + if ( self.times.last !== floor && + seqIdx === self.active ) { + + self.times.last = floor; + + if ( floor === self.inOuts.ofVideos[ seqIdx ]["out"] ) { + + Popcorn.sequence.cycle.call( self, seqIdx ); + } + } + }, false ); + + media.addEventListener( "ended", function( event ) { + + var target = event.srcElement || event.target, + seqIdx = +( (target.dataset && target.dataset.sequenceId) || target.getAttribute("data-sequence-id") ); + + Popcorn.sequence.cycle.call( self, seqIdx ); + }, false ); + }); + + return this; + }; + + Popcorn.sequence.init.prototype = Popcorn.sequence.prototype; + + Popcorn.sequence.cycle = function( fromIdx, toIdx ) { + + if ( !this.queue ) { + Popcorn.error("Popcorn.sequence.cycle is not a public method"); + } + + // no cycle needed, bail + if ( fromIdx === toIdx ) { + return this; + } + + // Localize references + var queue = this.queue, + ioVideos = this.inOuts.ofVideos, + current = queue[ fromIdx ], + nextIdx, next, clip; + + // Popcorn instances + var $popnext, + $popprev; + + nextIdx = typeof toIdx === "number" ? toIdx : fromIdx + 1; + + // Reset queue + if ( !queue[ nextIdx ] ) { + + nextIdx = 0; + this.playlist[ fromIdx ].pause(); + + } else { + + next = queue[ nextIdx ]; + clip = ioVideos[ nextIdx ]; + + // Constrain dimentions + Popcorn.extend( next, { + width: this.dims.width, + height: this.dims.height + }); + + $popnext = this.playlist[ nextIdx ]; + $popprev = this.playlist[ fromIdx ]; + + current.pause(); + + this.active = nextIdx; + this.times.last = clip["in"] - 1; + + if ($popnext !== undefined) { + console.log("$popnext ok!") + } + if ($popnext.currentTime !== undefined) { + console.log("$popnext.currentTime ok!") + } + if (clip !== undefined) { + console.log("clip ok!") + } + if (clip["in"] !== undefined) { + console.log("clip[in] ok!") + } + + // Hide the currently ending video + current.style.display = "none"; + + // Show the next video in the sequence + next.style.display = ""; + + // Play the next video in the sequence + $popnext.currentTime( clip["in"] ); + + $popnext.play(); + + // Trigger custom cycling event hook + this.trigger( "cycle", { + + position: { + previous: fromIdx, + current: nextIdx + } + + }); + + this.cycling = false; + } + + return this; + }; + + var excludes = [ "timeupdate", "play", "pause" ]; + + // Sequence object prototype + Popcorn.extend( Popcorn.sequence.prototype, { + + // Returns Popcorn object from sequence at index + eq: function( idx ) { + return this.playlist[ idx ]; + }, + // Remove a sequence from it's playback display container + remove: function() { + this.parent.innerHTML = null; + }, + // Returns Clip object from sequence at index + clip: function( idx ) { + return this.inOuts.ofVideos[ idx ]; + }, + // Returns sum duration for all videos in sequence + duration: function() { + + var ret = 0, + seq = this.inOuts.ofClips, + idx = 0; + + for ( ; idx < seq.length; idx++ ) { + ret += seq[ idx ]["out"] - seq[ idx ]["in"]; + } + + return ret; + }, + + play: function() { + + /* + var c = Math.round( this.queue[ this.active ].currentTime ); + + if ( ( ( this.queue.length - 1 ) === this.active ) && + ( this.inOuts[ "ofVideos" ][ this.active ][ "out" ] >= Math.round( this.queue[ this.active ].currentTime ) ) ) + { + this.jumpTo( 0 ); + } else */{ + this.queue[ this.active ].play(); + } + + return this; + }, + // Pause the sequence + pause: function() { + + this.queue[ this.active ].pause(); + + return this; + + }, + // Attach an event to a single point in time + cue: function ( time, fn ) { + + var index = this.active; + + this.inOuts.ofClips.forEach(function( off, idx ) { + if ( time >= off["in"] && time <= off["out"] ) { + index = idx; + } + }); + + //offsetBy = time - self.inOuts.ofVideos[ index ].in; + + time += this.inOuts.ofVideos[ index ]["in"] - this.inOuts.ofClips[ index ]["in"]; + + // Cue up the callback on the correct popcorn instance + this.playlist[ index ].cue( time, fn ); + + return this; + }, + // Binds event handlers that fire only when all + // videos in sequence have heard the event + on: function( type, callback ) { + + var self = this, + seq = this.playlist, + total = seq.length, + count = 0, + fnName; + + if ( !callback ) { + callback = Popcorn.nop; + } + + // Handling for DOM and Media events + if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) { + Popcorn.forEach( seq, function( video ) { + + video.on( type, function( event ) { + + event.active = self; + + if ( excludes.indexOf( type ) > -1 ) { + + callback.call( video, event ); + + } else { + if ( ++count === total ) { + callback.call( video, event ); + } + } + }); + }); + } else { + + // If no events registered with this name, create a cache + if ( !this.events[ type ] ) { + this.events[ type ] = {}; + } + + // Normalize a callback name key + fnName = callback.name || Popcorn.guid( "__" + type ); + + // Store in event cache + this.events[ type ][ fnName ] = callback; + } + + // Return the sequence object + return this; + }, + off: function( type, name ) { + + var seq = this.playlist; + + if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) { + Popcorn.forEach( seq, function( video ) { + video.off( type, name ); + }); + } else { + + this.events[ type ] = null; + } + + return this; + }, + emit: function( type, data ) { + var self = this; + + // Handling for DOM and Media events + if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) { + // find the active video and trigger api events on that video. + return; + } else { + // Only proceed if there are events of this type + // currently registered on the sequence + if ( this.events[ type ] ) { + Popcorn.forEach( this.events[ type ], function( callback, name ) { + callback.call( self, { type: type }, data ); + }); + } + } + return this; + }, + currentTime: function() { + var index = this.active, + currentTime = 0; + + this.inOuts.ofClips.forEach(function( off, idx ) { + if ( idx < index ) { + currentTime += this.inOuts.ofVideos[ idx ]["out"] - this.inOuts.ofVideos[ idx ]["in"]; + } + }, this ); + currentTime += this.playlist[ index ].currentTime() - this.inOuts.ofVideos[ index ]["in"]; + return currentTime; + }, + jumpTo: function( time ) { + + if ( time < 0 || time > this.duration() ) { + return this; + } + + var index, found, real, curInOuts; + offsetTime = 0; + + found = false; + + this.inOuts.ofClips.forEach(function( off, idx ) { + var inOuts = this.inOuts; + if ( !found ) { + if ( (time >= inOuts.ofClips[ idx ]["in"] && + time <= inOuts.ofClips[ idx ]["out"]) ) { + + found = true; + index = idx; + real = ( time - offsetTime ) + inOuts.ofVideos[ idx ]["in"]; + } else { + offsetTime += inOuts.ofClips[ idx ]["out"] - offsetTime; + } + } + }, this ); + Popcorn.sequence.cycle.call( this, this.active, index ); + curInOuts = this.inOuts.ofVideos[ index ] + + // Jump to the calculated time in the clip, making sure it's in the correct range + this.playlist[ index ].currentTime( real >= curInOuts["in"] && real <= curInOuts["out"] ? real : curInOuts["in"] ); + + return this; + } + }); + + [ ["exec", "cue"], ["listen", "on"], ["unlisten", "off"], ["trigger", "emit"] ].forEach(function( remap ) { + + Popcorn.sequence.prototype[ remap[0] ] = Popcorn.sequence.prototype[ remap[1] ]; + }) + + Popcorn.forEach( Popcorn.manifest, function( obj, plugin ) { + + // Implement passthrough methods to plugins + Popcorn.sequence.prototype[ plugin ] = function( options ) { + + var videos = {}, assignTo = [], + idx, off, inOuts, inIdx, outIdx, + keys, clip, clipInOut, clipRange; + + for ( idx = 0; idx < this.inOuts.ofClips.length; idx++ ) { + // store reference + off = this.inOuts.ofClips[ idx ]; + // array to test against + inOuts = range( off["in"], off["out"] ); + + inIdx = inOuts.indexOf( options.start ); + outIdx = inOuts.indexOf( options.end ); + + if ( inIdx > -1 ) { + videos[ idx ] = Popcorn.extend( {}, off, { + start: inOuts[ inIdx ], + clipIdx: inIdx + }); + } + + if ( outIdx > -1 ) { + videos[ idx ] = Popcorn.extend( {}, off, { + end: inOuts[ outIdx ], + clipIdx: outIdx + }); + } + } + + keys = Object.keys( videos ).map(function( val ) { + return +val; + }); + + assignTo = range( keys[ 0 ], keys[ 1 ] ); + + for ( idx = 0; idx < assignTo.length; idx++ ) { + + var compile = {}, + play = assignTo[ idx ], + vClip = videos[ play ]; + + if ( vClip ) { + // has instructions + clip = this.inOuts.ofVideos[ play ]; + clipInOut = vClip.clipIdx; + clipRange = range( clip["in"], clip["out"] ); + + if ( vClip.start ) { + compile.start = clipRange[ clipInOut ]; + compile.end = clipRange[ clipRange.length - 1 ]; + } + + if ( vClip.end ) { + compile.start = clipRange[ 0 ]; + compile.end = clipRange[ clipInOut ]; + } + } else { + compile.start = this.inOuts.ofVideos[ play ]["in"]; + compile.end = this.inOuts.ofVideos[ play ]["out"]; + } + + // Call the plugin on the appropriate Popcorn object in the playlist + // Merge original options object & compiled (start/end) object into + // a new object + this.playlist[ play ][ plugin ]( + Popcorn.extend( {}, options, compile ) + ); + } + // Return the sequence object + return this; + }; + }); +})( this, Popcorn );