(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 ); }, Abstract = { // [[Put]] props from dictionary onto |this| // MUST BE CALLED FROM WITHIN A CONSTRUCTOR: // Abstract.put.call( this, dictionary ); put: function( dictionary ) { // For each own property of src, let key be the property key // and desc be the property descriptor of the property. for ( var key in dictionary ) { if ( dictionary.hasOwnProperty( key ) ) { this[ key ] = dictionary[ key ]; } } } }, // 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 = "@VERSION"; // 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 = Popcorn.extend( {}, 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: new TrackEvents( this ) }; // 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( "loadedmetadata", 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.isDestroyed ) { self.data.durationChange = function() { var newDuration = self.media.duration, newDurationPlus = newDuration + 1, byStart = self.data.trackEvents.byStart, byEnd = self.data.trackEvents.byEnd; // Remove old padding events byStart.pop(); byEnd.pop(); // Remove any internal tracking of events that have end times greater than duration // otherwise their end events will never be hit. for ( var k = byEnd.length - 1; k > 0; k-- ) { if ( byEnd[ k ].end > newDuration ) { self.removeTrackEvent( byEnd[ k ]._id ); } } // Remove any internal tracking of events that have end times greater than duration // otherwise their end events will never be hit. for ( var i = 0; i < byStart.length; i++ ) { if ( byStart[ i ].end > newDuration ) { self.removeTrackEvent( byStart[ i ]._id ); } } // References to byEnd/byStart are reset, so accessing it this way is // forced upon us. self.data.trackEvents.byEnd.push({ start: newDurationPlus, end: newDurationPlus }); self.data.trackEvents.byStart.push({ start: newDurationPlus, end: newDurationPlus }); }; // Listen for duration changes and adjust internal tracking of event timings self.media.addEventListener( "durationchange", self.data.durationChange, false ); } 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 ); } } }; self.media.addEventListener( "error", function() { self.error = self.media.error; }, false ); // http://www.whatwg.org/specs/web-apps/current-work/#dom-media-readystate // // If media is in readyState (rS) >= 1, we know the media's duration, // which is required before running the isReady function. // If rS is 0, attach a listener for "loadedmetadata", // ( Which indicates that the media has moved from rS 0 to 1 ) // // This has been changed from a check for rS 2 because // in certain conditions, Firefox can enter this code after dropping // to rS 1 from a higher state such as 2 or 3. This caused a "loadeddata" // listener to be attached to the media object, an event that had // already triggered and would not trigger again. This left Popcorn with an // instance that could never start a timeUpdate loop. if ( self.media.readyState >= 1 ) { isReady(); } else { self.media.addEventListener( "loadedmetadata", 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 ) { if ( !elem.parentNode ) { return null; } 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 ] ) { return; } instance.data.disabled[ plugin ] = true; if ( plugin in Popcorn.registryByName && instance.data.running[ plugin ] ) { 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 ); instance.emit( "trackend", Popcorn.extend({}, event, { plugin: event.type, type: "trackend" }) ); } } return instance; }, enable: function( instance, plugin ) { if ( !instance.data.disabled[ plugin ] ) { return; } instance.data.disabled[ plugin ] = false; if ( plugin in Popcorn.registryByName && instance.data.running[ plugin ] ) { 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 ); instance.emit( "trackstart", Popcorn.extend({}, event, { plugin: event.type, type: "trackstart", track: 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; } Popcorn.instances.splice( Popcorn.instances.indexOf( instance ), 1 ); } }); // 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, eventType = "trackadded", trackEvent, sec, options; // 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 ) { // remove existing cue so a new one can be added via trackEvents.add this.data.trackEvents.remove( id ); TrackEvent.end( this, trackEvent ); // Update track event references Popcorn.removeTrackEvent.ref( this, id ); eventType = "cuechange"; // 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 ); // p.cue( "b", 11, function() {} ); if ( typeof time === "number" ) { fn = fn || Popcorn.nop(); } // p.cue( "c", function() {}); if ( typeof time === "function" ) { fn = time; time = -1; } } } } } options = { id: id, start: time, end: time + 1, _running: false, _natives: { start: fn || Popcorn.nop, end: Popcorn.nop, type: "cue" } }; if ( trackEvent ) { options = Popcorn.extend( trackEvent, options ); } if ( eventType === "cuechange" ) { // Supports user defined track event id options._id = options.id || options._id || Popcorn.guid( options._natives.type ); this.data.trackEvents.add( options ); TrackEvent.start( this, options ); this.timeUpdate( this, null, true ); // Store references to user added trackevents in ref table Popcorn.addTrackEvent.ref( this, options ); this.emit( eventType, Popcorn.extend({}, options, { id: id, type: eventType, previousValue: { time: trackEvent.start, fn: trackEvent._natives.start }, currentValue: { time: time, fn: fn || Popcorn.nop }, track: trackEvent })); } else { // Creating a one second track event with an empty end Popcorn.addTrackEvent( this, options ); } 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, clonedEvents, events = this.data.events[ type ]; // setup checks for custom event system if ( events ) { eventInterface = Popcorn.events.getInterface( type ); if ( eventInterface ) { evt = document.createEvent( eventInterface ); evt.initEvent( type, true, true, global, 1 ); this.media.dispatchEvent( evt ); return this; } // clone events in case callbacks remove callbacks themselves clonedEvents = events.slice(); // iterate through all callbacks while ( clonedEvents.length ) { clonedEvents.shift().call( this, data ); } } return this; }, listen: function( type, fn ) { var self = this, hasEvents = true, eventHook = Popcorn.events.hooks[ type ], origType = type, clonedEvents, tmp; if ( typeof fn !== "function" ) { throw new Error( "Popcorn.js Error: Listener is not a function" ); } // Setup event registry entry if ( !this.data.events[ type ] ) { this.data.events[ type ] = []; // Toggle if the previous assumption was untrue 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 ].push( fn ); // only attach one event of any type if ( !hasEvents && Popcorn.events.all.indexOf( type ) > -1 ) { this.media.addEventListener( type, function( event ) { if ( self.data.events[ type ] ) { // clone events in case callbacks remove callbacks themselves clonedEvents = self.data.events[ type ].slice(); // iterate through all callbacks while ( clonedEvents.length ) { clonedEvents.shift().call( self, event ); } } }, false ); } return this; }, unlisten: function( type, fn ) { var ind, events = this.data.events[ type ]; if ( !events ) { return; // no listeners = nothing to do } if ( typeof fn === "string" ) { // legacy support for string-based removal -- not recommended for ( var i = 0; i < events.length; i++ ) { if ( events[ i ].name === fn ) { // decrement i because array length just got smaller events.splice( i--, 1 ); } } return this; } else if ( typeof fn === "function" ) { while( ind !== -1 ) { ind = events.indexOf( fn ); if ( ind !== -1 ) { events.splice( ind, 1 ); } } return this; } // if we got to this point, we are deleting all functions of this type this.data.events[ type ] = null; return this; } }, hooks: { canplayall: { bind: "canplaythrough", add: function( event, callback ) { var state = false; if ( this.media.readyState ) { // always call canplayall asynchronously setTimeout(function() { callback.call( this, event ); }.bind(this), 0 ); 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 - construct simple "TrackEvent" // data type objects function TrackEvent( track ) { Abstract.put.call( this, track ); } // Determine if a TrackEvent's "start" and "trackstart" must be called. TrackEvent.start = function( instance, track ) { if ( track.end > instance.media.currentTime && track.start <= instance.media.currentTime && !track._running ) { track._running = true; instance.data.running[ track._natives.type ].push( track ); if ( !instance.data.disabled[ track._natives.type ] ) { track._natives.start.call( instance, null, track ); instance.emit( "trackstart", Popcorn.extend( {}, track, { plugin: track._natives.type, type: "trackstart", track: track }) ); } } }; // Determine if a TrackEvent's "end" and "trackend" must be called. TrackEvent.end = function( instance, track ) { var runningPlugins; if ( ( track.end <= instance.media.currentTime || track.start > instance.media.currentTime ) && track._running ) { runningPlugins = instance.data.running[ track._natives.type ]; track._running = false; runningPlugins.splice( runningPlugins.indexOf( track ), 1 ); if ( !instance.data.disabled[ track._natives.type ] ) { track._natives.end.call( instance, null, track ); instance.emit( "trackend", Popcorn.extend( {}, track, { plugin: track._natives.type, type: "trackend", track: track }) ); } } }; // Internal Only - construct "TrackEvents" // data type objects that are used by the Popcorn // instance, stored at p.data.trackEvents function TrackEvents( parent ) { this.parent = parent; this.byStart = [{ start: -1, end: -1 }]; this.byEnd = [{ start: -1, end: -1 }]; this.animating = []; this.startIndex = 0; this.endIndex = 0; this.previousUpdateTime = -1; this.count = 1; } function isMatch( obj, key, value ) { return obj[ key ] && obj[ key ] === value; } TrackEvents.prototype.where = function( params ) { return ( this.parent.getTrackEvents() || [] ).filter(function( event ) { var key, value; // If no explicit params, match all TrackEvents if ( !params ) { return true; } // Filter keys in params against both the top level properties // and the _natives properties for ( key in params ) { value = params[ key ]; if ( isMatch( event, key, value ) || isMatch( event._natives, key, value ) ) { return true; } } return false; }); }; TrackEvents.prototype.add = function( track ) { // Store this definition in an array sorted by times var byStart = this.byStart, byEnd = this.byEnd, startIndex, endIndex; // Push track event ids into the history if ( track && track._id ) { this.parent.data.history.push( track._id ); } track.start = Popcorn.util.toSeconds( track.start, this.parent.options.framerate ); track.end = Popcorn.util.toSeconds( track.end, this.parent.options.framerate ); 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; } } // update startIndex and endIndex if ( startIndex <= this.parent.data.trackEvents.startIndex && track.start <= this.parent.data.trackEvents.previousUpdateTime ) { this.parent.data.trackEvents.startIndex++; } if ( endIndex <= this.parent.data.trackEvents.endIndex && track.end < this.parent.data.trackEvents.previousUpdateTime ) { this.parent.data.trackEvents.endIndex++; } this.count++; }; TrackEvents.prototype.remove = function( removeId, state ) { if ( removeId instanceof TrackEvent ) { removeId = removeId.id; } if ( typeof removeId === "object" ) { // Filter by key=val and remove all matching TrackEvents this.where( removeId ).forEach(function( event ) { // |this| refers to the calling Popcorn "parent" instance this.removeTrackEvent( event._id ); }, this.parent ); return this; } var start, end, animate, historyLen, track, length = this.byStart.length, index = 0, indexWasAt = 0, byStart = [], byEnd = [], animating = [], history = [], comparable = {}; state = state || {}; while ( --length > -1 ) { start = this.byStart[ index ]; end = this.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; // cache the track event being removed track = 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 = this.animating.length; index = 0; if ( length ) { while ( --length > -1 ) { animate = this.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 <= this.startIndex ) { this.startIndex--; } if ( indexWasAt <= this.endIndex ) { this.endIndex--; } this.byStart = byStart; this.byEnd = byEnd; this.animating = animating; this.count--; historyLen = this.parent.data.history.length; for ( var i = 0; i < historyLen; i++ ) { if ( this.parent.data.history[ i ] !== removeId ) { history.push( this.parent.data.history[ i ] ); } } // Update ordered history array this.parent.data.history = history; }; // Helper function used to retrieve old values of properties that // are provided for update. function getPreviousProperties( oldOptions, newOptions ) { var matchProps = {}; for ( var prop in oldOptions ) { if ( hasOwn.call( newOptions, prop ) && hasOwn.call( oldOptions, prop ) ) { matchProps[ prop ] = oldOptions[ prop ]; } } return matchProps; } // Internal Only - Adds track events to the instance object Popcorn.addTrackEvent = function( obj, track ) { var temp; if ( track instanceof TrackEvent ) { return; } track = new TrackEvent( track ); // 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 ] ) ) { // To ensure that the TrackEvent Invariant Policy is enforced, // First, copy the properties of the newly created track event event // to a temporary holder temp = Popcorn.extend( {}, track ); // Next, copy the default onto the newly created trackevent, followed by the // temporary holder. Popcorn.extend( track, obj.options.defaults[ track._natives.type ], temp ); } if ( track._natives ) { // Supports user defined track event id track._id = track.id || track._id || Popcorn.guid( track._natives.type ); // Trigger _setup method if exists if ( track._natives._setup ) { track._natives._setup.call( obj, track ); obj.emit( "tracksetup", Popcorn.extend( {}, track, { plugin: track._natives.type, type: "tracksetup", track: track })); } } obj.data.trackEvents.add( track ); TrackEvent.start( obj, track ); this.timeUpdate( obj, null, true ); // Store references to user added trackevents in ref table if ( track._id ) { Popcorn.addTrackEvent.ref( obj, track ); } obj.emit( "trackadded", Popcorn.extend({}, track, track._natives ? { plugin: track._natives.type } : {}, { type: "trackadded", track: track })); }; // 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 track = obj.getTrackEvent( removeId ); if ( !track ) { return; } // If a _teardown function was defined, // enforce for track event removals if ( track._natives._teardown ) { track._natives._teardown.call( obj, track ); } obj.data.trackEvents.remove( removeId ); // Update track event references Popcorn.removeTrackEvent.ref( obj, removeId ); if ( track._natives ) { // Fire a trackremoved event obj.emit( "trackremoved", Popcorn.extend({}, track, { plugin: track._natives.type, type: "trackremoved", track: track })); } }; // 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, track: byEnd }) ); } } 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, track: byStart }) ); } } 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, track: byStart }) ); } } 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, track: byEnd }) ); } } 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 isfn = typeof definition === "function", blacklist = [ "start", "end", "type", "manifest" ], methods = [ "_setup", "_teardown", "start", "end", "frame" ], plugin = {}, setup; // 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 = options._natives.plugin = 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 ); args[ 1 ]._running = false; this.emit( "trackend", Popcorn.extend( {}, options, { plugin: natives.type, type: "trackend", track: Popcorn.getTrackEvent( this, options.id || options._id ) }) ); }, natives._teardown ); // extend teardown to always trigger trackteardown after teardown natives._teardown = combineFn( natives._teardown, function() { this.emit( "trackteardown", Popcorn.extend( {}, options, { plugin: name, type: "trackteardown", track: Popcorn.getTrackEvent( this, options.id || options._id ) })); }); // default to an empty string if no effect exists // split string into an array of effects options.compose = options.compose || []; if ( typeof options.compose === "string" ) { options.compose = options.compose.split( " " ); } options.effect = options.effect || []; if ( typeof options.effect === "string" ) { 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._id && options._natives ) { // ensure an initial id is there before setup is called options._id = Popcorn.guid( options._natives.type ); } if ( options instanceof TrackEvent ) { if ( options._natives ) { // Supports user defined track event id options._id = options.id || options._id || Popcorn.guid( options._natives.type ); // Trigger _setup method if exists if ( options._natives._setup ) { options._natives._setup.call( this, options ); this.emit( "tracksetup", Popcorn.extend( {}, options, { plugin: options._natives.type, type: "tracksetup", track: options })); } } this.data.trackEvents.add( options ); TrackEvent.start( this, options ); this.timeUpdate( this, null, true ); // Store references to user added trackevents in ref table if ( options._id ) { Popcorn.addTrackEvent.ref( this, options ); } } else { // 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 ) { // Don't attempt to create events for certain properties: // "start", "end", "type", "manifest". Fixes #1365 if ( blacklist.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, previousOpts, newOpts; // 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 { newOpts = options; previousOpts = getPreviousProperties( trackEvent, newOpts ); // Call the plugins defined update method if provided. Allows for // custom defined updating for a track event to be defined by the plugin author if ( trackEvent._natives._update ) { this.data.trackEvents.remove( trackEvent ); // It's safe to say that the intent of Start/End will never change // Update them first before calling update if ( hasOwn.call( options, "start" ) ) { trackEvent.start = options.start; } if ( hasOwn.call( options, "end" ) ) { trackEvent.end = options.end; } TrackEvent.end( this, trackEvent ); if ( isfn ) { definition.call( this, trackEvent ); } trackEvent._natives._update.call( this, trackEvent, options ); this.data.trackEvents.add( trackEvent ); TrackEvent.start( this, trackEvent ); } else { // This branch is taken when there is no explicitly defined // _update method for a plugin. Which will occur either explicitly or // as a result of the plugin definition being a function that _returns_ // a definition object. // // In either case, this path can ONLY be reached for TrackEvents that // already exist. // Directly update the TrackEvent instance. // This supports TrackEvent invariant enforcement. Popcorn.extend( trackEvent, options ); this.data.trackEvents.remove( id ); // If a _teardown function was defined, // enforce for track event removals if ( trackEvent._natives._teardown ) { trackEvent._natives._teardown.call( this, trackEvent ); } // Update track event references Popcorn.removeTrackEvent.ref( this, id ); if ( isfn ) { pluginFn.call( this, definition.call( this, trackEvent ), trackEvent ); } else { // Supports user defined track event id trackEvent._id = trackEvent.id || trackEvent._id || Popcorn.guid( trackEvent._natives.type ); if ( trackEvent._natives && trackEvent._natives._setup ) { trackEvent._natives._setup.call( this, trackEvent ); this.emit( "tracksetup", Popcorn.extend( {}, trackEvent, { plugin: trackEvent._natives.type, type: "tracksetup", track: trackEvent })); } this.data.trackEvents.add( trackEvent ); TrackEvent.start( this, trackEvent ); this.timeUpdate( this, null, true ); // Store references to user added trackevents in ref table Popcorn.addTrackEvent.ref( this, trackEvent ); } // Fire an event with change information this.emit( "trackchange", { id: trackEvent.id, type: "trackchange", previousValue: previousOpts, currentValue: trackEvent, track: trackEvent }); return this; } if ( trackEvent._natives.type !== "cue" ) { // Fire an event with change information this.emit( "trackchange", { id: trackEvent.id, type: "trackchange", previousValue: previousOpts, currentValue: newOpts, track: trackEvent }); } 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 ); pluginFn.call( this, isfn ? definition.call( this, mergedSetupOpts ) : definition, mergedSetupOpts ); return this; }; // 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 ( ex ) { // Push plugin function errors into logging queue Popcorn.plugin.errors.push({ plugin: pluginName, thrown: ex, source: fn.toString() }); // Trigger an error that the instance can listen for // and react to this.emit( "pluginerror", Popcorn.plugin.errors ); } }; } // Debug-mode flag for plugin development // True for Popcorn development versions, false for stable/tagged versions Popcorn.plugin.debug = ( Popcorn.version === "@" + "VERSION" ); // removePlugin( type ) removes all tracks of that from all instances of popcorn // removePlugin( obj, type ) removes all tracks of type from obj, where obj is a single instance of popcorn Popcorn.removePlugin = function( obj, name ) { // Check if we are removing plugin from an instance or from all of Popcorn if ( !name ) { // Fix the order name = obj; obj = Popcorn.p; if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) { Popcorn.error( "'" + name + "' is a protected function name" ); return; } var registryLen = Popcorn.registry.length, registryIdx; // remove plugin reference from registry for ( registryIdx = 0; registryIdx < registryLen; registryIdx++ ) { if ( Popcorn.registry[ registryIdx ].name === name ) { Popcorn.registry.splice( registryIdx, 1 ); delete Popcorn.registryByName[ name ]; delete Popcorn.manifest[ name ]; // delete the plugin delete obj[ name ]; // plugin found and removed, stop checking, we are done return; } } } var byStart = obj.data.trackEvents.byStart, byEnd = obj.data.trackEvents.byEnd, animating = obj.data.trackEvents.animating, idx, sl; // remove all trackEvents for ( idx = 0, sl = byStart.length; idx < sl; idx++ ) { if ( byStart[ idx ] && byStart[ idx ]._natives && byStart[ idx ]._natives.type === name ) { byStart[ idx ]._natives._teardown && byStart[ idx ]._natives._teardown.call( obj, byStart[ idx ] ); byStart.splice( idx, 1 ); // update for loop if something removed, but keep checking idx--; sl--; if ( obj.data.trackEvents.startIndex <= idx ) { obj.data.trackEvents.startIndex--; obj.data.trackEvents.endIndex--; } } // clean any remaining references in the end index // we do this seperate from the above check because they might not be in the same order if ( byEnd[ idx ] && byEnd[ idx ]._natives && byEnd[ idx ]._natives.type === name ) { byEnd.splice( idx, 1 ); } } //remove all animating events for ( idx = 0, sl = animating.length; idx < sl; idx++ ) { if ( animating[ idx ] && animating[ idx ]._natives && animating[ idx ]._natives.type === name ) { animating.splice( idx, 1 ); // update for loop if something removed, but keep checking idx--; sl--; } } }; Popcorn.compositions = {}; // Plugin inheritance Popcorn.compose = function( name, definition, manifest ) { // 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 || {}; // register the effect by name Popcorn.compositions[ name ] = definition; }; Popcorn.plugin.effect = Popcorn.effect = Popcorn.compose; var rnaiveExpr = /^(?:\.|#|\[)/; // Basic DOM utilities and helpers API. See #1037 Popcorn.dom = { debug: false, // Popcorn.dom.find( selector, context ) // // Returns the first element that matches the specified selector // Optionally provide a context element, defaults to `document` // // eg. // Popcorn.dom.find("video") returns the first video element // Popcorn.dom.find("#foo") returns the first element with `id="foo"` // Popcorn.dom.find("foo") returns the first element with `id="foo"` // Note: Popcorn.dom.find("foo") is the only allowed deviation // from valid querySelector selector syntax // // Popcorn.dom.find(".baz") returns the first element with `class="baz"` // Popcorn.dom.find("[preload]") returns the first element with `preload="..."` // ... // See https://developer.mozilla.org/En/DOM/Document.querySelector // // find: function( selector, context ) { var node = null; // Default context is the `document` context = context || document; if ( selector ) { // If the selector does not begin with "#", "." or "[", // it could be either a nodeName or ID w/o "#" if ( !rnaiveExpr.test( selector ) ) { // Try finding an element that matches by ID first node = document.getElementById( selector ); // If a match was found by ID, return the element if ( node !== null ) { return node; } } // Assume no elements have been found yet // Catch any invalid selector syntax errors and bury them. try { node = context.querySelector( selector ); } catch ( e ) { if ( Popcorn.dom.debug ) { throw new Error(e); } } } return node; } }; // Cache references to reused RegExps var rparams = /\?/, // XHR Setup object setup = { ajax: null, url: "", data: "", dataType: "", success: Popcorn.nop, type: "GET", async: true, contentType: "application/x-www-form-urlencoded; charset=UTF-8" }; Popcorn.xhr = function( options ) { var settings; options.dataType = options.dataType && options.dataType.toLowerCase() || null; if ( options.dataType && ( options.dataType === "jsonp" || options.dataType === "script" ) ) { Popcorn.xhr.getJSONP( options.url, options.success, options.dataType === "script" ); return; } // Merge the "setup" defaults and custom "options" // into a new plain object. settings = Popcorn.extend( {}, setup, options ); // Create new XMLHttpRequest object settings.ajax = new XMLHttpRequest(); if ( settings.ajax ) { if ( settings.type === "GET" && settings.data ) { // append query string settings.url += ( rparams.test( settings.url ) ? "&" : "?" ) + settings.data; // Garbage collect and reset settings.data settings.data = null; } // Open the request settings.ajax.open( settings.type, settings.url, settings.async ); // For POST, set the content-type request header if ( settings.type === "POST" ) { settings.ajax.setRequestHeader( "Content-Type", settings.contentType ); } settings.ajax.send( settings.data || null ); return Popcorn.xhr.httpData( settings ); } }; Popcorn.xhr.httpData = function( settings ) { var data, json = null, parser, xml = null; settings.ajax.onreadystatechange = function() { if ( settings.ajax.readyState === 4 ) { try { json = JSON.parse( settings.ajax.responseText ); } catch( e ) { //suppress } data = { xml: settings.ajax.responseXML, text: settings.ajax.responseText, json: json }; // Normalize: data.xml is non-null in IE9 regardless of if response is valid xml if ( !data.xml || !data.xml.documentElement ) { data.xml = null; try { parser = new DOMParser(); xml = parser.parseFromString( settings.ajax.responseText, "text/xml" ); if ( !xml.getElementsByTagName( "parsererror" ).length ) { data.xml = xml; } } catch ( e ) { // data.xml remains null } } // If a dataType was specified, return that type of data if ( settings.dataType ) { data = data[ settings.dataType ]; } settings.success.call( settings.ajax, data ); } }; return data; }; Popcorn.xhr.getJSONP = function( url, success, isScript ) { var head = document.head || document.getElementsByTagName( "head" )[ 0 ] || document.documentElement, script = document.createElement( "script" ), isFired = false, params = [], rjsonp = /(=)\?(?=&|$)|\?\?/, replaceInUrl, prefix, paramStr, callback, callparam; if ( !isScript ) { // is there a calback already in the url callparam = url.match( /(callback=[^&]*)/ ); if ( callparam !== null && callparam.length ) { prefix = callparam[ 1 ].split( "=" )[ 1 ]; // Since we need to support developer specified callbacks // and placeholders in harmony, make sure matches to "callback=" // aren't just placeholders. // We coded ourselves into a corner here. // JSONP callbacks should never have been // allowed to have developer specified callbacks if ( prefix === "?" ) { prefix = "jsonp"; } // get the callback name callback = Popcorn.guid( prefix ); // replace existing callback name with unique callback name url = url.replace( /(callback=[^&]*)/, "callback=" + callback ); } else { callback = Popcorn.guid( "jsonp" ); if ( rjsonp.test( url ) ) { url = url.replace( rjsonp, "$1" + callback ); } // split on first question mark, // this is to capture the query string params = url.split( /\?(.+)?/ ); // rebuild url with callback url = params[ 0 ] + "?"; if ( params[ 1 ] ) { url += params[ 1 ] + "&"; } url += "callback=" + callback; } // Define the JSONP success callback globally window[ callback ] = function( data ) { // Fire success callbacks success && success( data ); isFired = true; }; } script.addEventListener( "load", function() { // Handling remote script loading callbacks if ( isScript ) { // getScript success && success(); } // Executing for JSONP requests if ( isFired ) { // Garbage collect the callback delete window[ callback ]; } // Garbage collect the script resource head.removeChild( script ); }, false ); script.addEventListener( "error", function( e ) { // Handling remote script loading callbacks success && success( { error: e } ); // Executing for JSONP requests if ( !isScript ) { // Garbage collect the callback delete window[ callback ]; } // Garbage collect the script resource head.removeChild( script ); }, false ); script.src = url; head.insertBefore( script, head.firstChild ); return; }; Popcorn.getJSONP = Popcorn.xhr.getJSONP; Popcorn.getScript = Popcorn.xhr.getScript = function( url, success ) { return Popcorn.xhr.getJSONP( url, success, true ); }; Popcorn.util = { // Simple function to parse a timestamp into seconds // Acceptable formats are: // HH:MM:SS.MMM // HH:MM:SS;FF // Hours and minutes are optional. They default to 0 toSeconds: function( timeStr, framerate ) { // Hours and minutes are optional // Seconds must be specified // Seconds can be followed by milliseconds OR by the frame information var validTimeFormat = /^([0-9]+:){0,2}[0-9]+([.;][0-9]+)?$/, errorMessage = "Invalid time format", digitPairs, lastIndex, lastPair, firstPair, frameInfo, frameTime; if ( typeof timeStr === "number" ) { return timeStr; } if ( typeof timeStr === "string" && !validTimeFormat.test( timeStr ) ) { Popcorn.error( errorMessage ); } digitPairs = timeStr.split( ":" ); lastIndex = digitPairs.length - 1; lastPair = digitPairs[ lastIndex ]; // Fix last element: if ( lastPair.indexOf( ";" ) > -1 ) { frameInfo = lastPair.split( ";" ); frameTime = 0; if ( framerate && ( typeof framerate === "number" ) ) { frameTime = parseFloat( frameInfo[ 1 ], 10 ) / framerate; } digitPairs[ lastIndex ] = parseInt( frameInfo[ 0 ], 10 ) + frameTime; } firstPair = digitPairs[ 0 ]; return { 1: parseFloat( firstPair, 10 ), 2: ( parseInt( firstPair, 10 ) * 60 ) + parseFloat( digitPairs[ 1 ], 10 ), 3: ( parseInt( firstPair, 10 ) * 3600 ) + ( parseInt( digitPairs[ 1 ], 10 ) * 60 ) + parseFloat( digitPairs[ 2 ], 10 ) }[ digitPairs.length || 1 ]; } }; // alias for exec function Popcorn.p.cue = Popcorn.p.exec; // Protected API methods Popcorn.protect = { natives: getKeys( Popcorn.p ).map(function( val ) { return val.toLowerCase(); }) }; // Setup logging for deprecated methods Popcorn.forEach({ // Deprecated: Recommended "listen": "on", "unlisten": "off", "trigger": "emit", "exec": "cue" }, function( recommend, api ) { var original = Popcorn.p[ api ]; // Override the deprecated api method with a method of the same name // that logs a warning and defers to the new recommended method Popcorn.p[ api ] = function() { if ( typeof console !== "undefined" && console.warn ) { console.warn( "Deprecated method '" + api + "', " + (recommend == null ? "do not use." : "use '" + recommend + "' instead." ) ); // Restore api after first warning Popcorn.p[ api ] = original; } return Popcorn.p[ recommend ].apply( this, [].slice.call( arguments ) ); }; }); // Exposes Popcorn to global context global.Popcorn = Popcorn; })(window, window.document);