/static/popcorn/popcorn-complete.js
JavaScript | 9167 lines | 5793 code | 1632 blank | 1742 comment | 948 complexity | 8233cb13ba5c8f9a4a1b7108c1a76223 MD5 | raw file
Possible License(s): BSD-3-Clause, BSD-2-Clause, Apache-2.0, MIT, GPL-3.0
Large files files are truncated, but you can click here to view the full file
- /*
- * popcorn.js version 1.3
- * http://popcornjs.org
- *
- * Copyright 2011, Mozilla Foundation
- * Licensed under the MIT license
- */
- (function(global, document) {
- // Popcorn.js does not support archaic browsers
- if ( !document.addEventListener ) {
- global.Popcorn = {
- isSupported: false
- };
- var methods = ( "byId forEach extend effects error guid sizeOf isArray nop position disable enable destroy" +
- "addTrackEvent removeTrackEvent getTrackEvents getTrackEvent getLastTrackEventId " +
- "timeUpdate plugin removePlugin compose effect xhr getJSONP getScript" ).split(/\s+/);
- while ( methods.length ) {
- global.Popcorn[ methods.shift() ] = function() {};
- }
- return;
- }
- var
- AP = Array.prototype,
- OP = Object.prototype,
- forEach = AP.forEach,
- slice = AP.slice,
- hasOwn = OP.hasOwnProperty,
- toString = OP.toString,
- // Copy global Popcorn (may not exist)
- _Popcorn = global.Popcorn,
- // Ready fn cache
- readyStack = [],
- readyBound = false,
- readyFired = false,
- // Non-public internal data object
- internal = {
- events: {
- hash: {},
- apis: {}
- }
- },
- // Non-public `requestAnimFrame`
- // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
- requestAnimFrame = (function(){
- return global.requestAnimationFrame ||
- global.webkitRequestAnimationFrame ||
- global.mozRequestAnimationFrame ||
- global.oRequestAnimationFrame ||
- global.msRequestAnimationFrame ||
- function( callback, element ) {
- global.setTimeout( callback, 16 );
- };
- }()),
- // Non-public `getKeys`, return an object's keys as an array
- getKeys = function( obj ) {
- return Object.keys ? Object.keys( obj ) : (function( obj ) {
- var item,
- list = [];
- for ( item in obj ) {
- if ( hasOwn.call( obj, item ) ) {
- list.push( item );
- }
- }
- return list;
- })( obj );
- },
- // Declare constructor
- // Returns an instance object.
- Popcorn = function( entity, options ) {
- // Return new Popcorn object
- return new Popcorn.p.init( entity, options || null );
- };
- // Popcorn API version, automatically inserted via build system.
- Popcorn.version = "1.3";
- // Boolean flag allowing a client to determine if Popcorn can be supported
- Popcorn.isSupported = true;
- // Instance caching
- Popcorn.instances = [];
- // Declare a shortcut (Popcorn.p) to and a definition of
- // the new prototype for our Popcorn constructor
- Popcorn.p = Popcorn.prototype = {
- init: function( entity, options ) {
- var matches, nodeName,
- self = this;
- // Supports Popcorn(function () { /../ })
- // Originally proposed by Daniel Brooks
- if ( typeof entity === "function" ) {
- // If document ready has already fired
- if ( document.readyState === "complete" ) {
- entity( document, Popcorn );
- return;
- }
- // Add `entity` fn to ready stack
- readyStack.push( entity );
- // This process should happen once per page load
- if ( !readyBound ) {
- // set readyBound flag
- readyBound = true;
- var DOMContentLoaded = function() {
- readyFired = true;
- // Remove global DOM ready listener
- document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
- // Execute all ready function in the stack
- for ( var i = 0, readyStackLength = readyStack.length; i < readyStackLength; i++ ) {
- readyStack[ i ].call( document, Popcorn );
- }
- // GC readyStack
- readyStack = null;
- };
- // Register global DOM ready listener
- document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
- }
- return;
- }
- if ( typeof entity === "string" ) {
- try {
- matches = document.querySelector( entity );
- } catch( e ) {
- throw new Error( "Popcorn.js Error: Invalid media element selector: " + entity );
- }
- }
- // Get media element by id or object reference
- this.media = matches || entity;
- // inner reference to this media element's nodeName string value
- nodeName = ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video";
- // Create an audio or video element property reference
- this[ nodeName ] = this.media;
- this.options = options || {};
- // Resolve custom ID or default prefixed ID
- this.id = this.options.id || Popcorn.guid( nodeName );
- // Throw if an attempt is made to use an ID that already exists
- if ( Popcorn.byId( this.id ) ) {
- throw new Error( "Popcorn.js Error: Cannot use duplicate ID (" + this.id + ")" );
- }
- this.isDestroyed = false;
- this.data = {
- // data structure of all
- running: {
- cue: []
- },
- // Executed by either timeupdate event or in rAF loop
- timeUpdate: Popcorn.nop,
- // Allows disabling a plugin per instance
- disabled: {},
- // Stores DOM event queues by type
- events: {},
- // Stores Special event hooks data
- hooks: {},
- // Store track event history data
- history: [],
- // Stores ad-hoc state related data]
- state: {
- volume: this.media.volume
- },
- // Store track event object references by trackId
- trackRefs: {},
- // Playback track event queues
- trackEvents: {
- byStart: [{
- start: -1,
- end: -1
- }],
- byEnd: [{
- start: -1,
- end: -1
- }],
- animating: [],
- startIndex: 0,
- endIndex: 0,
- previousUpdateTime: -1
- }
- };
- // Register new instance
- Popcorn.instances.push( this );
- // function to fire when video is ready
- var isReady = function() {
- // chrome bug: http://code.google.com/p/chromium/issues/detail?id=119598
- // it is possible the video's time is less than 0
- // this has the potential to call track events more than once, when they should not
- // start: 0, end: 1 will start, end, start again, when it should just start
- // just setting it to 0 if it is below 0 fixes this issue
- if ( self.media.currentTime < 0 ) {
- self.media.currentTime = 0;
- }
- self.media.removeEventListener( "loadeddata", isReady, false );
- var duration, videoDurationPlus,
- runningPlugins, runningPlugin, rpLength, rpNatives;
- // Adding padding to the front and end of the arrays
- // this is so we do not fall off either end
- duration = self.media.duration;
- // Check for no duration info (NaN)
- videoDurationPlus = duration != duration ? Number.MAX_VALUE : duration + 1;
- Popcorn.addTrackEvent( self, {
- start: videoDurationPlus,
- end: videoDurationPlus
- });
- if ( self.options.frameAnimation ) {
- // if Popcorn is created with frameAnimation option set to true,
- // requestAnimFrame is used instead of "timeupdate" media event.
- // This is for greater frame time accuracy, theoretically up to
- // 60 frames per second as opposed to ~4 ( ~every 15-250ms)
- self.data.timeUpdate = function () {
- Popcorn.timeUpdate( self, {} );
- // fire frame for each enabled active plugin of every type
- Popcorn.forEach( Popcorn.manifest, function( key, val ) {
- runningPlugins = self.data.running[ val ];
- // ensure there are running plugins on this type on this instance
- if ( runningPlugins ) {
- rpLength = runningPlugins.length;
- for ( var i = 0; i < rpLength; i++ ) {
- runningPlugin = runningPlugins[ i ];
- rpNatives = runningPlugin._natives;
- rpNatives && rpNatives.frame &&
- rpNatives.frame.call( self, {}, runningPlugin, self.currentTime() );
- }
- }
- });
- self.emit( "timeupdate" );
- !self.isDestroyed && requestAnimFrame( self.data.timeUpdate );
- };
- !self.isDestroyed && requestAnimFrame( self.data.timeUpdate );
- } else {
- self.data.timeUpdate = function( event ) {
- Popcorn.timeUpdate( self, event );
- };
- if ( !self.isDestroyed ) {
- self.media.addEventListener( "timeupdate", self.data.timeUpdate, false );
- }
- }
- };
- Object.defineProperty( this, "error", {
- get: function() {
- return self.media.error;
- }
- });
- if ( self.media.readyState >= 2 ) {
- isReady();
- } else {
- self.media.addEventListener( "loadeddata", isReady, false );
- }
- return this;
- }
- };
- // Extend constructor prototype to instance prototype
- // Allows chaining methods to instances
- Popcorn.p.init.prototype = Popcorn.p;
- Popcorn.byId = function( str ) {
- var instances = Popcorn.instances,
- length = instances.length,
- i = 0;
- for ( ; i < length; i++ ) {
- if ( instances[ i ].id === str ) {
- return instances[ i ];
- }
- }
- return null;
- };
- Popcorn.forEach = function( obj, fn, context ) {
- if ( !obj || !fn ) {
- return {};
- }
- context = context || this;
- var key, len;
- // Use native whenever possible
- if ( forEach && obj.forEach === forEach ) {
- return obj.forEach( fn, context );
- }
- if ( toString.call( obj ) === "[object NodeList]" ) {
- for ( key = 0, len = obj.length; key < len; key++ ) {
- fn.call( context, obj[ key ], key, obj );
- }
- return obj;
- }
- for ( key in obj ) {
- if ( hasOwn.call( obj, key ) ) {
- fn.call( context, obj[ key ], key, obj );
- }
- }
- return obj;
- };
- Popcorn.extend = function( obj ) {
- var dest = obj, src = slice.call( arguments, 1 );
- Popcorn.forEach( src, function( copy ) {
- for ( var prop in copy ) {
- dest[ prop ] = copy[ prop ];
- }
- });
- return dest;
- };
- // A Few reusable utils, memoized onto Popcorn
- Popcorn.extend( Popcorn, {
- noConflict: function( deep ) {
- if ( deep ) {
- global.Popcorn = _Popcorn;
- }
- return Popcorn;
- },
- error: function( msg ) {
- throw new Error( msg );
- },
- guid: function( prefix ) {
- Popcorn.guid.counter++;
- return ( prefix ? prefix : "" ) + ( +new Date() + Popcorn.guid.counter );
- },
- sizeOf: function( obj ) {
- var size = 0;
- for ( var prop in obj ) {
- size++;
- }
- return size;
- },
- isArray: Array.isArray || function( array ) {
- return toString.call( array ) === "[object Array]";
- },
- nop: function() {},
- position: function( elem ) {
- var clientRect = elem.getBoundingClientRect(),
- bounds = {},
- doc = elem.ownerDocument,
- docElem = document.documentElement,
- body = document.body,
- clientTop, clientLeft, scrollTop, scrollLeft, top, left;
- // Determine correct clientTop/Left
- clientTop = docElem.clientTop || body.clientTop || 0;
- clientLeft = docElem.clientLeft || body.clientLeft || 0;
- // Determine correct scrollTop/Left
- scrollTop = ( global.pageYOffset && docElem.scrollTop || body.scrollTop );
- scrollLeft = ( global.pageXOffset && docElem.scrollLeft || body.scrollLeft );
- // Temp top/left
- top = Math.ceil( clientRect.top + scrollTop - clientTop );
- left = Math.ceil( clientRect.left + scrollLeft - clientLeft );
- for ( var p in clientRect ) {
- bounds[ p ] = Math.round( clientRect[ p ] );
- }
- return Popcorn.extend({}, bounds, { top: top, left: left });
- },
- disable: function( instance, plugin ) {
- if ( !instance.data.disabled[ plugin ] ) {
- instance.data.disabled[ plugin ] = true;
- for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) {
- event = instance.data.running[ plugin ][ i ];
- event._natives.end.call( instance, null, event );
- }
- }
- return instance;
- },
- enable: function( instance, plugin ) {
- if ( instance.data.disabled[ plugin ] ) {
- instance.data.disabled[ plugin ] = false;
- for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) {
- event = instance.data.running[ plugin ][ i ];
- event._natives.start.call( instance, null, event );
- }
- }
- return instance;
- },
- destroy: function( instance ) {
- var events = instance.data.events,
- trackEvents = instance.data.trackEvents,
- singleEvent, item, fn, plugin;
- // Iterate through all events and remove them
- for ( item in events ) {
- singleEvent = events[ item ];
- for ( fn in singleEvent ) {
- delete singleEvent[ fn ];
- }
- events[ item ] = null;
- }
- // remove all plugins off the given instance
- for ( plugin in Popcorn.registryByName ) {
- Popcorn.removePlugin( instance, plugin );
- }
- // Remove all data.trackEvents #1178
- trackEvents.byStart.length = 0;
- trackEvents.byEnd.length = 0;
- if ( !instance.isDestroyed ) {
- instance.data.timeUpdate && instance.media.removeEventListener( "timeupdate", instance.data.timeUpdate, false );
- instance.isDestroyed = true;
- }
- }
- });
- // Memoized GUID Counter
- Popcorn.guid.counter = 1;
- // Factory to implement getters, setters and controllers
- // as Popcorn instance methods. The IIFE will create and return
- // an object with defined methods
- Popcorn.extend(Popcorn.p, (function() {
- var methods = "load play pause currentTime playbackRate volume duration preload playbackRate " +
- "autoplay loop controls muted buffered readyState seeking paused played seekable ended",
- ret = {};
- // Build methods, store in object that is returned and passed to extend
- Popcorn.forEach( methods.split( /\s+/g ), function( name ) {
- ret[ name ] = function( arg ) {
- var previous;
- if ( typeof this.media[ name ] === "function" ) {
- // Support for shorthanded play(n)/pause(n) jump to currentTime
- // If arg is not null or undefined and called by one of the
- // allowed shorthandable methods, then set the currentTime
- // Supports time as seconds or SMPTE
- if ( arg != null && /play|pause/.test( name ) ) {
- this.media.currentTime = Popcorn.util.toSeconds( arg );
- }
- this.media[ name ]();
- return this;
- }
- if ( arg != null ) {
- // Capture the current value of the attribute property
- previous = this.media[ name ];
- // Set the attribute property with the new value
- this.media[ name ] = arg;
- // If the new value is not the same as the old value
- // emit an "attrchanged event"
- if ( previous !== arg ) {
- this.emit( "attrchange", {
- attribute: name,
- previousValue: previous,
- currentValue: arg
- });
- }
- return this;
- }
- return this.media[ name ];
- };
- });
- return ret;
- })()
- );
- Popcorn.forEach( "enable disable".split(" "), function( method ) {
- Popcorn.p[ method ] = function( plugin ) {
- return Popcorn[ method ]( this, plugin );
- };
- });
- Popcorn.extend(Popcorn.p, {
- // Rounded currentTime
- roundTime: function() {
- return Math.round( this.media.currentTime );
- },
- // Attach an event to a single point in time
- exec: function( id, time, fn ) {
- var length = arguments.length,
- trackEvent, sec;
- // Check if first could possibly be a SMPTE string
- // p.cue( "smpte string", fn );
- // try/catch avoid awful throw in Popcorn.util.toSeconds
- // TODO: Get rid of that, replace with NaN return?
- try {
- sec = Popcorn.util.toSeconds( id );
- } catch ( e ) {}
- // If it can be converted into a number then
- // it's safe to assume that the string was SMPTE
- if ( typeof sec === "number" ) {
- id = sec;
- }
- // Shift arguments based on use case
- //
- // Back compat for:
- // p.cue( time, fn );
- if ( typeof id === "number" && length === 2 ) {
- fn = time;
- time = id;
- id = Popcorn.guid( "cue" );
- } else {
- // Support for new forms
- // p.cue( "empty-cue" );
- if ( length === 1 ) {
- // Set a time for an empty cue. It's not important what
- // the time actually is, because the cue is a no-op
- time = -1;
- } else {
- // Get the trackEvent that matches the given id.
- trackEvent = this.getTrackEvent( id );
- if ( trackEvent ) {
- // p.cue( "my-id", 12 );
- // p.cue( "my-id", function() { ... });
- if ( typeof id === "string" && length === 2 ) {
- // p.cue( "my-id", 12 );
- // The path will update the cue time.
- if ( typeof time === "number" ) {
- // Re-use existing trackEvent start callback
- fn = trackEvent._natives.start;
- }
- // p.cue( "my-id", function() { ... });
- // The path will update the cue function
- if ( typeof time === "function" ) {
- fn = time;
- // Re-use existing trackEvent start time
- time = trackEvent.start;
- }
- }
- } else {
- if ( length >= 2 ) {
- // p.cue( "a", "00:00:00");
- if ( typeof time === "string" ) {
- try {
- sec = Popcorn.util.toSeconds( time );
- } catch ( e ) {}
- time = sec;
- }
- // p.cue( "b", 11 );
- if ( typeof time === "number" ) {
- fn = Popcorn.nop();
- }
- // p.cue( "c", function() {});
- if ( typeof time === "function" ) {
- fn = time;
- time = -1;
- }
- }
- }
- }
- }
- // Creating a one second track event with an empty end
- // Or update an existing track event with new values
- Popcorn.addTrackEvent( this, {
- id: id,
- start: time,
- end: time + 1,
- _running: false,
- _natives: {
- start: fn || Popcorn.nop,
- end: Popcorn.nop,
- type: "cue"
- }
- });
- return this;
- },
- // Mute the calling media, optionally toggle
- mute: function( toggle ) {
- var event = toggle == null || toggle === true ? "muted" : "unmuted";
- // If `toggle` is explicitly `false`,
- // unmute the media and restore the volume level
- if ( event === "unmuted" ) {
- this.media.muted = false;
- this.media.volume = this.data.state.volume;
- }
- // If `toggle` is either null or undefined,
- // save the current volume and mute the media element
- if ( event === "muted" ) {
- this.data.state.volume = this.media.volume;
- this.media.muted = true;
- }
- // Trigger either muted|unmuted event
- this.emit( event );
- return this;
- },
- // Convenience method, unmute the calling media
- unmute: function( toggle ) {
- return this.mute( toggle == null ? false : !toggle );
- },
- // Get the client bounding box of an instance element
- position: function() {
- return Popcorn.position( this.media );
- },
- // Toggle a plugin's playback behaviour (on or off) per instance
- toggle: function( plugin ) {
- return Popcorn[ this.data.disabled[ plugin ] ? "enable" : "disable" ]( this, plugin );
- },
- // Set default values for plugin options objects per instance
- defaults: function( plugin, defaults ) {
- // If an array of default configurations is provided,
- // iterate and apply each to this instance
- if ( Popcorn.isArray( plugin ) ) {
- Popcorn.forEach( plugin, function( obj ) {
- for ( var name in obj ) {
- this.defaults( name, obj[ name ] );
- }
- }, this );
- return this;
- }
- if ( !this.options.defaults ) {
- this.options.defaults = {};
- }
- if ( !this.options.defaults[ plugin ] ) {
- this.options.defaults[ plugin ] = {};
- }
- Popcorn.extend( this.options.defaults[ plugin ], defaults );
- return this;
- }
- });
- Popcorn.Events = {
- UIEvents: "blur focus focusin focusout load resize scroll unload",
- MouseEvents: "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave click dblclick",
- Events: "loadstart progress suspend emptied stalled play pause error " +
- "loadedmetadata loadeddata waiting playing canplay canplaythrough " +
- "seeking seeked timeupdate ended ratechange durationchange volumechange"
- };
- Popcorn.Events.Natives = Popcorn.Events.UIEvents + " " +
- Popcorn.Events.MouseEvents + " " +
- Popcorn.Events.Events;
- internal.events.apiTypes = [ "UIEvents", "MouseEvents", "Events" ];
- // Privately compile events table at load time
- (function( events, data ) {
- var apis = internal.events.apiTypes,
- eventsList = events.Natives.split( /\s+/g ),
- idx = 0, len = eventsList.length, prop;
- for( ; idx < len; idx++ ) {
- data.hash[ eventsList[idx] ] = true;
- }
- apis.forEach(function( val, idx ) {
- data.apis[ val ] = {};
- var apiEvents = events[ val ].split( /\s+/g ),
- len = apiEvents.length,
- k = 0;
- for ( ; k < len; k++ ) {
- data.apis[ val ][ apiEvents[ k ] ] = true;
- }
- });
- })( Popcorn.Events, internal.events );
- Popcorn.events = {
- isNative: function( type ) {
- return !!internal.events.hash[ type ];
- },
- getInterface: function( type ) {
- if ( !Popcorn.events.isNative( type ) ) {
- return false;
- }
- var eventApi = internal.events,
- apis = eventApi.apiTypes,
- apihash = eventApi.apis,
- idx = 0, len = apis.length, api, tmp;
- for ( ; idx < len; idx++ ) {
- tmp = apis[ idx ];
- if ( apihash[ tmp ][ type ] ) {
- api = tmp;
- break;
- }
- }
- return api;
- },
- // Compile all native events to single array
- all: Popcorn.Events.Natives.split( /\s+/g ),
- // Defines all Event handling static functions
- fn: {
- trigger: function( type, data ) {
- var eventInterface, evt;
- // setup checks for custom event system
- if ( this.data.events[ type ] && Popcorn.sizeOf( this.data.events[ type ] ) ) {
- eventInterface = Popcorn.events.getInterface( type );
- if ( eventInterface ) {
- evt = document.createEvent( eventInterface );
- evt.initEvent( type, true, true, global, 1 );
- this.media.dispatchEvent( evt );
- return this;
- }
- // Custom events
- Popcorn.forEach( this.data.events[ type ], function( obj, key ) {
- obj.call( this, data );
- }, this );
- }
- return this;
- },
- listen: function( type, fn ) {
- var self = this,
- hasEvents = true,
- eventHook = Popcorn.events.hooks[ type ],
- origType = type,
- tmp;
- if ( !this.data.events[ type ] ) {
- this.data.events[ type ] = {};
- hasEvents = false;
- }
- // Check and setup event hooks
- if ( eventHook ) {
- // Execute hook add method if defined
- if ( eventHook.add ) {
- eventHook.add.call( this, {}, fn );
- }
- // Reassign event type to our piggyback event type if defined
- if ( eventHook.bind ) {
- type = eventHook.bind;
- }
- // Reassign handler if defined
- if ( eventHook.handler ) {
- tmp = fn;
- fn = function wrapper( event ) {
- eventHook.handler.call( self, event, tmp );
- };
- }
- // assume the piggy back event is registered
- hasEvents = true;
- // Setup event registry entry
- if ( !this.data.events[ type ] ) {
- this.data.events[ type ] = {};
- // Toggle if the previous assumption was untrue
- hasEvents = false;
- }
- }
- // Register event and handler
- this.data.events[ type ][ fn.name || ( fn.toString() + Popcorn.guid() ) ] = fn;
- // only attach one event of any type
- if ( !hasEvents && Popcorn.events.all.indexOf( type ) > -1 ) {
- this.media.addEventListener( type, function( event ) {
- Popcorn.forEach( self.data.events[ type ], function( obj, key ) {
- if ( typeof obj === "function" ) {
- obj.call( self, event );
- }
- });
- }, false);
- }
- return this;
- },
- unlisten: function( type, fn ) {
- if ( this.data.events[ type ] && this.data.events[ type ][ fn ] ) {
- delete this.data.events[ type ][ fn ];
- return this;
- }
- this.data.events[ type ] = null;
- return this;
- }
- },
- hooks: {
- canplayall: {
- bind: "canplaythrough",
- add: function( event, callback ) {
- var state = false;
- if ( this.media.readyState ) {
- callback.call( this, event );
- state = true;
- }
- this.data.hooks.canplayall = {
- fired: state
- };
- },
- // declare special handling instructions
- handler: function canplayall( event, callback ) {
- if ( !this.data.hooks.canplayall.fired ) {
- // trigger original user callback once
- callback.call( this, event );
- this.data.hooks.canplayall.fired = true;
- }
- }
- }
- }
- };
- // Extend Popcorn.events.fns (listen, unlisten, trigger) to all Popcorn instances
- // Extend aliases (on, off, emit)
- Popcorn.forEach( [ [ "trigger", "emit" ], [ "listen", "on" ], [ "unlisten", "off" ] ], function( key ) {
- Popcorn.p[ key[ 0 ] ] = Popcorn.p[ key[ 1 ] ] = Popcorn.events.fn[ key[ 0 ] ];
- });
- // Internal Only - Adds track events to the instance object
- Popcorn.addTrackEvent = function( obj, track ) {
- var trackEvent, isUpdate, eventType;
- // Do a lookup for existing trackevents with this id
- if ( track.id ) {
- trackEvent = obj.getTrackEvent( track.id );
- }
- // If a track event by this id currently exists, modify it
- if ( trackEvent ) {
- isUpdate = true;
- // Create a new object with the existing trackEvent
- // Extend with new track properties
- track = Popcorn.extend( {}, trackEvent, track );
- // Remove the existing track from the instance
- obj.removeTrackEvent( track.id );
- }
- // Determine if this track has default options set for it
- // If so, apply them to the track object
- if ( track && track._natives && track._natives.type &&
- ( obj.options.defaults && obj.options.defaults[ track._natives.type ] ) ) {
- track = Popcorn.extend( {}, obj.options.defaults[ track._natives.type ], track );
- }
- if ( track._natives ) {
- // Supports user defined track event id
- track._id = track.id || track._id || Popcorn.guid( track._natives.type );
- // Push track event ids into the history
- obj.data.history.push( track._id );
- }
- track.start = Popcorn.util.toSeconds( track.start, obj.options.framerate );
- track.end = Popcorn.util.toSeconds( track.end, obj.options.framerate );
- // Store this definition in an array sorted by times
- var byStart = obj.data.trackEvents.byStart,
- byEnd = obj.data.trackEvents.byEnd,
- startIndex, endIndex;
- for ( startIndex = byStart.length - 1; startIndex >= 0; startIndex-- ) {
- if ( track.start >= byStart[ startIndex ].start ) {
- byStart.splice( startIndex + 1, 0, track );
- break;
- }
- }
- for ( endIndex = byEnd.length - 1; endIndex >= 0; endIndex-- ) {
- if ( track.end > byEnd[ endIndex ].end ) {
- byEnd.splice( endIndex + 1, 0, track );
- break;
- }
- }
- // Display track event immediately if it's enabled and current
- if ( track.end > obj.media.currentTime &&
- track.start <= obj.media.currentTime ) {
- track._running = true;
- obj.data.running[ track._natives.type ].push( track );
- if ( !obj.data.disabled[ track._natives.type ] ) {
- track._natives.start.call( obj, null, track );
- }
- }
- // update startIndex and endIndex
- if ( startIndex <= obj.data.trackEvents.startIndex &&
- track.start <= obj.data.trackEvents.previousUpdateTime ) {
- obj.data.trackEvents.startIndex++;
- }
- if ( endIndex <= obj.data.trackEvents.endIndex &&
- track.end < obj.data.trackEvents.previousUpdateTime ) {
- obj.data.trackEvents.endIndex++;
- }
- this.timeUpdate( obj, null, true );
- // Store references to user added trackevents in ref table
- if ( track._id ) {
- Popcorn.addTrackEvent.ref( obj, track );
- }
- // If the call to addTrackEvent was an update/modify call, fire an event
- if ( isUpdate ) {
- // Determine appropriate event type to trigger
- // they are identical in function, but the naming
- // adds some level of intuition for the end developer
- // to rely on
- if ( track._natives.type === "cue" ) {
- eventType = "cuechange";
- } else {
- eventType = "trackchange";
- }
- // Fire an event with change information
- obj.emit( eventType, {
- id: track.id,
- previousValue: {
- time: trackEvent.start,
- fn: trackEvent._natives.start
- },
- currentValue: {
- time: track.start,
- fn: track._natives.start
- }
- });
- }
- };
- // Internal Only - Adds track event references to the instance object's trackRefs hash table
- Popcorn.addTrackEvent.ref = function( obj, track ) {
- obj.data.trackRefs[ track._id ] = track;
- return obj;
- };
- Popcorn.removeTrackEvent = function( obj, removeId ) {
- var start, end, animate,
- historyLen = obj.data.history.length,
- length = obj.data.trackEvents.byStart.length,
- index = 0,
- indexWasAt = 0,
- byStart = [],
- byEnd = [],
- animating = [],
- history = [];
- while ( --length > -1 ) {
- start = obj.data.trackEvents.byStart[ index ];
- end = obj.data.trackEvents.byEnd[ index ];
- // Padding events will not have _id properties.
- // These should be safely pushed onto the front and back of the
- // track event array
- if ( !start._id ) {
- byStart.push( start );
- byEnd.push( end );
- }
- // Filter for user track events (vs system track events)
- if ( start._id ) {
- // If not a matching start event for removal
- if ( start._id !== removeId ) {
- byStart.push( start );
- }
- // If not a matching end event for removal
- if ( end._id !== removeId ) {
- byEnd.push( end );
- }
- // If the _id is matched, capture the current index
- if ( start._id === removeId ) {
- indexWasAt = index;
- // If a _teardown function was defined,
- // enforce for track event removals
- if ( start._natives._teardown ) {
- start._natives._teardown.call( obj, start );
- }
- }
- }
- // Increment the track index
- index++;
- }
- // Reset length to be used by the condition below to determine
- // if animating track events should also be filtered for removal.
- // Reset index below to be used by the reverse while as an
- // incrementing counter
- length = obj.data.trackEvents.animating.length;
- index = 0;
- if ( length ) {
- while ( --length > -1 ) {
- animate = obj.data.trackEvents.animating[ index ];
- // Padding events will not have _id properties.
- // These should be safely pushed onto the front and back of the
- // track event array
- if ( !animate._id ) {
- animating.push( animate );
- }
- // If not a matching animate event for removal
- if ( animate._id && animate._id !== removeId ) {
- animating.push( animate );
- }
- // Increment the track index
- index++;
- }
- }
- // Update
- if ( indexWasAt <= obj.data.trackEvents.startIndex ) {
- obj.data.trackEvents.startIndex--;
- }
- if ( indexWasAt <= obj.data.trackEvents.endIndex ) {
- obj.data.trackEvents.endIndex--;
- }
- obj.data.trackEvents.byStart = byStart;
- obj.data.trackEvents.byEnd = byEnd;
- obj.data.trackEvents.animating = animating;
- for ( var i = 0; i < historyLen; i++ ) {
- if ( obj.data.history[ i ] !== removeId ) {
- history.push( obj.data.history[ i ] );
- }
- }
- // Update ordered history array
- obj.data.history = history;
- // Update track event references
- Popcorn.removeTrackEvent.ref( obj, removeId );
- };
- // Internal Only - Removes track event references from instance object's trackRefs hash table
- Popcorn.removeTrackEvent.ref = function( obj, removeId ) {
- delete obj.data.trackRefs[ removeId ];
- return obj;
- };
- // Return an array of track events bound to this instance object
- Popcorn.getTrackEvents = function( obj ) {
- var trackevents = [],
- refs = obj.data.trackEvents.byStart,
- length = refs.length,
- idx = 0,
- ref;
- for ( ; idx < length; idx++ ) {
- ref = refs[ idx ];
- // Return only user attributed track event references
- if ( ref._id ) {
- trackevents.push( ref );
- }
- }
- return trackevents;
- };
- // Internal Only - Returns an instance object's trackRefs hash table
- Popcorn.getTrackEvents.ref = function( obj ) {
- return obj.data.trackRefs;
- };
- // Return a single track event bound to this instance object
- Popcorn.getTrackEvent = function( obj, trackId ) {
- return obj.data.trackRefs[ trackId ];
- };
- // Internal Only - Returns an instance object's track reference by track id
- Popcorn.getTrackEvent.ref = function( obj, trackId ) {
- return obj.data.trackRefs[ trackId ];
- };
- Popcorn.getLastTrackEventId = function( obj ) {
- return obj.data.history[ obj.data.history.length - 1 ];
- };
- Popcorn.timeUpdate = function( obj, event ) {
- var currentTime = obj.media.currentTime,
- previousTime = obj.data.trackEvents.previousUpdateTime,
- tracks = obj.data.trackEvents,
- end = tracks.endIndex,
- start = tracks.startIndex,
- byStartLen = tracks.byStart.length,
- byEndLen = tracks.byEnd.length,
- registryByName = Popcorn.registryByName,
- trackstart = "trackstart",
- trackend = "trackend",
- byEnd, byStart, byAnimate, natives, type, runningPlugins;
- // Playbar advancing
- if ( previousTime <= currentTime ) {
- while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end <= currentTime ) {
- byEnd = tracks.byEnd[ end ];
- natives = byEnd._natives;
- type = natives && natives.type;
- // If plugin does not exist on this instance, remove it
- if ( !natives ||
- ( !!registryByName[ type ] ||
- !!obj[ type ] ) ) {
- if ( byEnd._running === true ) {
- byEnd._running = false;
- runningPlugins = obj.data.running[ type ];
- runningPlugins.splice( runningPlugins.indexOf( byEnd ), 1 );
- if ( !obj.data.disabled[ type ] ) {
- natives.end.call( obj, event, byEnd );
- obj.emit( trackend,
- Popcorn.extend({}, byEnd, {
- plugin: type,
- type: trackend
- })
- );
- }
- }
- end++;
- } else {
- // remove track event
- Popcorn.removeTrackEvent( obj, byEnd._id );
- return;
- }
- }
- while ( tracks.byStart[ start ] && tracks.byStart[ start ].start <= currentTime ) {
- byStart = tracks.byStart[ start ];
- natives = byStart._natives;
- type = natives && natives.type;
- // If plugin does not exist on this instance, remove it
- if ( !natives ||
- ( !!registryByName[ type ] ||
- !!obj[ type ] ) ) {
- if ( byStart.end > currentTime &&
- byStart._running === false ) {
- byStart._running = true;
- obj.data.running[ type ].push( byStart );
- if ( !obj.data.disabled[ type ] ) {
- natives.start.call( obj, event, byStart );
- obj.emit( trackstart,
- Popcorn.extend({}, byStart, {
- plugin: type,
- type: trackstart
- })
- );
- }
- }
- start++;
- } else {
- // remove track event
- Popcorn.removeTrackEvent( obj, byStart._id );
- return;
- }
- }
- // Playbar receding
- } else if ( previousTime > currentTime ) {
- while ( tracks.byStart[ start ] && tracks.byStart[ start ].start > currentTime ) {
- byStart = tracks.byStart[ start ];
- natives = byStart._natives;
- type = natives && natives.type;
- // if plugin does not exist on this instance, remove it
- if ( !natives ||
- ( !!registryByName[ type ] ||
- !!obj[ type ] ) ) {
- if ( byStart._running === true ) {
- byStart._running = false;
- runningPlugins = obj.data.running[ type ];
- runningPlugins.splice( runningPlugins.indexOf( byStart ), 1 );
- if ( !obj.data.disabled[ type ] ) {
- natives.end.call( obj, event, byStart );
- obj.emit( trackend,
- Popcorn.extend({}, byStart, {
- plugin: type,
- type: trackend
- })
- );
- }
- }
- start--;
- } else {
- // remove track event
- Popcorn.removeTrackEvent( obj, byStart._id );
- return;
- }
- }
- while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end > currentTime ) {
- byEnd = tracks.byEnd[ end ];
- natives = byEnd._natives;
- type = natives && natives.type;
- // if plugin does not exist on this instance, remove it
- if ( !natives ||
- ( !!registryByName[ type ] ||
- !!obj[ type ] ) ) {
- if ( byEnd.start <= currentTime &&
- byEnd._running === false ) {
- byEnd._running = true;
- obj.data.running[ type ].push( byEnd );
- if ( !obj.data.disabled[ type ] ) {
- natives.start.call( obj, event, byEnd );
- obj.emit( trackstart,
- Popcorn.extend({}, byEnd, {
- plugin: type,
- type: trackstart
- })
- );
- }
- }
- end--;
- } else {
- // remove track event
- Popcorn.removeTrackEvent( obj, byEnd._id );
- return;
- }
- }
- }
- tracks.endIndex = end;
- tracks.startIndex = start;
- tracks.previousUpdateTime = currentTime;
- //enforce index integrity if trackRemoved
- tracks.byStart.length < byStartLen && tracks.startIndex--;
- tracks.byEnd.length < byEndLen && tracks.endIndex--;
- };
- // Map and Extend TrackEvent functions to all Popcorn instances
- Popcorn.extend( Popcorn.p, {
- getTrackEvents: function() {
- return Popcorn.getTrackEvents.call( null, this );
- },
- getTrackEvent: function( id ) {
- return Popcorn.getTrackEvent.call( null, this, id );
- },
- getLastTrackEventId: function() {
- return Popcorn.getLastTrackEventId.call( null, this );
- },
- removeTrackEvent: function( id ) {
- Popcorn.removeTrackEvent.call( null, this, id );
- return this;
- },
- removePlugin: function( name ) {
- Popcorn.removePlugin.call( null, this, name );
- return this;
- },
- timeUpdate: function( event ) {
- Popcorn.timeUpdate.call( null, this, event );
- return this;
- },
- destroy: function() {
- Popcorn.destroy.call( null, this );
- return this;
- }
- });
- // Plugin manifests
- Popcorn.manifest = {};
- // Plugins are registered
- Popcorn.registry = [];
- Popcorn.registryByName = {};
- // An interface for extending Popcorn
- // with plugin functionality
- Popcorn.plugin = function( name, definition, manifest ) {
- if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
- Popcorn.error( "'" + name + "' is a protected function name" );
- return;
- }
- // Provides some sugar, but ultimately extends
- // the definition into Popcorn.p
- var reserved = [ "start", "end" ],
- plugin = {},
- setup,
- isfn = typeof definition === "function",
- methods = [ "_setup", "_teardown", "start", "end", "frame" ];
- // combines calls of two function calls into one
- var combineFn = function( first, second ) {
- first = first || Popcorn.nop;
- second = second || Popcorn.nop;
- return function() {
- first.apply( this, arguments );
- second.apply( this, arguments );
- };
- };
- // If `manifest` arg is undefined, check for manifest within the `definition` object
- // If no `definition.manifest`, an empty object is a sufficient fallback
- Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {};
- // apply safe, and empty default functions
- methods.forEach(function( method ) {
- definition[ method ] = safeTry( definition[ method ] || Popcorn.nop, name );
- });
- var pluginFn = function( setup, options ) {
- if ( !options ) {
- return this;
- }
- // When the "ranges" property is set and its value is an array, short-circuit
- // the pluginFn definition to recall itself with an options object generated from
- // each range object in the ranges array. (eg. { start: 15, end: 16 } )
- if ( options.ranges && Popcorn.isArray(options.ranges) ) {
- Popcorn.forEach( options.ranges, function( range ) {
- // Create a fresh object, extend with current options
- // and start/end range object's properties
- // Works with in/out as well.
- var opts = Popcorn.extend( {}, options, range );
- // Remove the ranges property to prevent infinitely
- // entering this condition
- delete opts.ranges;
- // Call the plugin with the newly created opts object
- this[ name ]( opts );
- }, this);
- // Return the Popcorn instance to avoid creating an empty track event
- return this;
- }
- // Storing the plugin natives
- var natives = options._natives = {},
- compose = "",
- originalOpts, manifestOpts;
- Popcorn.extend( natives, setup );
- options._natives.type = name;
- options._running = false;
- natives.start = natives.start || natives[ "in" ];
- natives.end = natives.end || natives[ "out" ];
- if ( options.once ) {
- natives.end = combineFn( natives.end, function() {
- this.removeTrackEvent( options._id );
- });
- }
- // extend teardown to always call end if running
- natives._teardown = combineFn(function() {
- var args = slice.call( arguments ),
- runningPlugins = this.data.running[ natives.type ];
- // end function signature is not the same as teardown,
- // put null on the front of arguments for the event parameter
- args.unshift( null );
- // only call end if event is running
- args[ 1 ]._running &&
- runningPlugins.splice( runningPlugins.indexOf( options ), 1 ) &&
- natives.end.apply( this, args );
- }, natives._teardown );
- // default to an empty string if no effect exists
- // split string into an array of effects
- options.compose = options.compose && options.compose.split( " " ) || [];
- options.effect = options.effect && options.effect.split( " " ) || [];
- // join the two arrays together
- options.compose = options.compose.concat( options.effect );
- options.compose.forEach(function( composeOption ) {
- // if the requested compose is garbage, throw it away
- compose = Popcorn.compositions[ composeOption ] || {};
- // extends previous functions with compose function
- methods.forEach(function( method ) {
- natives[ method ] = combineFn( natives[ method ], compose[ method ] );
- });
- });
- // Ensure a manifest object, an empty object is a sufficient fallback
- options._natives.manifest = manifest;
- // Checks for expected properties
- if ( !( "start" in options ) ) {
- options.start = options[ "in" ] || 0;
- }
- if ( !options.end && options.end !== 0 ) {
- options.end = options[ "out" ] || Number.MAX_VALUE;
- }
- // Use hasOwn to detect non-inherited toString, since all
- // objects will receive a toString - its otherwise undetectable
- if ( !hasOwn.call( options, "toString" ) ) {
- options.toString = function() {
- var props = [
- "start: " + options.start,
- "end: " + options.end,
- "id: " + (options.id || options._id)
- ];
- // Matches null and undefined, allows: false, 0, "" and truthy
- if ( options.target != null ) {
- props.push( "target: " + options.target );
- }
- return name + " ( " + props.join(", ") + " )";
- };
- }
- // Resolves 239, 241, 242
- if ( !options.target ) {
- // Sometimes the manifest may be missing entirely
- // or it has an options object that doesn't have a `target` property
- manifestOpts = "options" in manifest && manifest.options;
- options.target = manifestOpts && "target" in manifestOpts && manifestOpts.target;
- }
- if ( options._natives ) {
- // ensure an initial id is there before setup is called
- options._id = Popcorn.guid( options._natives.type );
- }
- // Trigger _setup method if exists
- options._natives._setup && options._natives._setup.call( this, options );
- // Create new track event for this instance
- Popcorn.addTrackEvent( this, options );
- // Future support for plugin event definitions
- // for all of the native events
- Popcorn.forEach( setup, function( callback, type ) {
- if ( type !== "type" ) {
- if ( reserved.indexOf( type ) === -1 ) {
- this.on( type, callback );
- }
- }
- }, this );
- return this;
- };
- // Extend Popcorn.p with new named definition
- // Assign new named definition
- Popcorn.p[ name ] = plugin[ name ] = function( id, options ) {
- var length = arguments.length,
- trackEvent, defaults, mergedSetupOpts;
- // Shift arguments based on use case
- //
- // Back compat for:
- // p.plugin( options );
- if ( id && !options ) {
- options = id;
- id = null;
- } else {
- // Get the trackEvent that matches the given id.
- trackEvent = this.getTrackEvent( id );
- // If the track event does not exist, ensure that the options
- // object has a proper id
- if ( !trackEvent ) {
- options.id = id;
- // If the track event does exist, merge the updated properties
- } else {
- options = Popcorn.extend( {}, trackEvent, options );
- Popcorn.addTrackEvent( this, options );
- return this;
- }
- }
- this.data.running[ name ] = this.data.running[ name ] || [];
- // Merge with defaults if they exist, make sure per call is prioritized
- defaults = ( this.options.defaults && this.options.defaults[ name ] ) || {};
- mergedSetupOpts = Popcorn.extend( {}, defaults, options );
- return pluginFn.call( this, isfn ? definition.call( this, mergedSetupOpts ) : definition,
- mergedSetupOpts );
- };
- // if the manifest parameter exists we should extend it onto the definition object
- // so that it shows up when calling Popcorn.registry and Popcorn.registryByName
- if ( manifest ) {
- Popcorn.extend( definition, {
- manifest: manifest
- });
- }
- // Push into the registry
- var entry = {
- fn: plugin[ name ],
- definition: definition,
- base: definition,
- parents: [],
- name: name
- };
- Popcorn.registry.push(
- Popcorn.extend( plugin, entry, {
- type: name
- })
- );
- Popcorn.registryByName[ name ] = entry;
- return plugin;
- };
- // Storage for plugin function errors
- Popcorn.plugin.errors = [];
- // Returns wrapped plugin function
- function safeTry( fn, pluginName ) {
- return function() {
- // When Popcorn.plugin.debug is true, do not suppress errors
- if ( Popcorn.plugin.debug ) {
- return fn.apply( this, arguments );
- }
- try {
- return fn.apply( this, arguments );
- } catch…
Large files files are truncated, but you can click here to view the full file