src/js/popcorn.js
branchpopcorn-port
changeset 51 1444edeae73f
child 77 b3140ffed377
equal deleted inserted replaced
50:b5849024c5c5 51:1444edeae73f
       
     1 (function(global, document) {
       
     2 
       
     3   // Popcorn.js does not support archaic browsers
       
     4   if ( !document.addEventListener ) {
       
     5     global.Popcorn = {
       
     6       isSupported: false
       
     7     };
       
     8 
       
     9     var methods = ( "forEach extend effects error guid sizeOf isArray nop position disable enable destroy " +
       
    10           "addTrackEvent removeTrackEvent getTrackEvents getTrackEvent getLastTrackEventId " +
       
    11           "timeUpdate plugin removePlugin compose effect parser xhr getJSONP getScript" ).split(/\s+/);
       
    12 
       
    13     while( methods.length ) {
       
    14       global.Popcorn[ methods.shift() ] = function() {};
       
    15     }
       
    16     return;
       
    17   }
       
    18 
       
    19   var
       
    20 
       
    21   AP = Array.prototype,
       
    22   OP = Object.prototype,
       
    23 
       
    24   forEach = AP.forEach,
       
    25   slice = AP.slice,
       
    26   hasOwn = OP.hasOwnProperty,
       
    27   toString = OP.toString,
       
    28 
       
    29   // Copy global Popcorn (may not exist)
       
    30   _Popcorn = global.Popcorn,
       
    31 
       
    32   //  ID string matching
       
    33   rIdExp  = /^(#([\w\-\_\.]+))$/,
       
    34 
       
    35   //  Ready fn cache
       
    36   readyStack = [],
       
    37   readyBound = false,
       
    38   readyFired = false,
       
    39 
       
    40   //  Non-public internal data object
       
    41   internal = {
       
    42     events: {
       
    43       hash: {},
       
    44       apis: {}
       
    45     }
       
    46   },
       
    47 
       
    48   //  Non-public `requestAnimFrame`
       
    49   //  http://paulirish.com/2011/requestanimationframe-for-smart-animating/
       
    50   requestAnimFrame = (function(){
       
    51     return global.requestAnimationFrame ||
       
    52       global.webkitRequestAnimationFrame ||
       
    53       global.mozRequestAnimationFrame ||
       
    54       global.oRequestAnimationFrame ||
       
    55       global.msRequestAnimationFrame ||
       
    56       function( callback, element ) {
       
    57         global.setTimeout( callback, 16 );
       
    58       };
       
    59   }()),
       
    60 
       
    61   //  Declare constructor
       
    62   //  Returns an instance object.
       
    63   Popcorn = function( entity, options ) {
       
    64     //  Return new Popcorn object
       
    65     return new Popcorn.p.init( entity, options || null );
       
    66   };
       
    67 
       
    68   //  Popcorn API version, automatically inserted via build system.
       
    69   Popcorn.version = "@VERSION";
       
    70 
       
    71   //  Boolean flag allowing a client to determine if Popcorn can be supported
       
    72   Popcorn.isSupported = true;
       
    73 
       
    74   //  Instance caching
       
    75   Popcorn.instances = [];
       
    76 
       
    77   //  Declare a shortcut (Popcorn.p) to and a definition of
       
    78   //  the new prototype for our Popcorn constructor
       
    79   Popcorn.p = Popcorn.prototype = {
       
    80 
       
    81     init: function( entity, options ) {
       
    82 
       
    83       var matches;
       
    84 
       
    85       //  Supports Popcorn(function () { /../ })
       
    86       //  Originally proposed by Daniel Brooks
       
    87 
       
    88       if ( typeof entity === "function" ) {
       
    89 
       
    90         //  If document ready has already fired
       
    91         if ( document.readyState === "interactive" || document.readyState === "complete" ) {
       
    92 
       
    93           entity( document, Popcorn );
       
    94 
       
    95           return;
       
    96         }
       
    97         //  Add `entity` fn to ready stack
       
    98         readyStack.push( entity );
       
    99 
       
   100         //  This process should happen once per page load
       
   101         if ( !readyBound ) {
       
   102 
       
   103           //  set readyBound flag
       
   104           readyBound = true;
       
   105 
       
   106           var DOMContentLoaded  = function() {
       
   107 
       
   108             readyFired = true;
       
   109 
       
   110             //  Remove global DOM ready listener
       
   111             document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
       
   112 
       
   113             //  Execute all ready function in the stack
       
   114             for ( var i = 0, readyStackLength = readyStack.length; i < readyStackLength; i++ ) {
       
   115 
       
   116               readyStack[ i ].call( document, Popcorn );
       
   117 
       
   118             }
       
   119             //  GC readyStack
       
   120             readyStack = null;
       
   121           };
       
   122 
       
   123           //  Register global DOM ready listener
       
   124           document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
       
   125         }
       
   126 
       
   127         return;
       
   128       }
       
   129 
       
   130       //  Check if entity is a valid string id
       
   131       matches = rIdExp.exec( entity );
       
   132 
       
   133       //  Get media element by id or object reference
       
   134       this.media = matches && matches.length && matches[ 2 ] ?
       
   135                      document.getElementById( matches[ 2 ] ) :
       
   136                      entity;
       
   137 
       
   138       //  Create an audio or video element property reference
       
   139       this[ ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video" ] = this.media;
       
   140 
       
   141       //  Register new instance
       
   142       Popcorn.instances.push( this );
       
   143 
       
   144       this.options = options || {};
       
   145 
       
   146       this.isDestroyed = false;
       
   147 
       
   148       this.data = {
       
   149 
       
   150         // Allows disabling a plugin per instance
       
   151         disabled: [],
       
   152 
       
   153         // Stores DOM event queues by type
       
   154         events: {},
       
   155 
       
   156         // Stores Special event hooks data
       
   157         hooks: {},
       
   158 
       
   159         // Store track event history data
       
   160         history: [],
       
   161 
       
   162         // Stores ad-hoc state related data]
       
   163         state: {
       
   164           volume: this.media.volume
       
   165         },
       
   166 
       
   167         // Store track event object references by trackId
       
   168         trackRefs: {},
       
   169 
       
   170         // Playback track event queues
       
   171         trackEvents: {
       
   172           byStart: [{
       
   173 
       
   174             start: -1,
       
   175             end: -1
       
   176           }],
       
   177           byEnd: [{
       
   178             start: -1,
       
   179             end: -1
       
   180           }],
       
   181           animating: [],
       
   182           startIndex: 0,
       
   183           endIndex: 0,
       
   184           previousUpdateTime: -1
       
   185         }
       
   186       };
       
   187 
       
   188       //  Wrap true ready check
       
   189       var isReady = function( that ) {
       
   190 
       
   191         var duration, videoDurationPlus, animate;
       
   192 
       
   193         if ( that.media.readyState >= 2 ) {
       
   194           //  Adding padding to the front and end of the arrays
       
   195           //  this is so we do not fall off either end
       
   196 
       
   197           duration = that.media.duration;
       
   198           //  Check for no duration info (NaN)
       
   199           videoDurationPlus = duration != duration ? Number.MAX_VALUE : duration + 1;
       
   200 
       
   201           Popcorn.addTrackEvent( that, {
       
   202             start: videoDurationPlus,
       
   203             end: videoDurationPlus
       
   204           });
       
   205 
       
   206           if ( that.options.frameAnimation ) {
       
   207             //  if Popcorn is created with frameAnimation option set to true,
       
   208             //  requestAnimFrame is used instead of "timeupdate" media event.
       
   209             //  This is for greater frame time accuracy, theoretically up to
       
   210             //  60 frames per second as opposed to ~4 ( ~every 15-250ms)
       
   211             animate = function () {
       
   212 
       
   213               Popcorn.timeUpdate( that, {} );
       
   214 
       
   215               that.trigger( "timeupdate" );
       
   216 
       
   217               requestAnimFrame( animate );
       
   218             };
       
   219 
       
   220             requestAnimFrame( animate );
       
   221 
       
   222           } else {
       
   223 
       
   224             that.data.timeUpdateFunction = function( event ) {
       
   225               Popcorn.timeUpdate( that, event );
       
   226             };
       
   227 
       
   228             if ( !that.isDestroyed ) {
       
   229               that.media.addEventListener( "timeupdate", that.data.timeUpdateFunction, false );
       
   230             }
       
   231           }
       
   232         } else {
       
   233           global.setTimeout(function() {
       
   234             isReady( that );
       
   235           }, 1 );
       
   236         }
       
   237       };
       
   238 
       
   239       isReady( this );
       
   240 
       
   241       return this;
       
   242     }
       
   243   };
       
   244 
       
   245   //  Extend constructor prototype to instance prototype
       
   246   //  Allows chaining methods to instances
       
   247   Popcorn.p.init.prototype = Popcorn.p;
       
   248 
       
   249   Popcorn.forEach = function( obj, fn, context ) {
       
   250 
       
   251     if ( !obj || !fn ) {
       
   252       return {};
       
   253     }
       
   254 
       
   255     context = context || this;
       
   256 
       
   257     var key, len;
       
   258 
       
   259     // Use native whenever possible
       
   260     if ( forEach && obj.forEach === forEach ) {
       
   261       return obj.forEach( fn, context );
       
   262     }
       
   263 
       
   264     if ( toString.call( obj ) === "[object NodeList]" ) {
       
   265       for ( key = 0, len = obj.length; key < len; key++ ) {
       
   266         fn.call( context, obj[ key ], key, obj );
       
   267       }
       
   268       return obj;
       
   269     }
       
   270 
       
   271     for ( key in obj ) {
       
   272       if ( hasOwn.call( obj, key ) ) {
       
   273         fn.call( context, obj[ key ], key, obj );
       
   274       }
       
   275     }
       
   276     return obj;
       
   277   };
       
   278 
       
   279   Popcorn.extend = function( obj ) {
       
   280     var dest = obj, src = slice.call( arguments, 1 );
       
   281 
       
   282     Popcorn.forEach( src, function( copy ) {
       
   283       for ( var prop in copy ) {
       
   284         dest[ prop ] = copy[ prop ];
       
   285       }
       
   286     });
       
   287 
       
   288     return dest;
       
   289   };
       
   290 
       
   291 
       
   292   // A Few reusable utils, memoized onto Popcorn
       
   293   Popcorn.extend( Popcorn, {
       
   294     noConflict: function( deep ) {
       
   295 
       
   296       if ( deep ) {
       
   297         global.Popcorn = _Popcorn;
       
   298       }
       
   299 
       
   300       return Popcorn;
       
   301     },
       
   302     error: function( msg ) {
       
   303       throw new Error( msg );
       
   304     },
       
   305     guid: function( prefix ) {
       
   306       Popcorn.guid.counter++;
       
   307       return  ( prefix ? prefix : "" ) + ( +new Date() + Popcorn.guid.counter );
       
   308     },
       
   309     sizeOf: function( obj ) {
       
   310       var size = 0;
       
   311 
       
   312       for ( var prop in obj ) {
       
   313         size++;
       
   314       }
       
   315 
       
   316       return size;
       
   317     },
       
   318     isArray: Array.isArray || function( array ) {
       
   319       return toString.call( array ) === "[object Array]";
       
   320     },
       
   321 
       
   322     nop: function() {},
       
   323 
       
   324     position: function( elem ) {
       
   325 
       
   326       var clientRect = elem.getBoundingClientRect(),
       
   327           bounds = {},
       
   328           doc = elem.ownerDocument,
       
   329           docElem = document.documentElement,
       
   330           body = document.body,
       
   331           clientTop, clientLeft, scrollTop, scrollLeft, top, left;
       
   332 
       
   333       //  Determine correct clientTop/Left
       
   334       clientTop = docElem.clientTop || body.clientTop || 0;
       
   335       clientLeft = docElem.clientLeft || body.clientLeft || 0;
       
   336 
       
   337       //  Determine correct scrollTop/Left
       
   338       scrollTop = ( global.pageYOffset && docElem.scrollTop || body.scrollTop );
       
   339       scrollLeft = ( global.pageXOffset && docElem.scrollLeft || body.scrollLeft );
       
   340 
       
   341       //  Temp top/left
       
   342       top = Math.ceil( clientRect.top + scrollTop - clientTop );
       
   343       left = Math.ceil( clientRect.left + scrollLeft - clientLeft );
       
   344 
       
   345       for ( var p in clientRect ) {
       
   346         bounds[ p ] = Math.round( clientRect[ p ] );
       
   347       }
       
   348 
       
   349       return Popcorn.extend({}, bounds, { top: top, left: left });
       
   350     },
       
   351 
       
   352     disable: function( instance, plugin ) {
       
   353 
       
   354       var disabled = instance.data.disabled;
       
   355 
       
   356       if ( disabled.indexOf( plugin ) === -1 ) {
       
   357         disabled.push( plugin );
       
   358       }
       
   359 
       
   360       return instance;
       
   361     },
       
   362     enable: function( instance, plugin ) {
       
   363 
       
   364       var disabled = instance.data.disabled,
       
   365           index = disabled.indexOf( plugin );
       
   366 
       
   367       if ( index > -1 ) {
       
   368         disabled.splice( index, 1 );
       
   369       }
       
   370 
       
   371       return instance;
       
   372     },
       
   373     destroy: function( instance ) {
       
   374       var events = instance.data.events,
       
   375           singleEvent, item, fn;
       
   376 
       
   377       //  Iterate through all events and remove them
       
   378       for ( item in events ) {
       
   379         singleEvent = events[ item ];
       
   380         for ( fn in singleEvent ) {
       
   381           delete singleEvent[ fn ];
       
   382         }
       
   383         events[ item ] = null;
       
   384       }
       
   385 
       
   386       if ( !instance.isDestroyed ) {
       
   387         instance.media.removeEventListener( "timeupdate", instance.data.timeUpdateFunction, false );
       
   388         instance.isDestroyed = true;
       
   389       }
       
   390     }
       
   391   });
       
   392 
       
   393   //  Memoized GUID Counter
       
   394   Popcorn.guid.counter = 1;
       
   395 
       
   396   //  Factory to implement getters, setters and controllers
       
   397   //  as Popcorn instance methods. The IIFE will create and return
       
   398   //  an object with defined methods
       
   399   Popcorn.extend(Popcorn.p, (function() {
       
   400 
       
   401       var methods = "load play pause currentTime playbackRate volume duration preload playbackRate " +
       
   402                     "autoplay loop controls muted buffered readyState seeking paused played seekable ended",
       
   403           ret = {};
       
   404 
       
   405 
       
   406       //  Build methods, store in object that is returned and passed to extend
       
   407       Popcorn.forEach( methods.split( /\s+/g ), function( name ) {
       
   408 
       
   409         ret[ name ] = function( arg ) {
       
   410 
       
   411           if ( typeof this.media[ name ] === "function" ) {
       
   412 
       
   413             // Support for shorthanded play(n)/pause(n) jump to currentTime
       
   414             // If arg is not null or undefined and called by one of the
       
   415             // allowed shorthandable methods, then set the currentTime
       
   416             // Supports time as seconds or SMPTE
       
   417             if ( arg != null && /play|pause/.test( name ) ) {
       
   418               this.media.currentTime = Popcorn.util.toSeconds( arg );
       
   419             }
       
   420 
       
   421             this.media[ name ]();
       
   422 
       
   423             return this;
       
   424           }
       
   425 
       
   426 
       
   427           if ( arg != null ) {
       
   428 
       
   429             this.media[ name ] = arg;
       
   430 
       
   431             return this;
       
   432           }
       
   433 
       
   434           return this.media[ name ];
       
   435         };
       
   436       });
       
   437 
       
   438       return ret;
       
   439 
       
   440     })()
       
   441   );
       
   442 
       
   443   Popcorn.forEach( "enable disable".split(" "), function( method ) {
       
   444     Popcorn.p[ method ] = function( plugin ) {
       
   445       return Popcorn[ method ]( this, plugin );
       
   446     };
       
   447   });
       
   448 
       
   449   Popcorn.extend(Popcorn.p, {
       
   450 
       
   451     //  Rounded currentTime
       
   452     roundTime: function() {
       
   453       return -~this.media.currentTime;
       
   454     },
       
   455 
       
   456     //  Attach an event to a single point in time
       
   457     exec: function( time, fn ) {
       
   458 
       
   459       //  Creating a one second track event with an empty end
       
   460       Popcorn.addTrackEvent( this, {
       
   461         start: time,
       
   462         end: time + 1,
       
   463         _running: false,
       
   464         _natives: {
       
   465           start: fn || Popcorn.nop,
       
   466           end: Popcorn.nop,
       
   467           type: "exec"
       
   468         }
       
   469       });
       
   470 
       
   471       return this;
       
   472     },
       
   473 
       
   474     // Mute the calling media, optionally toggle
       
   475     mute: function( toggle ) {
       
   476 
       
   477       var event = toggle == null || toggle === true ? "muted" : "unmuted";
       
   478 
       
   479       // If `toggle` is explicitly `false`,
       
   480       // unmute the media and restore the volume level
       
   481       if ( event === "unmuted" ) {
       
   482         this.media.muted = false;
       
   483         this.media.volume = this.data.state.volume;
       
   484       }
       
   485 
       
   486       // If `toggle` is either null or undefined,
       
   487       // save the current volume and mute the media element
       
   488       if ( event === "muted" ) {
       
   489         this.data.state.volume = this.media.volume;
       
   490         this.media.muted = true;
       
   491       }
       
   492 
       
   493       // Trigger either muted|unmuted event
       
   494       this.trigger( event );
       
   495 
       
   496       return this;
       
   497     },
       
   498 
       
   499     // Convenience method, unmute the calling media
       
   500     unmute: function( toggle ) {
       
   501 
       
   502       return this.mute( toggle == null ? false : !toggle );
       
   503     },
       
   504 
       
   505     // Get the client bounding box of an instance element
       
   506     position: function() {
       
   507       return Popcorn.position( this.media );
       
   508     },
       
   509 
       
   510     // Toggle a plugin's playback behaviour (on or off) per instance
       
   511     toggle: function( plugin ) {
       
   512       return Popcorn[ this.data.disabled.indexOf( plugin ) > -1 ? "enable" : "disable" ]( this, plugin );
       
   513     },
       
   514 
       
   515     // Set default values for plugin options objects per instance
       
   516     defaults: function( plugin, defaults ) {
       
   517 
       
   518       // If an array of default configurations is provided,
       
   519       // iterate and apply each to this instance
       
   520       if ( Popcorn.isArray( plugin ) ) {
       
   521 
       
   522         Popcorn.forEach( plugin, function( obj ) {
       
   523           for ( var name in obj ) {
       
   524             this.defaults( name, obj[ name ] );
       
   525           }
       
   526         }, this );
       
   527 
       
   528         return this;
       
   529       }
       
   530 
       
   531       if ( !this.options.defaults ) {
       
   532         this.options.defaults = {};
       
   533       }
       
   534 
       
   535       if ( !this.options.defaults[ plugin ] ) {
       
   536         this.options.defaults[ plugin ] = {};
       
   537       }
       
   538 
       
   539       Popcorn.extend( this.options.defaults[ plugin ], defaults );
       
   540 
       
   541       return this;
       
   542     }
       
   543   });
       
   544 
       
   545   Popcorn.Events  = {
       
   546     UIEvents: "blur focus focusin focusout load resize scroll unload",
       
   547     MouseEvents: "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave click dblclick",
       
   548     Events: "loadstart progress suspend emptied stalled play pause " +
       
   549             "loadedmetadata loadeddata waiting playing canplay canplaythrough " +
       
   550             "seeking seeked timeupdate ended ratechange durationchange volumechange"
       
   551   };
       
   552 
       
   553   Popcorn.Events.Natives = Popcorn.Events.UIEvents + " " +
       
   554                            Popcorn.Events.MouseEvents + " " +
       
   555                            Popcorn.Events.Events;
       
   556 
       
   557   internal.events.apiTypes = [ "UIEvents", "MouseEvents", "Events" ];
       
   558 
       
   559   // Privately compile events table at load time
       
   560   (function( events, data ) {
       
   561 
       
   562     var apis = internal.events.apiTypes,
       
   563     eventsList = events.Natives.split( /\s+/g ),
       
   564     idx = 0, len = eventsList.length, prop;
       
   565 
       
   566     for( ; idx < len; idx++ ) {
       
   567       data.hash[ eventsList[idx] ] = true;
       
   568     }
       
   569 
       
   570     apis.forEach(function( val, idx ) {
       
   571 
       
   572       data.apis[ val ] = {};
       
   573 
       
   574       var apiEvents = events[ val ].split( /\s+/g ),
       
   575       len = apiEvents.length,
       
   576       k = 0;
       
   577 
       
   578       for ( ; k < len; k++ ) {
       
   579         data.apis[ val ][ apiEvents[ k ] ] = true;
       
   580       }
       
   581     });
       
   582   })( Popcorn.Events, internal.events );
       
   583 
       
   584   Popcorn.events = {
       
   585 
       
   586     isNative: function( type ) {
       
   587       return !!internal.events.hash[ type ];
       
   588     },
       
   589     getInterface: function( type ) {
       
   590 
       
   591       if ( !Popcorn.events.isNative( type ) ) {
       
   592         return false;
       
   593       }
       
   594 
       
   595       var eventApi = internal.events,
       
   596         apis = eventApi.apiTypes,
       
   597         apihash = eventApi.apis,
       
   598         idx = 0, len = apis.length, api, tmp;
       
   599 
       
   600       for ( ; idx < len; idx++ ) {
       
   601         tmp = apis[ idx ];
       
   602 
       
   603         if ( apihash[ tmp ][ type ] ) {
       
   604           api = tmp;
       
   605           break;
       
   606         }
       
   607       }
       
   608       return api;
       
   609     },
       
   610     //  Compile all native events to single array
       
   611     all: Popcorn.Events.Natives.split( /\s+/g ),
       
   612     //  Defines all Event handling static functions
       
   613     fn: {
       
   614       trigger: function( type, data ) {
       
   615 
       
   616         var eventInterface, evt;
       
   617         //  setup checks for custom event system
       
   618         if ( this.data.events[ type ] && Popcorn.sizeOf( this.data.events[ type ] ) ) {
       
   619 
       
   620           eventInterface  = Popcorn.events.getInterface( type );
       
   621 
       
   622           if ( eventInterface ) {
       
   623 
       
   624             evt = document.createEvent( eventInterface );
       
   625             evt.initEvent( type, true, true, global, 1 );
       
   626 
       
   627             this.media.dispatchEvent( evt );
       
   628 
       
   629             return this;
       
   630           }
       
   631 
       
   632           //  Custom events
       
   633           Popcorn.forEach( this.data.events[ type ], function( obj, key ) {
       
   634 
       
   635             obj.call( this, data );
       
   636 
       
   637           }, this );
       
   638 
       
   639         }
       
   640 
       
   641         return this;
       
   642       },
       
   643       listen: function( type, fn ) {
       
   644 
       
   645         var self = this,
       
   646             hasEvents = true,
       
   647             eventHook = Popcorn.events.hooks[ type ],
       
   648             origType = type,
       
   649             tmp;
       
   650 
       
   651         if ( !this.data.events[ type ] ) {
       
   652           this.data.events[ type ] = {};
       
   653           hasEvents = false;
       
   654         }
       
   655 
       
   656         // Check and setup event hooks
       
   657         if ( eventHook ) {
       
   658 
       
   659           // Execute hook add method if defined
       
   660           if ( eventHook.add ) {
       
   661             eventHook.add.call( this, {}, fn );
       
   662           }
       
   663 
       
   664           // Reassign event type to our piggyback event type if defined
       
   665           if ( eventHook.bind ) {
       
   666             type = eventHook.bind;
       
   667           }
       
   668 
       
   669           // Reassign handler if defined
       
   670           if ( eventHook.handler ) {
       
   671             tmp = fn;
       
   672 
       
   673             fn = function wrapper( event ) {
       
   674               eventHook.handler.call( self, event, tmp );
       
   675             };
       
   676           }
       
   677 
       
   678           // assume the piggy back event is registered
       
   679           hasEvents = true;
       
   680 
       
   681           // Setup event registry entry
       
   682           if ( !this.data.events[ type ] ) {
       
   683             this.data.events[ type ] = {};
       
   684             // Toggle if the previous assumption was untrue
       
   685             hasEvents = false;
       
   686           }
       
   687         }
       
   688 
       
   689         //  Register event and handler
       
   690         this.data.events[ type ][ fn.name || ( fn.toString() + Popcorn.guid() ) ] = fn;
       
   691 
       
   692         // only attach one event of any type
       
   693         if ( !hasEvents && Popcorn.events.all.indexOf( type ) > -1 ) {
       
   694 
       
   695           this.media.addEventListener( type, function( event ) {
       
   696 
       
   697             Popcorn.forEach( self.data.events[ type ], function( obj, key ) {
       
   698               if ( typeof obj === "function" ) {
       
   699                 obj.call( self, event );
       
   700               }
       
   701             });
       
   702 
       
   703           }, false);
       
   704         }
       
   705         return this;
       
   706       },
       
   707       unlisten: function( type, fn ) {
       
   708 
       
   709         if ( this.data.events[ type ] && this.data.events[ type ][ fn ] ) {
       
   710 
       
   711           delete this.data.events[ type ][ fn ];
       
   712 
       
   713           return this;
       
   714         }
       
   715 
       
   716         this.data.events[ type ] = null;
       
   717 
       
   718         return this;
       
   719       }
       
   720     },
       
   721     hooks: {
       
   722       canplayall: {
       
   723         bind: "canplaythrough",
       
   724         add: function( event, callback ) {
       
   725 
       
   726           var state = false;
       
   727 
       
   728           if ( this.media.readyState ) {
       
   729 
       
   730             callback.call( this, event );
       
   731 
       
   732             state = true;
       
   733           }
       
   734 
       
   735           this.data.hooks.canplayall = {
       
   736             fired: state
       
   737           };
       
   738         },
       
   739         // declare special handling instructions
       
   740         handler: function canplayall( event, callback ) {
       
   741 
       
   742           if ( !this.data.hooks.canplayall.fired ) {
       
   743             // trigger original user callback once
       
   744             callback.call( this, event );
       
   745 
       
   746             this.data.hooks.canplayall.fired = true;
       
   747           }
       
   748         }
       
   749       }
       
   750     }
       
   751   };
       
   752 
       
   753   //  Extend Popcorn.events.fns (listen, unlisten, trigger) to all Popcorn instances
       
   754   Popcorn.forEach( [ "trigger", "listen", "unlisten" ], function( key ) {
       
   755     Popcorn.p[ key ] = Popcorn.events.fn[ key ];
       
   756   });
       
   757 
       
   758   // Internal Only - Adds track events to the instance object
       
   759   Popcorn.addTrackEvent = function( obj, track ) {
       
   760 
       
   761     // Determine if this track has default options set for it
       
   762     // If so, apply them to the track object
       
   763     if ( track && track._natives && track._natives.type &&
       
   764         ( obj.options.defaults && obj.options.defaults[ track._natives.type ] ) ) {
       
   765 
       
   766       track = Popcorn.extend( {}, obj.options.defaults[ track._natives.type ], track );
       
   767     }
       
   768 
       
   769     if ( track._natives ) {
       
   770       //  Supports user defined track event id
       
   771       track._id = !track.id ? Popcorn.guid( track._natives.type ) : track.id;
       
   772 
       
   773       //  Push track event ids into the history
       
   774       obj.data.history.push( track._id );
       
   775     }
       
   776 
       
   777     track.start = Popcorn.util.toSeconds( track.start, obj.options.framerate );
       
   778     track.end   = Popcorn.util.toSeconds( track.end, obj.options.framerate );
       
   779 
       
   780     //  Store this definition in an array sorted by times
       
   781     var byStart = obj.data.trackEvents.byStart,
       
   782         byEnd = obj.data.trackEvents.byEnd,
       
   783         idx;
       
   784 
       
   785     for ( idx = byStart.length - 1; idx >= 0; idx-- ) {
       
   786 
       
   787       if ( track.start >= byStart[ idx ].start ) {
       
   788         byStart.splice( idx + 1, 0, track );
       
   789         break;
       
   790       }
       
   791     }
       
   792 
       
   793     for ( idx = byEnd.length - 1; idx >= 0; idx-- ) {
       
   794 
       
   795       if ( track.end > byEnd[ idx ].end ) {
       
   796         byEnd.splice( idx + 1, 0, track );
       
   797         break;
       
   798       }
       
   799     }
       
   800 
       
   801     this.timeUpdate( obj, null );
       
   802 
       
   803     // Store references to user added trackevents in ref table
       
   804     if ( track._id ) {
       
   805       Popcorn.addTrackEvent.ref( obj, track );
       
   806     }
       
   807   };
       
   808 
       
   809   // Internal Only - Adds track event references to the instance object's trackRefs hash table
       
   810   Popcorn.addTrackEvent.ref = function( obj, track ) {
       
   811     obj.data.trackRefs[ track._id ] = track;
       
   812 
       
   813     return obj;
       
   814   };
       
   815 
       
   816   Popcorn.removeTrackEvent  = function( obj, trackId ) {
       
   817 
       
   818     var historyLen = obj.data.history.length,
       
   819         indexWasAt = 0,
       
   820         byStart = [],
       
   821         byEnd = [],
       
   822         animating = [],
       
   823         history = [];
       
   824 
       
   825     Popcorn.forEach( obj.data.trackEvents.byStart, function( o, i, context ) {
       
   826       // Preserve the original start/end trackEvents
       
   827       if ( !o._id ) {
       
   828         byStart.push( obj.data.trackEvents.byStart[i] );
       
   829         byEnd.push( obj.data.trackEvents.byEnd[i] );
       
   830       }
       
   831 
       
   832       // Filter for user track events (vs system track events)
       
   833       if ( o._id ) {
       
   834 
       
   835         // Filter for the trackevent to remove
       
   836         if ( o._id !== trackId ) {
       
   837           byStart.push( obj.data.trackEvents.byStart[i] );
       
   838           byEnd.push( obj.data.trackEvents.byEnd[i] );
       
   839         }
       
   840 
       
   841         //  Capture the position of the track being removed.
       
   842         if ( o._id === trackId ) {
       
   843           indexWasAt = i;
       
   844           o._natives._teardown && o._natives._teardown.call( obj, o );
       
   845         }
       
   846       }
       
   847 
       
   848     });
       
   849 
       
   850     if ( obj.data.trackEvents.animating.length ) {
       
   851       Popcorn.forEach( obj.data.trackEvents.animating, function( o, i, context ) {
       
   852         // Preserve the original start/end trackEvents
       
   853         if ( !o._id ) {
       
   854           animating.push( obj.data.trackEvents.animating[i] );
       
   855         }
       
   856 
       
   857         // Filter for user track events (vs system track events)
       
   858         if ( o._id ) {
       
   859           // Filter for the trackevent to remove
       
   860           if ( o._id !== trackId ) {
       
   861             animating.push( obj.data.trackEvents.animating[i] );
       
   862           }
       
   863         }
       
   864       });
       
   865     }
       
   866 
       
   867     //  Update
       
   868     if ( indexWasAt <= obj.data.trackEvents.startIndex ) {
       
   869       obj.data.trackEvents.startIndex--;
       
   870     }
       
   871 
       
   872     if ( indexWasAt <= obj.data.trackEvents.endIndex ) {
       
   873       obj.data.trackEvents.endIndex--;
       
   874     }
       
   875 
       
   876     obj.data.trackEvents.byStart = byStart;
       
   877     obj.data.trackEvents.byEnd = byEnd;
       
   878     obj.data.trackEvents.animating = animating;
       
   879 
       
   880     for ( var i = 0; i < historyLen; i++ ) {
       
   881       if ( obj.data.history[ i ] !== trackId ) {
       
   882         history.push( obj.data.history[ i ] );
       
   883       }
       
   884     }
       
   885 
       
   886     // Update ordered history array
       
   887     obj.data.history = history;
       
   888 
       
   889     // Update track event references
       
   890     Popcorn.removeTrackEvent.ref( obj, trackId );
       
   891   };
       
   892 
       
   893   // Internal Only - Removes track event references from instance object's trackRefs hash table
       
   894   Popcorn.removeTrackEvent.ref = function( obj, trackId ) {
       
   895     delete obj.data.trackRefs[ trackId ];
       
   896 
       
   897     return obj;
       
   898   };
       
   899 
       
   900   // Return an array of track events bound to this instance object
       
   901   Popcorn.getTrackEvents = function( obj ) {
       
   902 
       
   903     var trackevents = [],
       
   904       refs = obj.data.trackEvents.byStart,
       
   905       length = refs.length,
       
   906       idx = 0,
       
   907       ref;
       
   908 
       
   909     for ( ; idx < length; idx++ ) {
       
   910       ref = refs[ idx ];
       
   911       // Return only user attributed track event references
       
   912       if ( ref._id ) {
       
   913         trackevents.push( ref );
       
   914       }
       
   915     }
       
   916 
       
   917     return trackevents;
       
   918   };
       
   919 
       
   920   // Internal Only - Returns an instance object's trackRefs hash table
       
   921   Popcorn.getTrackEvents.ref = function( obj ) {
       
   922     return obj.data.trackRefs;
       
   923   };
       
   924 
       
   925   // Return a single track event bound to this instance object
       
   926   Popcorn.getTrackEvent = function( obj, trackId ) {
       
   927     return obj.data.trackRefs[ trackId ];
       
   928   };
       
   929 
       
   930   // Internal Only - Returns an instance object's track reference by track id
       
   931   Popcorn.getTrackEvent.ref = function( obj, trackId ) {
       
   932     return obj.data.trackRefs[ trackId ];
       
   933   };
       
   934 
       
   935   Popcorn.getLastTrackEventId = function( obj ) {
       
   936     return obj.data.history[ obj.data.history.length - 1 ];
       
   937   };
       
   938 
       
   939   Popcorn.timeUpdate = function( obj, event ) {
       
   940 
       
   941     var currentTime = obj.media.currentTime,
       
   942         previousTime = obj.data.trackEvents.previousUpdateTime,
       
   943         tracks = obj.data.trackEvents,
       
   944         animating = tracks.animating,
       
   945         end = tracks.endIndex,
       
   946         start = tracks.startIndex,
       
   947         animIndex = 0,
       
   948 
       
   949         registryByName = Popcorn.registryByName,
       
   950 
       
   951         byEnd, byStart, byAnimate, natives, type;
       
   952 
       
   953     //  Playbar advancing
       
   954     if ( previousTime <= currentTime ) {
       
   955 
       
   956       while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end <= currentTime ) {
       
   957 
       
   958         byEnd = tracks.byEnd[ end ];
       
   959         natives = byEnd._natives;
       
   960         type = natives && natives.type;
       
   961 
       
   962         //  If plugin does not exist on this instance, remove it
       
   963         if ( !natives ||
       
   964             ( !!registryByName[ type ] ||
       
   965               !!obj[ type ] ) ) {
       
   966 
       
   967           if ( byEnd._running === true ) {
       
   968             byEnd._running = false;
       
   969             natives.end.call( obj, event, byEnd );
       
   970           }
       
   971 
       
   972           end++;
       
   973         } else {
       
   974           // remove track event
       
   975           Popcorn.removeTrackEvent( obj, byEnd._id );
       
   976           return;
       
   977         }
       
   978       }
       
   979 
       
   980       while ( tracks.byStart[ start ] && tracks.byStart[ start ].start <= currentTime ) {
       
   981 
       
   982         byStart = tracks.byStart[ start ];
       
   983         natives = byStart._natives;
       
   984         type = natives && natives.type;
       
   985 
       
   986         //  If plugin does not exist on this instance, remove it
       
   987         if ( !natives ||
       
   988             ( !!registryByName[ type ] ||
       
   989               !!obj[ type ] ) ) {
       
   990 
       
   991           if ( byStart.end > currentTime &&
       
   992                 byStart._running === false &&
       
   993                   obj.data.disabled.indexOf( type ) === -1 ) {
       
   994 
       
   995             byStart._running = true;
       
   996             natives.start.call( obj, event, byStart );
       
   997 
       
   998             // If the `frameAnimation` option is used,
       
   999             // push the current byStart object into the `animating` cue
       
  1000             if ( obj.options.frameAnimation &&
       
  1001                 ( byStart && byStart._running && byStart._natives.frame ) ) {
       
  1002 
       
  1003               animating.push( byStart );
       
  1004             }
       
  1005           }
       
  1006           start++;
       
  1007         } else {
       
  1008           // remove track event
       
  1009           Popcorn.removeTrackEvent( obj, byStart._id );
       
  1010           return;
       
  1011         }
       
  1012       }
       
  1013 
       
  1014       // If the `frameAnimation` option is used, iterate the animating track
       
  1015       // and execute the `frame` callback
       
  1016       if ( obj.options.frameAnimation ) {
       
  1017         while ( animIndex < animating.length ) {
       
  1018 
       
  1019           byAnimate = animating[ animIndex ];
       
  1020 
       
  1021           if ( !byAnimate._running ) {
       
  1022             animating.splice( animIndex, 1 );
       
  1023           } else {
       
  1024             byAnimate._natives.frame.call( obj, event, byAnimate, currentTime );
       
  1025             animIndex++;
       
  1026           }
       
  1027         }
       
  1028       }
       
  1029 
       
  1030     // Playbar receding
       
  1031     } else if ( previousTime > currentTime ) {
       
  1032 
       
  1033       while ( tracks.byStart[ start ] && tracks.byStart[ start ].start > currentTime ) {
       
  1034 
       
  1035         byStart = tracks.byStart[ start ];
       
  1036         natives = byStart._natives;
       
  1037         type = natives && natives.type;
       
  1038 
       
  1039         // if plugin does not exist on this instance, remove it
       
  1040         if ( !natives ||
       
  1041             ( !!registryByName[ type ] ||
       
  1042               !!obj[ type ] ) ) {
       
  1043 
       
  1044           if ( byStart._running === true ) {
       
  1045             byStart._running = false;
       
  1046             natives.end.call( obj, event, byStart );
       
  1047           }
       
  1048           start--;
       
  1049         } else {
       
  1050           // remove track event
       
  1051           Popcorn.removeTrackEvent( obj, byStart._id );
       
  1052           return;
       
  1053         }
       
  1054       }
       
  1055 
       
  1056       while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end > currentTime ) {
       
  1057 
       
  1058         byEnd = tracks.byEnd[ end ];
       
  1059         natives = byEnd._natives;
       
  1060         type = natives && natives.type;
       
  1061 
       
  1062         // if plugin does not exist on this instance, remove it
       
  1063         if ( !natives ||
       
  1064             ( !!registryByName[ type ] ||
       
  1065               !!obj[ type ] ) ) {
       
  1066 
       
  1067           if ( byEnd.start <= currentTime &&
       
  1068                 byEnd._running === false  &&
       
  1069                   obj.data.disabled.indexOf( type ) === -1 ) {
       
  1070 
       
  1071             byEnd._running = true;
       
  1072             natives.start.call( obj, event, byEnd );
       
  1073 
       
  1074             // If the `frameAnimation` option is used,
       
  1075             // push the current byEnd object into the `animating` cue
       
  1076             if ( obj.options.frameAnimation &&
       
  1077                   ( byEnd && byEnd._running && byEnd._natives.frame ) ) {
       
  1078 
       
  1079               animating.push( byEnd );
       
  1080             }
       
  1081           }
       
  1082           end--;
       
  1083         } else {
       
  1084           // remove track event
       
  1085           Popcorn.removeTrackEvent( obj, byEnd._id );
       
  1086           return;
       
  1087         }
       
  1088       }
       
  1089 
       
  1090       // If the `frameAnimation` option is used, iterate the animating track
       
  1091       // and execute the `frame` callback
       
  1092       if ( obj.options.frameAnimation ) {
       
  1093         while ( animIndex < animating.length ) {
       
  1094 
       
  1095           byAnimate = animating[ animIndex ];
       
  1096 
       
  1097           if ( !byAnimate._running ) {
       
  1098             animating.splice( animIndex, 1 );
       
  1099           } else {
       
  1100             byAnimate._natives.frame.call( obj, event, byAnimate, currentTime );
       
  1101             animIndex++;
       
  1102           }
       
  1103         }
       
  1104       }
       
  1105     // time bar is not moving ( video is paused )
       
  1106     }
       
  1107 
       
  1108     tracks.endIndex = end;
       
  1109     tracks.startIndex = start;
       
  1110     tracks.previousUpdateTime = currentTime;
       
  1111   };
       
  1112 
       
  1113   //  Map and Extend TrackEvent functions to all Popcorn instances
       
  1114   Popcorn.extend( Popcorn.p, {
       
  1115 
       
  1116     getTrackEvents: function() {
       
  1117       return Popcorn.getTrackEvents.call( null, this );
       
  1118     },
       
  1119 
       
  1120     getTrackEvent: function( id ) {
       
  1121       return Popcorn.getTrackEvent.call( null, this, id );
       
  1122     },
       
  1123 
       
  1124     getLastTrackEventId: function() {
       
  1125       return Popcorn.getLastTrackEventId.call( null, this );
       
  1126     },
       
  1127 
       
  1128     removeTrackEvent: function( id ) {
       
  1129 
       
  1130       Popcorn.removeTrackEvent.call( null, this, id );
       
  1131       return this;
       
  1132     },
       
  1133 
       
  1134     removePlugin: function( name ) {
       
  1135       Popcorn.removePlugin.call( null, this, name );
       
  1136       return this;
       
  1137     },
       
  1138 
       
  1139     timeUpdate: function( event ) {
       
  1140       Popcorn.timeUpdate.call( null, this, event );
       
  1141       return this;
       
  1142     },
       
  1143 
       
  1144     destroy: function() {
       
  1145       Popcorn.destroy.call( null, this );
       
  1146       return this;
       
  1147     }
       
  1148   });
       
  1149 
       
  1150   //  Plugin manifests
       
  1151   Popcorn.manifest = {};
       
  1152   //  Plugins are registered
       
  1153   Popcorn.registry = [];
       
  1154   Popcorn.registryByName = {};
       
  1155   //  An interface for extending Popcorn
       
  1156   //  with plugin functionality
       
  1157   Popcorn.plugin = function( name, definition, manifest ) {
       
  1158 
       
  1159     if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
       
  1160       Popcorn.error( "'" + name + "' is a protected function name" );
       
  1161       return;
       
  1162     }
       
  1163 
       
  1164     //  Provides some sugar, but ultimately extends
       
  1165     //  the definition into Popcorn.p
       
  1166     var reserved = [ "start", "end" ],
       
  1167         plugin = {},
       
  1168         setup,
       
  1169         isfn = typeof definition === "function",
       
  1170         methods = [ "_setup", "_teardown", "start", "end", "frame" ];
       
  1171 
       
  1172     // combines calls of two function calls into one
       
  1173     var combineFn = function( first, second ) {
       
  1174 
       
  1175       first = first || Popcorn.nop;
       
  1176       second = second || Popcorn.nop;
       
  1177 
       
  1178       return function() {
       
  1179         first.apply( this, arguments );
       
  1180         second.apply( this, arguments );
       
  1181       };
       
  1182     };
       
  1183 
       
  1184     //  If `manifest` arg is undefined, check for manifest within the `definition` object
       
  1185     //  If no `definition.manifest`, an empty object is a sufficient fallback
       
  1186     Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {};
       
  1187 
       
  1188     // apply safe, and empty default functions
       
  1189     methods.forEach(function( method ) {
       
  1190       definition[ method ] = safeTry( definition[ method ] || Popcorn.nop, name );
       
  1191     });
       
  1192 
       
  1193     var pluginFn = function( setup, options ) {
       
  1194 
       
  1195       if ( !options ) {
       
  1196         return this;
       
  1197       }
       
  1198 
       
  1199       //  Storing the plugin natives
       
  1200       var natives = options._natives = {},
       
  1201           compose = "",
       
  1202           defaults, originalOpts, manifestOpts, mergedSetupOpts;
       
  1203 
       
  1204       Popcorn.extend( natives, setup );
       
  1205 
       
  1206       options._natives.type = name;
       
  1207       options._running = false;
       
  1208 
       
  1209       natives.start = natives.start || natives[ "in" ];
       
  1210       natives.end = natives.end || natives[ "out" ];
       
  1211 
       
  1212       // Check for previously set default options
       
  1213       defaults = this.options.defaults && this.options.defaults[ options._natives && options._natives.type ];
       
  1214 
       
  1215       // default to an empty string if no effect exists
       
  1216       // split string into an array of effects
       
  1217       options.compose = options.compose && options.compose.split( " " ) || [];
       
  1218       options.effect = options.effect && options.effect.split( " " ) || [];
       
  1219 
       
  1220       // join the two arrays together
       
  1221       options.compose = options.compose.concat( options.effect );
       
  1222 
       
  1223       options.compose.forEach(function( composeOption ) {
       
  1224 
       
  1225         // if the requested compose is garbage, throw it away
       
  1226         compose = Popcorn.compositions[ composeOption ] || {};
       
  1227 
       
  1228         // extends previous functions with compose function
       
  1229         methods.forEach(function( method ) {
       
  1230           natives[ method ] = combineFn( natives[ method ], compose[ method ] );
       
  1231         });
       
  1232       });
       
  1233 
       
  1234       //  Ensure a manifest object, an empty object is a sufficient fallback
       
  1235       options._natives.manifest = manifest;
       
  1236 
       
  1237       //  Checks for expected properties
       
  1238       if ( !( "start" in options ) ) {
       
  1239         options.start = options[ "in" ] || 0;
       
  1240       }
       
  1241 
       
  1242       if ( !( "end" in options ) ) {
       
  1243         options.end = options[ "out" ] || this.duration() || Number.MAX_VALUE;
       
  1244       }
       
  1245 
       
  1246       // Merge with defaults if they exist, make sure per call is prioritized
       
  1247       mergedSetupOpts = defaults ? Popcorn.extend( {}, defaults, options ) :
       
  1248                           options;
       
  1249 
       
  1250       // Resolves 239, 241, 242
       
  1251       if ( !mergedSetupOpts.target ) {
       
  1252 
       
  1253         //  Sometimes the manifest may be missing entirely
       
  1254         //  or it has an options object that doesn't have a `target` property
       
  1255         manifestOpts = "options" in manifest && manifest.options;
       
  1256 
       
  1257         mergedSetupOpts.target = manifestOpts && "target" in manifestOpts && manifestOpts.target;
       
  1258       }
       
  1259 
       
  1260       // Trigger _setup method if exists
       
  1261       options._natives._setup && options._natives._setup.call( this, mergedSetupOpts );
       
  1262 
       
  1263       // Create new track event for this instance
       
  1264       Popcorn.addTrackEvent( this, Popcorn.extend( mergedSetupOpts, options ) );
       
  1265 
       
  1266       //  Future support for plugin event definitions
       
  1267       //  for all of the native events
       
  1268       Popcorn.forEach( setup, function( callback, type ) {
       
  1269 
       
  1270         if ( type !== "type" ) {
       
  1271 
       
  1272           if ( reserved.indexOf( type ) === -1 ) {
       
  1273 
       
  1274             this.listen( type, callback );
       
  1275           }
       
  1276         }
       
  1277 
       
  1278       }, this );
       
  1279 
       
  1280       return this;
       
  1281     };
       
  1282 
       
  1283     //  Assign new named definition
       
  1284     plugin[ name ] = function( options ) {
       
  1285       return pluginFn.call( this, isfn ? definition.call( this, options ) : definition,
       
  1286                                   options );
       
  1287     };
       
  1288 
       
  1289     //  Extend Popcorn.p with new named definition
       
  1290     Popcorn.extend( Popcorn.p, plugin );
       
  1291 
       
  1292     //  Push into the registry
       
  1293     var entry = {
       
  1294       fn: plugin[ name ],
       
  1295       definition: definition,
       
  1296       base: definition,
       
  1297       parents: [],
       
  1298       name: name
       
  1299     };
       
  1300     Popcorn.registry.push(
       
  1301        Popcorn.extend( plugin, entry, {
       
  1302         type: name
       
  1303       })
       
  1304     );
       
  1305     Popcorn.registryByName[ name ] = entry;
       
  1306 
       
  1307     return plugin;
       
  1308   };
       
  1309 
       
  1310   // Storage for plugin function errors
       
  1311   Popcorn.plugin.errors = [];
       
  1312 
       
  1313   // Returns wrapped plugin function
       
  1314   function safeTry( fn, pluginName ) {
       
  1315     return function() {
       
  1316       try {
       
  1317         return fn.apply( this, arguments );
       
  1318       } catch ( ex ) {
       
  1319         // Push plugin function errors into logging queue
       
  1320         Popcorn.plugin.errors.push({
       
  1321           plugin: pluginName,
       
  1322           thrown: ex,
       
  1323           source: fn.toString()
       
  1324         });
       
  1325 
       
  1326         // Trigger an error that the instance can listen for
       
  1327         // and react to
       
  1328         this.trigger( "error", Popcorn.plugin.errors );
       
  1329       }
       
  1330     };
       
  1331   }
       
  1332 
       
  1333   // Debug-mode flag for plugin development
       
  1334   Popcorn.plugin.debug = false;
       
  1335 
       
  1336   //  removePlugin( type ) removes all tracks of that from all instances of popcorn
       
  1337   //  removePlugin( obj, type ) removes all tracks of type from obj, where obj is a single instance of popcorn
       
  1338   Popcorn.removePlugin = function( obj, name ) {
       
  1339 
       
  1340     //  Check if we are removing plugin from an instance or from all of Popcorn
       
  1341     if ( !name ) {
       
  1342 
       
  1343       //  Fix the order
       
  1344       name = obj;
       
  1345       obj = Popcorn.p;
       
  1346 
       
  1347       if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
       
  1348         Popcorn.error( "'" + name + "' is a protected function name" );
       
  1349         return;
       
  1350       }
       
  1351 
       
  1352       var registryLen = Popcorn.registry.length,
       
  1353           registryIdx;
       
  1354 
       
  1355       // remove plugin reference from registry
       
  1356       for ( registryIdx = 0; registryIdx < registryLen; registryIdx++ ) {
       
  1357         if ( Popcorn.registry[ registryIdx ].name === name ) {
       
  1358           Popcorn.registry.splice( registryIdx, 1 );
       
  1359           delete Popcorn.registryByName[ name ];
       
  1360 
       
  1361           // delete the plugin
       
  1362           delete obj[ name ];
       
  1363 
       
  1364           // plugin found and removed, stop checking, we are done
       
  1365           return;
       
  1366         }
       
  1367       }
       
  1368 
       
  1369     }
       
  1370 
       
  1371     var byStart = obj.data.trackEvents.byStart,
       
  1372         byEnd = obj.data.trackEvents.byEnd,
       
  1373         animating = obj.data.trackEvents.animating,
       
  1374         idx, sl;
       
  1375 
       
  1376     // remove all trackEvents
       
  1377     for ( idx = 0, sl = byStart.length; idx < sl; idx++ ) {
       
  1378 
       
  1379       if ( ( byStart[ idx ] && byStart[ idx ]._natives && byStart[ idx ]._natives.type === name ) &&
       
  1380                 ( byEnd[ idx ] && byEnd[ idx ]._natives && byEnd[ idx ]._natives.type === name ) ) {
       
  1381 
       
  1382         byStart[ idx ]._natives._teardown && byStart[ idx ]._natives._teardown.call( obj, byStart[ idx ] );
       
  1383 
       
  1384         byStart.splice( idx, 1 );
       
  1385         byEnd.splice( idx, 1 );
       
  1386 
       
  1387         // update for loop if something removed, but keep checking
       
  1388         idx--; sl--;
       
  1389         if ( obj.data.trackEvents.startIndex <= idx ) {
       
  1390           obj.data.trackEvents.startIndex--;
       
  1391           obj.data.trackEvents.endIndex--;
       
  1392         }
       
  1393       }
       
  1394     }
       
  1395 
       
  1396     //remove all animating events
       
  1397     for ( idx = 0, sl = animating.length; idx < sl; idx++ ) {
       
  1398 
       
  1399       if ( animating[ idx ] && animating[ idx ]._natives && animating[ idx ]._natives.type === name ) {
       
  1400 
       
  1401         animating.splice( idx, 1 );
       
  1402 
       
  1403         // update for loop if something removed, but keep checking
       
  1404         idx--; sl--;
       
  1405       }
       
  1406     }
       
  1407 
       
  1408   };
       
  1409 
       
  1410   Popcorn.compositions = {};
       
  1411 
       
  1412   //  Plugin inheritance
       
  1413   Popcorn.compose = function( name, definition, manifest ) {
       
  1414 
       
  1415     //  If `manifest` arg is undefined, check for manifest within the `definition` object
       
  1416     //  If no `definition.manifest`, an empty object is a sufficient fallback
       
  1417     Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {};
       
  1418 
       
  1419     // register the effect by name
       
  1420     Popcorn.compositions[ name ] = definition;
       
  1421   };
       
  1422 
       
  1423   Popcorn.plugin.effect = Popcorn.effect = Popcorn.compose;
       
  1424 
       
  1425   // stores parsers keyed on filetype
       
  1426   Popcorn.parsers = {};
       
  1427 
       
  1428   // An interface for extending Popcorn
       
  1429   // with parser functionality
       
  1430   Popcorn.parser = function( name, type, definition ) {
       
  1431 
       
  1432     if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
       
  1433       Popcorn.error( "'" + name + "' is a protected function name" );
       
  1434       return;
       
  1435     }
       
  1436 
       
  1437     // fixes parameters for overloaded function call
       
  1438     if ( typeof type === "function" && !definition ) {
       
  1439       definition = type;
       
  1440       type = "";
       
  1441     }
       
  1442 
       
  1443     if ( typeof definition !== "function" || typeof type !== "string" ) {
       
  1444       return;
       
  1445     }
       
  1446 
       
  1447     // Provides some sugar, but ultimately extends
       
  1448     // the definition into Popcorn.p
       
  1449 
       
  1450     var natives = Popcorn.events.all,
       
  1451         parseFn,
       
  1452         parser = {};
       
  1453 
       
  1454     parseFn = function( filename, callback ) {
       
  1455 
       
  1456       if ( !filename ) {
       
  1457         return this;
       
  1458       }
       
  1459 
       
  1460       var that = this;
       
  1461 
       
  1462       Popcorn.xhr({
       
  1463         url: filename,
       
  1464         dataType: type,
       
  1465         success: function( data ) {
       
  1466 
       
  1467           var tracksObject = definition( data ),
       
  1468               tracksData,
       
  1469               tracksDataLen,
       
  1470               tracksDef,
       
  1471               idx = 0;
       
  1472 
       
  1473           tracksData = tracksObject.data || [];
       
  1474           tracksDataLen = tracksData.length;
       
  1475           tracksDef = null;
       
  1476 
       
  1477           //  If no tracks to process, return immediately
       
  1478           if ( !tracksDataLen ) {
       
  1479             return;
       
  1480           }
       
  1481 
       
  1482           //  Create tracks out of parsed object
       
  1483           for ( ; idx < tracksDataLen; idx++ ) {
       
  1484 
       
  1485             tracksDef = tracksData[ idx ];
       
  1486 
       
  1487             for ( var key in tracksDef ) {
       
  1488 
       
  1489               if ( hasOwn.call( tracksDef, key ) && !!that[ key ] ) {
       
  1490 
       
  1491                 that[ key ]( tracksDef[ key ] );
       
  1492               }
       
  1493             }
       
  1494           }
       
  1495           if ( callback ) {
       
  1496             callback();
       
  1497           }
       
  1498         }
       
  1499       });
       
  1500 
       
  1501       return this;
       
  1502     };
       
  1503 
       
  1504     // Assign new named definition
       
  1505     parser[ name ] = parseFn;
       
  1506 
       
  1507     // Extend Popcorn.p with new named definition
       
  1508     Popcorn.extend( Popcorn.p, parser );
       
  1509 
       
  1510     // keys the function name by filetype extension
       
  1511     //Popcorn.parsers[ name ] = true;
       
  1512 
       
  1513     return parser;
       
  1514   };
       
  1515 
       
  1516   Popcorn.player = function( name, player ) {
       
  1517 
       
  1518     player = player || {};
       
  1519 
       
  1520     var playerFn = function( target, src, options ) {
       
  1521 
       
  1522       options = options || {};
       
  1523 
       
  1524       // List of events
       
  1525       var date = new Date() / 1000,
       
  1526           baselineTime = date,
       
  1527           currentTime = 0,
       
  1528           events = {},
       
  1529 
       
  1530           // The container div of the resource
       
  1531           container = document.getElementById( rIdExp.exec( target ) && rIdExp.exec( target )[ 2 ] ) ||
       
  1532                         document.getElementById( target ) ||
       
  1533                           target,
       
  1534           basePlayer = {},
       
  1535           timeout,
       
  1536           popcorn;
       
  1537 
       
  1538       // copies a div into the media object
       
  1539       for( var val in container ) {
       
  1540 
       
  1541         if ( typeof container[ val ] === "object" ) {
       
  1542 
       
  1543           basePlayer[ val ] = container[ val ];
       
  1544         } else if ( typeof container[ val ] === "function" ) {
       
  1545 
       
  1546           basePlayer[ val ] = (function( value ) {
       
  1547 
       
  1548             return function() {
       
  1549 
       
  1550               return container[ value ].apply( container, arguments );
       
  1551             };
       
  1552           }( val ));
       
  1553         } else {
       
  1554 
       
  1555           Popcorn.player.defineProperty( basePlayer, val, {
       
  1556             get: (function( value ) {
       
  1557 
       
  1558               return function() {
       
  1559 
       
  1560                 return container[ value ];
       
  1561               };
       
  1562             }( val )),
       
  1563             set: Popcorn.nop,
       
  1564             configurable: true
       
  1565           });
       
  1566         }
       
  1567       }
       
  1568 
       
  1569       var timeupdate = function() {
       
  1570 
       
  1571         date = new Date() / 1000;
       
  1572 
       
  1573         if ( !basePlayer.paused ) {
       
  1574 
       
  1575           basePlayer.currentTime = basePlayer.currentTime + ( date - baselineTime );
       
  1576           basePlayer.dispatchEvent( "timeupdate" );
       
  1577           timeout = setTimeout( timeupdate, 10 );
       
  1578         }
       
  1579 
       
  1580         baselineTime = date;
       
  1581       };
       
  1582 
       
  1583       basePlayer.play = function() {
       
  1584 
       
  1585         this.paused = false;
       
  1586 
       
  1587         if ( basePlayer.readyState >= 4 ) {
       
  1588 
       
  1589           baselineTime = new Date() / 1000;
       
  1590           basePlayer.dispatchEvent( "play" );
       
  1591           timeupdate();
       
  1592         }
       
  1593       };
       
  1594 
       
  1595       basePlayer.pause = function() {
       
  1596 
       
  1597         this.paused = true;
       
  1598         basePlayer.dispatchEvent( "pause" );
       
  1599       };
       
  1600 
       
  1601       Popcorn.player.defineProperty( basePlayer, "currentTime", {
       
  1602         get: function() {
       
  1603 
       
  1604           return currentTime;
       
  1605         },
       
  1606         set: function( val ) {
       
  1607 
       
  1608           // make sure val is a number
       
  1609           currentTime = +val;
       
  1610           basePlayer.dispatchEvent( "timeupdate" );
       
  1611           return currentTime;
       
  1612         },
       
  1613         configurable: true
       
  1614       });
       
  1615 
       
  1616       // Adds an event listener to the object
       
  1617       basePlayer.addEventListener = function( evtName, fn ) {
       
  1618 
       
  1619         if ( !events[ evtName ] ) {
       
  1620 
       
  1621           events[ evtName ] = [];
       
  1622         }
       
  1623 
       
  1624         events[ evtName ].push( fn );
       
  1625         return fn;
       
  1626       };
       
  1627 
       
  1628       // Can take event object or simple string
       
  1629       basePlayer.dispatchEvent = function( oEvent ) {
       
  1630 
       
  1631         var evt,
       
  1632             self = this,
       
  1633             eventInterface,
       
  1634             eventName = oEvent.type;
       
  1635 
       
  1636         // A string was passed, create event object
       
  1637         if ( !eventName ) {
       
  1638 
       
  1639           eventName = oEvent;
       
  1640           eventInterface  = Popcorn.events.getInterface( eventName );
       
  1641 
       
  1642           if ( eventInterface ) {
       
  1643 
       
  1644             evt = document.createEvent( eventInterface );
       
  1645             evt.initEvent( eventName, true, true, window, 1 );
       
  1646           }
       
  1647         }
       
  1648 
       
  1649         Popcorn.forEach( events[ eventName ], function( val ) {
       
  1650 
       
  1651           val.call( self, evt, self );
       
  1652         });
       
  1653       };
       
  1654 
       
  1655       // Attempt to get src from playerFn parameter
       
  1656       basePlayer.src = src || "";
       
  1657       basePlayer.readyState = 0;
       
  1658       basePlayer.duration = 0;
       
  1659       basePlayer.paused = true;
       
  1660       basePlayer.ended = 0;
       
  1661 
       
  1662       // basePlayer has no concept of sound
       
  1663       basePlayer.volume = 1;
       
  1664       basePlayer.muted = false;
       
  1665 
       
  1666       if ( player._setup ) {
       
  1667 
       
  1668         player._setup.call( basePlayer, options );
       
  1669       } else {
       
  1670 
       
  1671         // there is no setup, which means there is nothing to load
       
  1672         basePlayer.readyState = 4;
       
  1673         basePlayer.dispatchEvent( 'load' );
       
  1674       }
       
  1675 
       
  1676       popcorn = new Popcorn.p.init( basePlayer, options );
       
  1677 
       
  1678       return popcorn;
       
  1679     };
       
  1680 
       
  1681     Popcorn[ name ] = Popcorn[ name ] || playerFn;
       
  1682   };
       
  1683 
       
  1684   Popcorn.player.defineProperty = Object.defineProperty || function( object, description, options ) {
       
  1685 
       
  1686     object.__defineGetter__( description, options.get || Popcorn.nop );
       
  1687     object.__defineSetter__( description, options.set || Popcorn.nop );
       
  1688   };
       
  1689 
       
  1690   //  Cache references to reused RegExps
       
  1691   var rparams = /\?/,
       
  1692   //  XHR Setup object
       
  1693   setup = {
       
  1694     url: "",
       
  1695     data: "",
       
  1696     dataType: "",
       
  1697     success: Popcorn.nop,
       
  1698     type: "GET",
       
  1699     async: true,
       
  1700     xhr: function() {
       
  1701       return new global.XMLHttpRequest();
       
  1702     }
       
  1703   };
       
  1704 
       
  1705   Popcorn.xhr = function( options ) {
       
  1706 
       
  1707     options.dataType = options.dataType && options.dataType.toLowerCase() || null;
       
  1708 
       
  1709     if ( options.dataType &&
       
  1710          ( options.dataType === "jsonp" || options.dataType === "script" ) ) {
       
  1711 
       
  1712       Popcorn.xhr.getJSONP(
       
  1713         options.url,
       
  1714         options.success,
       
  1715         options.dataType === "script"
       
  1716       );
       
  1717       return;
       
  1718     }
       
  1719 
       
  1720     var settings = Popcorn.extend( {}, setup, options );
       
  1721 
       
  1722     //  Create new XMLHttpRequest object
       
  1723     settings.ajax  = settings.xhr();
       
  1724 
       
  1725     if ( settings.ajax ) {
       
  1726 
       
  1727       if ( settings.type === "GET" && settings.data ) {
       
  1728 
       
  1729         //  append query string
       
  1730         settings.url += ( rparams.test( settings.url ) ? "&" : "?" ) + settings.data;
       
  1731 
       
  1732         //  Garbage collect and reset settings.data
       
  1733         settings.data = null;
       
  1734       }
       
  1735 
       
  1736 
       
  1737       settings.ajax.open( settings.type, settings.url, settings.async );
       
  1738       settings.ajax.send( settings.data || null );
       
  1739 
       
  1740       return Popcorn.xhr.httpData( settings );
       
  1741     }
       
  1742   };
       
  1743 
       
  1744 
       
  1745   Popcorn.xhr.httpData = function( settings ) {
       
  1746 
       
  1747     var data, json = null;
       
  1748 
       
  1749     settings.ajax.onreadystatechange = function() {
       
  1750 
       
  1751       if ( settings.ajax.readyState === 4 ) {
       
  1752 
       
  1753         try {
       
  1754           json = JSON.parse( settings.ajax.responseText );
       
  1755         } catch( e ) {
       
  1756           //suppress
       
  1757         }
       
  1758 
       
  1759         data = {
       
  1760           xml: settings.ajax.responseXML,
       
  1761           text: settings.ajax.responseText,
       
  1762           json: json
       
  1763         };
       
  1764 
       
  1765         //  If a dataType was specified, return that type of data
       
  1766         if ( settings.dataType ) {
       
  1767           data = data[ settings.dataType ];
       
  1768         }
       
  1769 
       
  1770 
       
  1771         settings.success.call( settings.ajax, data );
       
  1772 
       
  1773       }
       
  1774     };
       
  1775     return data;
       
  1776   };
       
  1777 
       
  1778   Popcorn.xhr.getJSONP = function( url, success, isScript ) {
       
  1779 
       
  1780     var head = document.head || document.getElementsByTagName( "head" )[ 0 ] || document.documentElement,
       
  1781       script = document.createElement( "script" ),
       
  1782       paramStr = url.split( "?" )[ 1 ],
       
  1783       isFired = false,
       
  1784       params = [],
       
  1785       callback, parts, callparam;
       
  1786 
       
  1787     if ( paramStr && !isScript ) {
       
  1788       params = paramStr.split( "&" );
       
  1789     }
       
  1790 
       
  1791     if ( params.length ) {
       
  1792       parts = params[ params.length - 1 ].split( "=" );
       
  1793     }
       
  1794 
       
  1795     callback = params.length ? ( parts[ 1 ] ? parts[ 1 ] : parts[ 0 ]  ) : "jsonp";
       
  1796 
       
  1797     if ( !paramStr && !isScript ) {
       
  1798       url += "?callback=" + callback;
       
  1799     }
       
  1800 
       
  1801     if ( callback && !isScript ) {
       
  1802 
       
  1803       //  If a callback name already exists
       
  1804       if ( !!window[ callback ] ) {
       
  1805         //  Create a new unique callback name
       
  1806         callback = Popcorn.guid( callback );
       
  1807       }
       
  1808 
       
  1809       //  Define the JSONP success callback globally
       
  1810       window[ callback ] = function( data ) {
       
  1811         // Fire success callbacks
       
  1812         success && success( data );
       
  1813         isFired = true;
       
  1814       };
       
  1815 
       
  1816       //  Replace callback param and callback name
       
  1817       url = url.replace( parts.join( "=" ), parts[ 0 ] + "=" + callback );
       
  1818     }
       
  1819 
       
  1820     script.onload = function() {
       
  1821 
       
  1822       //  Handling remote script loading callbacks
       
  1823       if ( isScript ) {
       
  1824         //  getScript
       
  1825         success && success();
       
  1826       }
       
  1827 
       
  1828       //  Executing for JSONP requests
       
  1829       if ( isFired ) {
       
  1830         //  Garbage collect the callback
       
  1831         delete window[ callback ];
       
  1832       }
       
  1833       //  Garbage collect the script resource
       
  1834       head.removeChild( script );
       
  1835     };
       
  1836 
       
  1837     script.src = url;
       
  1838 
       
  1839     head.insertBefore( script, head.firstChild );
       
  1840 
       
  1841     return;
       
  1842   };
       
  1843 
       
  1844   Popcorn.getJSONP = Popcorn.xhr.getJSONP;
       
  1845 
       
  1846   Popcorn.getScript = Popcorn.xhr.getScript = function( url, success ) {
       
  1847 
       
  1848     return Popcorn.xhr.getJSONP( url, success, true );
       
  1849   };
       
  1850 
       
  1851   Popcorn.util = {
       
  1852     // Simple function to parse a timestamp into seconds
       
  1853     // Acceptable formats are:
       
  1854     // HH:MM:SS.MMM
       
  1855     // HH:MM:SS;FF
       
  1856     // Hours and minutes are optional. They default to 0
       
  1857     toSeconds: function( timeStr, framerate ) {
       
  1858       // Hours and minutes are optional
       
  1859       // Seconds must be specified
       
  1860       // Seconds can be followed by milliseconds OR by the frame information
       
  1861       var validTimeFormat = /^([0-9]+:){0,2}[0-9]+([.;][0-9]+)?$/,
       
  1862           errorMessage = "Invalid time format",
       
  1863           digitPairs, lastIndex, lastPair, firstPair,
       
  1864           frameInfo, frameTime;
       
  1865 
       
  1866       if ( typeof timeStr === "number" ) {
       
  1867         return timeStr;
       
  1868       }
       
  1869 
       
  1870       if ( typeof timeStr === "string" &&
       
  1871             !validTimeFormat.test( timeStr ) ) {
       
  1872         Popcorn.error( errorMessage );
       
  1873       }
       
  1874 
       
  1875       digitPairs = timeStr.split( ":" );
       
  1876       lastIndex = digitPairs.length - 1;
       
  1877       lastPair = digitPairs[ lastIndex ];
       
  1878 
       
  1879       // Fix last element:
       
  1880       if ( lastPair.indexOf( ";" ) > -1 ) {
       
  1881 
       
  1882         frameInfo = lastPair.split( ";" );
       
  1883         frameTime = 0;
       
  1884 
       
  1885         if ( framerate && ( typeof framerate === "number" ) ) {
       
  1886           frameTime = parseFloat( frameInfo[ 1 ], 10 ) / framerate;
       
  1887         }
       
  1888 
       
  1889         digitPairs[ lastIndex ] = parseInt( frameInfo[ 0 ], 10 ) + frameTime;
       
  1890       }
       
  1891 
       
  1892       firstPair = digitPairs[ 0 ];
       
  1893 
       
  1894       return {
       
  1895 
       
  1896         1: parseFloat( firstPair, 10 ),
       
  1897 
       
  1898         2: ( parseInt( firstPair, 10 ) * 60 ) +
       
  1899               parseFloat( digitPairs[ 1 ], 10 ),
       
  1900 
       
  1901         3: ( parseInt( firstPair, 10 ) * 3600 ) +
       
  1902             ( parseInt( digitPairs[ 1 ], 10 ) * 60 ) +
       
  1903               parseFloat( digitPairs[ 2 ], 10 )
       
  1904 
       
  1905       }[ digitPairs.length || 1 ];
       
  1906     }
       
  1907   };
       
  1908 
       
  1909 
       
  1910   // Initialize locale data
       
  1911   // Based on http://en.wikipedia.org/wiki/Language_localisation#Language_tags_and_codes
       
  1912   function initLocale( arg ) {
       
  1913 
       
  1914     var locale = typeof arg === "string" ? arg : [ arg.language, arg.region ].join( "-" ),
       
  1915         parts = locale.split( "-" );
       
  1916 
       
  1917     // Setup locale data table
       
  1918     return {
       
  1919       iso6391: locale,
       
  1920       language: parts[ 0 ] || "",
       
  1921       region: parts[ 1 ] || ""
       
  1922     };
       
  1923   }
       
  1924 
       
  1925   // Declare locale data table
       
  1926   var localeData = initLocale( global.navigator.userLanguage || global.navigator.language );
       
  1927 
       
  1928   Popcorn.locale = {
       
  1929 
       
  1930     // Popcorn.locale.get()
       
  1931     // returns reference to privately
       
  1932     // defined localeData
       
  1933     get: function() {
       
  1934       return localeData;
       
  1935     },
       
  1936 
       
  1937     // Popcorn.locale.set( string|object );
       
  1938     set: function( arg ) {
       
  1939 
       
  1940       localeData = initLocale( arg );
       
  1941 
       
  1942       Popcorn.locale.broadcast();
       
  1943 
       
  1944       return localeData;
       
  1945     },
       
  1946 
       
  1947     // Popcorn.locale.broadcast( type )
       
  1948     // Sends events to all popcorn media instances that are
       
  1949     // listening for locale events
       
  1950     broadcast: function( type ) {
       
  1951 
       
  1952       var instances = Popcorn.instances,
       
  1953           length = instances.length,
       
  1954           idx = 0,
       
  1955           instance;
       
  1956 
       
  1957       type = type || "locale:changed";
       
  1958 
       
  1959       // Iterate all current instances
       
  1960       for ( ; idx < length; idx++ ) {
       
  1961         instance = instances[ idx ];
       
  1962 
       
  1963         // For those instances with locale event listeners,
       
  1964         // trigger a locale change event
       
  1965         if ( type in instance.data.events  ) {
       
  1966           instance.trigger( type );
       
  1967         }
       
  1968       }
       
  1969     }
       
  1970   };
       
  1971 
       
  1972   // alias for exec function
       
  1973   Popcorn.p.cue = Popcorn.p.exec;
       
  1974 
       
  1975   //  Protected API methods
       
  1976   Popcorn.protect = {
       
  1977     natives: Object.keys( Popcorn.p ).join( "," ).toLowerCase().split( "," )
       
  1978   };
       
  1979 
       
  1980   //  Exposes Popcorn to global context
       
  1981   global.Popcorn = Popcorn;
       
  1982 
       
  1983 })(window, window.document);