web/static/res/js/popcorn.sequence.js
author Edwin Razafimahatratra <edwin@robotalismsoft.com>
Mon, 10 Dec 2012 20:55:18 +0100
changeset 38 cdbb56b876c9
parent 36 6cd5bc3dc7a2
child 78 8c3f0b94d056
permissions -rw-r--r--
mosaic effect loops

/*!
 * 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;
    },

    // Returns sum duration for n first videos in sequences
    durationSeqs: function(n) {

      var ret = 0,
          seq = this.inOuts.ofClips,
          idx = 0;

      for ( ; idx < n; 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 );