web/static/res/js/popcorn.sequence.js
changeset 18 f6232b308fbd
child 36 6cd5bc3dc7a2
equal deleted inserted replaced
17:ec4f33084f8d 18:f6232b308fbd
       
     1 /*!
       
     2  * Popcorn.sequence
       
     3  *
       
     4  * Copyright 2011, Rick Waldron
       
     5  * Licensed under MIT license.
       
     6  *
       
     7  */
       
     8 
       
     9 /* jslint forin: true, maxerr: 50, indent: 4, es5: true  */
       
    10 /* global Popcorn: true */
       
    11 
       
    12 // Requires Popcorn.js
       
    13 (function( global, Popcorn ) {
       
    14 
       
    15   // TODO: as support increases, migrate to element.dataset
       
    16   var doc = global.document,
       
    17       location = global.location,
       
    18       rprotocol = /:\/\//,
       
    19       // TODO: better solution to this sucky stop-gap
       
    20       lochref = location.href.replace( location.href.split("/").slice(-1)[0], "" ),
       
    21       // privately held
       
    22       range = function(start, stop, step) {
       
    23 
       
    24         start = start || 0;
       
    25         stop = ( stop || start || 0 ) + 1;
       
    26         step = step || 1;
       
    27 
       
    28         var len = Math.ceil((stop - start) / step) || 0,
       
    29             idx = 0,
       
    30             range = [];
       
    31 
       
    32         range.length = len;
       
    33 
       
    34         while (idx < len) {
       
    35          range[idx++] = start;
       
    36          start += step;
       
    37         }
       
    38         return range;
       
    39       };
       
    40 
       
    41   Popcorn.sequence = function( parent, list ) {
       
    42     return new Popcorn.sequence.init( parent, list );
       
    43   };
       
    44 
       
    45   Popcorn.sequence.init = function( parent, list ) {
       
    46 
       
    47     // Video element
       
    48     this.parent = doc.getElementById( parent );
       
    49 
       
    50     // Store ref to a special ID
       
    51     this.seqId = Popcorn.guid( "__sequenced" );
       
    52 
       
    53     // List of HTMLVideoElements
       
    54     this.queue = [];
       
    55 
       
    56     // List of Popcorn objects
       
    57     this.playlist = [];
       
    58 
       
    59     // Lists of in/out points
       
    60     this.inOuts = {
       
    61 
       
    62       // Stores the video in/out times for each video in sequence
       
    63       ofVideos: [],
       
    64 
       
    65       // Stores the clip in/out times for each clip in sequences
       
    66       ofClips: []
       
    67 
       
    68     };
       
    69 
       
    70     // Store first video dimensions
       
    71     this.dims = {
       
    72       width: 0, //this.video.videoWidth,
       
    73       height: 0 //this.video.videoHeight
       
    74     };
       
    75 
       
    76     this.active = 0;
       
    77     this.cycling = false;
       
    78     this.playing = false;
       
    79 
       
    80     this.times = {
       
    81       last: 0
       
    82     };
       
    83 
       
    84     // Store event pointers and queues
       
    85     this.events = {
       
    86 
       
    87     };
       
    88 
       
    89     var self = this,
       
    90         clipOffset = 0;
       
    91 
       
    92     // Create `video` elements
       
    93     Popcorn.forEach( list, function( media, idx ) {
       
    94 
       
    95       var video = doc.createElement( "video" );
       
    96 
       
    97       video.preload = "auto";
       
    98 
       
    99       // Setup newly created video element
       
   100       video.controls = true;
       
   101 
       
   102       // If the first, show it, if the after, hide it
       
   103       video.style.display = ( idx && "none" ) || "" ;
       
   104 
       
   105       // Seta registered sequence id
       
   106       video.id = self.seqId + "-" + idx ;
       
   107 
       
   108       // Push this video into the sequence queue
       
   109       self.queue.push( video );
       
   110 
       
   111       var //satisfy lint
       
   112        mIn = media["in"],
       
   113        mOut = media["out"];
       
   114 
       
   115       // Push the in/out points into sequence ioVideos
       
   116       self.inOuts.ofVideos.push({
       
   117         "in": mIn !== undefined && typeof mIn === "number" ?  mIn : 1,
       
   118         "out": mOut !== undefined && typeof mOut === "number" ? mOut : 0
       
   119       });
       
   120 
       
   121       self.inOuts.ofVideos[ idx ]["out"] = self.inOuts.ofVideos[ idx ]["out"] || self.inOuts.ofVideos[ idx ]["in"] + 2;
       
   122 
       
   123       // Set the sources
       
   124       video.src = !rprotocol.test( media.src ) ? lochref + media.src : media.src;
       
   125 
       
   126       // Set some squence specific data vars
       
   127       video.setAttribute("data-sequence-owner", parent );
       
   128       video.setAttribute("data-sequence-guid", self.seqId );
       
   129       video.setAttribute("data-sequence-id", idx );
       
   130       video.setAttribute("data-sequence-clip", [ self.inOuts.ofVideos[ idx ]["in"], self.inOuts.ofVideos[ idx ]["out"] ].join(":") );
       
   131 
       
   132       // Append the video to the parent element
       
   133       self.parent.appendChild( video );
       
   134 
       
   135 
       
   136       self.playlist.push( Popcorn("#" + video.id ) );
       
   137 
       
   138     });
       
   139 
       
   140     self.inOuts.ofVideos.forEach(function( obj ) {
       
   141 
       
   142       var clipDuration = obj["out"] - obj["in"],
       
   143           offs = {
       
   144             "in": clipOffset,
       
   145             "out": clipOffset + clipDuration
       
   146           };
       
   147 
       
   148       self.inOuts.ofClips.push( offs );
       
   149 
       
   150       clipOffset = offs["out"];
       
   151     });
       
   152 
       
   153     Popcorn.forEach( this.queue, function( media, idx ) {
       
   154 
       
   155       function canPlayThrough( event ) {
       
   156 
       
   157         // If this is idx zero, use it as dimension for all
       
   158         if ( !idx ) {
       
   159           self.dims.width = media.videoWidth;
       
   160           self.dims.height = media.videoHeight;
       
   161         }
       
   162 
       
   163         // -0.2 prevents trackEvents from firing when they trigger on a clips in value
       
   164         media.currentTime = self.inOuts.ofVideos[ idx ]["in"] - 0.2;
       
   165 
       
   166         media.removeEventListener( "canplaythrough", canPlayThrough, false );
       
   167 
       
   168         return true;
       
   169       }
       
   170 
       
   171       // Hook up event oners for managing special playback
       
   172       media.addEventListener( "canplaythrough", canPlayThrough, false );
       
   173 
       
   174       // TODO: consolidate & DRY
       
   175       media.addEventListener( "play", function( event ) {
       
   176 
       
   177         self.playing = true;
       
   178 
       
   179       }, false );
       
   180 
       
   181       media.addEventListener( "pause", function( event ) {
       
   182 
       
   183         self.playing = false;
       
   184 
       
   185       }, false );
       
   186 
       
   187       media.addEventListener( "timeupdate", function( event ) {
       
   188 
       
   189         var target = event.srcElement || event.target,
       
   190             seqIdx = +(  (target.dataset && target.dataset.sequenceId) || target.getAttribute("data-sequence-id") ),
       
   191             floor = Math.floor( media.currentTime );
       
   192 
       
   193         if ( self.times.last !== floor &&
       
   194               seqIdx === self.active ) {
       
   195 
       
   196           self.times.last = floor;
       
   197 
       
   198           if ( floor === self.inOuts.ofVideos[ seqIdx ]["out"] ) {
       
   199 
       
   200             Popcorn.sequence.cycle.call( self, seqIdx );
       
   201           }
       
   202         }
       
   203       }, false );
       
   204 
       
   205       media.addEventListener( "ended", function( event ) {
       
   206 
       
   207         var target = event.srcElement || event.target,
       
   208             seqIdx = +(  (target.dataset && target.dataset.sequenceId) || target.getAttribute("data-sequence-id") );
       
   209 
       
   210         Popcorn.sequence.cycle.call( self, seqIdx );
       
   211       }, false );
       
   212     });
       
   213 
       
   214     return this;
       
   215   };
       
   216 
       
   217   Popcorn.sequence.init.prototype = Popcorn.sequence.prototype;
       
   218 
       
   219   Popcorn.sequence.cycle = function( fromIdx, toIdx ) {
       
   220 
       
   221     if ( !this.queue ) {
       
   222       Popcorn.error("Popcorn.sequence.cycle is not a public method");
       
   223     }
       
   224 
       
   225     // no cycle needed, bail
       
   226     if ( fromIdx === toIdx ) {
       
   227       return this;
       
   228     }
       
   229 
       
   230     // Localize references
       
   231     var queue = this.queue,
       
   232         ioVideos = this.inOuts.ofVideos,
       
   233         current = queue[ fromIdx ],
       
   234         nextIdx, next, clip;
       
   235 
       
   236     // Popcorn instances
       
   237     var $popnext,
       
   238         $popprev;
       
   239 
       
   240     nextIdx = typeof toIdx === "number" ? toIdx : fromIdx + 1;
       
   241 
       
   242     // Reset queue
       
   243     if ( !queue[ nextIdx ] ) {
       
   244 
       
   245       nextIdx = 0;
       
   246       this.playlist[ fromIdx ].pause();
       
   247 
       
   248     } else {
       
   249 
       
   250       next = queue[ nextIdx ];
       
   251       clip = ioVideos[ nextIdx ];
       
   252 
       
   253       // Constrain dimentions
       
   254       Popcorn.extend( next, {
       
   255         width: this.dims.width,
       
   256         height: this.dims.height
       
   257       });
       
   258 
       
   259       $popnext = this.playlist[ nextIdx ];
       
   260       $popprev = this.playlist[ fromIdx ];
       
   261 
       
   262       current.pause();
       
   263 
       
   264       this.active = nextIdx;
       
   265       this.times.last = clip["in"] - 1;
       
   266 
       
   267       if ($popnext !== undefined) {
       
   268         console.log("$popnext ok!")
       
   269       }
       
   270       if ($popnext.currentTime !== undefined) {
       
   271         console.log("$popnext.currentTime ok!")
       
   272       }
       
   273       if (clip !== undefined) {
       
   274         console.log("clip ok!")
       
   275       }
       
   276       if (clip["in"] !== undefined) {
       
   277         console.log("clip[in] ok!")
       
   278       }
       
   279 
       
   280       // Hide the currently ending video
       
   281       current.style.display = "none";
       
   282 
       
   283       // Show the next video in the sequence
       
   284       next.style.display = "";
       
   285 
       
   286       // Play the next video in the sequence
       
   287       $popnext.currentTime( clip["in"] );
       
   288 
       
   289       $popnext.play();
       
   290 
       
   291       // Trigger custom cycling event hook
       
   292       this.trigger( "cycle", {
       
   293 
       
   294         position: {
       
   295           previous: fromIdx,
       
   296           current: nextIdx
       
   297         }
       
   298 
       
   299       });
       
   300 
       
   301       this.cycling = false;
       
   302     }
       
   303 
       
   304     return this;
       
   305   };
       
   306 
       
   307   var excludes = [ "timeupdate", "play", "pause" ];
       
   308 
       
   309   // Sequence object prototype
       
   310   Popcorn.extend( Popcorn.sequence.prototype, {
       
   311 
       
   312     // Returns Popcorn object from sequence at index
       
   313     eq: function( idx ) {
       
   314       return this.playlist[ idx ];
       
   315     },
       
   316     // Remove a sequence from it's playback display container
       
   317     remove: function() {
       
   318       this.parent.innerHTML = null;
       
   319     },
       
   320     // Returns Clip object from sequence at index
       
   321     clip: function( idx ) {
       
   322       return this.inOuts.ofVideos[ idx ];
       
   323     },
       
   324     // Returns sum duration for all videos in sequence
       
   325     duration: function() {
       
   326 
       
   327       var ret = 0,
       
   328           seq = this.inOuts.ofClips,
       
   329           idx = 0;
       
   330 
       
   331       for ( ; idx < seq.length; idx++ ) {
       
   332         ret += seq[ idx ]["out"] - seq[ idx ]["in"];
       
   333       }
       
   334 
       
   335       return ret;
       
   336     },
       
   337 
       
   338     play: function() {
       
   339 
       
   340       /*
       
   341       var c = Math.round( this.queue[ this.active ].currentTime );
       
   342 
       
   343       if ( ( ( this.queue.length - 1 ) === this.active ) &&
       
   344            ( this.inOuts[ "ofVideos" ][ this.active ][ "out" ] >= Math.round( this.queue[ this.active ].currentTime ) ) )
       
   345       {
       
   346         this.jumpTo( 0 );
       
   347       } else */{
       
   348         this.queue[ this.active ].play();
       
   349       }
       
   350 
       
   351       return this;
       
   352     },
       
   353     // Pause the sequence
       
   354     pause: function() {
       
   355 
       
   356       this.queue[ this.active ].pause();
       
   357 
       
   358       return this;
       
   359 
       
   360     },
       
   361     // Attach an event to a single point in time
       
   362     cue: function ( time, fn ) {
       
   363 
       
   364       var index = this.active;
       
   365 
       
   366       this.inOuts.ofClips.forEach(function( off, idx ) {
       
   367         if ( time >= off["in"] && time <= off["out"] ) {
       
   368           index = idx;
       
   369         }
       
   370       });
       
   371 
       
   372       //offsetBy = time - self.inOuts.ofVideos[ index ].in;
       
   373 
       
   374       time += this.inOuts.ofVideos[ index ]["in"] - this.inOuts.ofClips[ index ]["in"];
       
   375 
       
   376       // Cue up the callback on the correct popcorn instance
       
   377       this.playlist[ index ].cue( time, fn );
       
   378 
       
   379       return this;
       
   380     },
       
   381     // Binds event handlers that fire only when all
       
   382     // videos in sequence have heard the event
       
   383     on: function( type, callback ) {
       
   384 
       
   385       var self = this,
       
   386           seq = this.playlist,
       
   387           total = seq.length,
       
   388           count = 0,
       
   389           fnName;
       
   390 
       
   391       if ( !callback ) {
       
   392         callback = Popcorn.nop;
       
   393       }
       
   394 
       
   395       // Handling for DOM and Media events
       
   396       if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) {
       
   397         Popcorn.forEach( seq, function( video ) {
       
   398 
       
   399           video.on( type, function( event ) {
       
   400 
       
   401             event.active = self;
       
   402 
       
   403             if ( excludes.indexOf( type ) > -1 ) {
       
   404 
       
   405               callback.call( video, event );
       
   406 
       
   407             } else {
       
   408               if ( ++count === total ) {
       
   409                 callback.call( video, event );
       
   410               }
       
   411             }
       
   412           });
       
   413         });
       
   414       } else {
       
   415 
       
   416         // If no events registered with this name, create a cache
       
   417         if ( !this.events[ type ] ) {
       
   418           this.events[ type ] = {};
       
   419         }
       
   420 
       
   421         // Normalize a callback name key
       
   422         fnName = callback.name || Popcorn.guid( "__" + type );
       
   423 
       
   424         // Store in event cache
       
   425         this.events[ type ][ fnName ] = callback;
       
   426       }
       
   427 
       
   428       // Return the sequence object
       
   429       return this;
       
   430     },
       
   431     off: function( type, name ) {
       
   432 
       
   433       var seq = this.playlist;
       
   434 
       
   435       if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) {
       
   436         Popcorn.forEach( seq, function( video ) {
       
   437           video.off( type, name );
       
   438         });
       
   439       } else {
       
   440 
       
   441         this.events[ type ] = null;
       
   442       }
       
   443 
       
   444       return this;
       
   445     },
       
   446     emit: function( type, data ) {
       
   447       var self = this;
       
   448 
       
   449       // Handling for DOM and Media events
       
   450       if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) {
       
   451         //  find the active video and trigger api events on that video.
       
   452         return;
       
   453       } else {
       
   454         // Only proceed if there are events of this type
       
   455         // currently registered on the sequence
       
   456         if ( this.events[ type ] ) {
       
   457           Popcorn.forEach( this.events[ type ], function( callback, name ) {
       
   458             callback.call( self, { type: type }, data );
       
   459           });
       
   460         }
       
   461       }
       
   462       return this;
       
   463     },
       
   464     currentTime: function() {
       
   465       var index = this.active,
       
   466           currentTime = 0;
       
   467 
       
   468       this.inOuts.ofClips.forEach(function( off, idx ) {
       
   469         if ( idx < index ) {
       
   470           currentTime += this.inOuts.ofVideos[ idx ]["out"] - this.inOuts.ofVideos[ idx ]["in"];
       
   471         }
       
   472       }, this );
       
   473       currentTime += this.playlist[ index ].currentTime() - this.inOuts.ofVideos[ index ]["in"];
       
   474       return currentTime;
       
   475     },
       
   476     jumpTo: function( time ) {
       
   477 
       
   478       if ( time < 0 || time > this.duration() ) {
       
   479         return this;
       
   480       }
       
   481 
       
   482       var index, found, real, curInOuts;
       
   483           offsetTime = 0;
       
   484 
       
   485       found = false;
       
   486 
       
   487       this.inOuts.ofClips.forEach(function( off, idx ) {
       
   488         var inOuts = this.inOuts;
       
   489         if ( !found ) {
       
   490           if ( (time >= inOuts.ofClips[ idx ]["in"] &&
       
   491                 time <= inOuts.ofClips[ idx ]["out"]) ) {
       
   492 
       
   493             found = true;
       
   494             index = idx;
       
   495             real = ( time - offsetTime ) + inOuts.ofVideos[ idx ]["in"];
       
   496           } else {
       
   497             offsetTime += inOuts.ofClips[ idx ]["out"] - offsetTime;
       
   498           }
       
   499         }
       
   500       }, this );
       
   501       Popcorn.sequence.cycle.call( this, this.active, index );
       
   502       curInOuts = this.inOuts.ofVideos[ index ]
       
   503 
       
   504       // Jump to the calculated time in the clip, making sure it's in the correct range
       
   505       this.playlist[ index ].currentTime( real >= curInOuts["in"] && real <= curInOuts["out"] ? real : curInOuts["in"] );
       
   506 
       
   507       return this;
       
   508     }
       
   509   });
       
   510 
       
   511   [ ["exec", "cue"], ["listen", "on"], ["unlisten", "off"], ["trigger", "emit"] ].forEach(function( remap ) {
       
   512 
       
   513     Popcorn.sequence.prototype[ remap[0] ] = Popcorn.sequence.prototype[ remap[1] ];
       
   514   })
       
   515 
       
   516   Popcorn.forEach( Popcorn.manifest, function( obj, plugin ) {
       
   517 
       
   518     // Implement passthrough methods to plugins
       
   519     Popcorn.sequence.prototype[ plugin ] = function( options ) {
       
   520 
       
   521       var videos = {}, assignTo = [],
       
   522           idx, off, inOuts, inIdx, outIdx,
       
   523           keys, clip, clipInOut, clipRange;
       
   524 
       
   525       for ( idx = 0; idx < this.inOuts.ofClips.length; idx++  ) {
       
   526         // store reference
       
   527         off = this.inOuts.ofClips[ idx ];
       
   528         // array to test against
       
   529         inOuts = range( off["in"], off["out"] );
       
   530 
       
   531         inIdx = inOuts.indexOf( options.start );
       
   532         outIdx = inOuts.indexOf( options.end );
       
   533 
       
   534         if ( inIdx > -1 ) {
       
   535           videos[ idx ] = Popcorn.extend( {}, off, {
       
   536             start: inOuts[ inIdx ],
       
   537             clipIdx: inIdx
       
   538           });
       
   539         }
       
   540 
       
   541         if ( outIdx > -1 ) {
       
   542           videos[ idx ] = Popcorn.extend( {}, off, {
       
   543             end: inOuts[ outIdx ],
       
   544             clipIdx: outIdx
       
   545           });
       
   546         }
       
   547       }
       
   548 
       
   549       keys = Object.keys( videos ).map(function( val ) {
       
   550                 return +val;
       
   551               });
       
   552 
       
   553       assignTo = range( keys[ 0 ], keys[ 1 ] );
       
   554 
       
   555       for ( idx = 0; idx < assignTo.length; idx++ ) {
       
   556 
       
   557         var compile = {},
       
   558         play = assignTo[ idx ],
       
   559         vClip = videos[ play ];
       
   560 
       
   561         if ( vClip ) {
       
   562           // has instructions
       
   563           clip = this.inOuts.ofVideos[ play ];
       
   564           clipInOut = vClip.clipIdx;
       
   565           clipRange = range( clip["in"], clip["out"] );
       
   566 
       
   567           if ( vClip.start ) {
       
   568             compile.start = clipRange[ clipInOut ];
       
   569             compile.end = clipRange[ clipRange.length - 1 ];
       
   570           }
       
   571 
       
   572           if ( vClip.end ) {
       
   573             compile.start = clipRange[ 0 ];
       
   574             compile.end = clipRange[ clipInOut ];
       
   575           }
       
   576         } else {
       
   577           compile.start = this.inOuts.ofVideos[ play ]["in"];
       
   578           compile.end = this.inOuts.ofVideos[ play ]["out"];
       
   579         }
       
   580 
       
   581         // Call the plugin on the appropriate Popcorn object in the playlist
       
   582         // Merge original options object & compiled (start/end) object into
       
   583         // a new object
       
   584         this.playlist[ play ][ plugin ](
       
   585           Popcorn.extend( {}, options, compile )
       
   586         );
       
   587       }
       
   588       // Return the sequence object
       
   589       return this;
       
   590     };
       
   591   });
       
   592 })( this, Popcorn );