Created
January 29, 2015 19:28
-
-
Save rpatil/8bb236c8ceae62c35b19 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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 = "@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 = 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 ( 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; | |
// Trim leading/trailing whitespace to avoid false negatives | |
selector = selector.trim(); | |
// 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 = { | |
url: "", | |
data: "", | |
dataType: "", | |
success: Popcorn.nop, | |
type: "GET", | |
async: true, | |
xhr: function() { | |
return new global.XMLHttpRequest(); | |
} | |
}; | |
Popcorn.xhr = function( options ) { | |
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; | |
} | |
var settings = Popcorn.extend( {}, setup, options ); | |
// Create new XMLHttpRequest object | |
settings.ajax = settings.xhr(); | |
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; | |
} | |
settings.ajax.open( settings.type, settings.url, settings.async ); | |
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.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); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment