diff -r b5849024c5c5 -r 1444edeae73f src/js/popcorn.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/js/popcorn.js Mon Oct 03 16:08:36 2011 +0200 @@ -0,0 +1,1983 @@ +(function(global, document) { + + // Popcorn.js does not support archaic browsers + if ( !document.addEventListener ) { + global.Popcorn = { + isSupported: false + }; + + var methods = ( "forEach extend effects error guid sizeOf isArray nop position disable enable destroy " + + "addTrackEvent removeTrackEvent getTrackEvents getTrackEvent getLastTrackEventId " + + "timeUpdate plugin removePlugin compose effect parser 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, + + // ID string matching + rIdExp = /^(#([\w\-\_\.]+))$/, + + // 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 ); + }; + }()), + + // 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; + + // Supports Popcorn(function () { /../ }) + // Originally proposed by Daniel Brooks + + if ( typeof entity === "function" ) { + + // If document ready has already fired + if ( document.readyState === "interactive" || 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; + } + + // Check if entity is a valid string id + matches = rIdExp.exec( entity ); + + // Get media element by id or object reference + this.media = matches && matches.length && matches[ 2 ] ? + document.getElementById( matches[ 2 ] ) : + entity; + + // Create an audio or video element property reference + this[ ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video" ] = this.media; + + // Register new instance + Popcorn.instances.push( this ); + + this.options = options || {}; + + this.isDestroyed = false; + + this.data = { + + // 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 + } + }; + + // Wrap true ready check + var isReady = function( that ) { + + var duration, videoDurationPlus, animate; + + if ( that.media.readyState >= 2 ) { + // Adding padding to the front and end of the arrays + // this is so we do not fall off either end + + duration = that.media.duration; + // Check for no duration info (NaN) + videoDurationPlus = duration != duration ? Number.MAX_VALUE : duration + 1; + + Popcorn.addTrackEvent( that, { + start: videoDurationPlus, + end: videoDurationPlus + }); + + if ( that.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) + animate = function () { + + Popcorn.timeUpdate( that, {} ); + + that.trigger( "timeupdate" ); + + requestAnimFrame( animate ); + }; + + requestAnimFrame( animate ); + + } else { + + that.data.timeUpdateFunction = function( event ) { + Popcorn.timeUpdate( that, event ); + }; + + if ( !that.isDestroyed ) { + that.media.addEventListener( "timeupdate", that.data.timeUpdateFunction, false ); + } + } + } else { + global.setTimeout(function() { + isReady( that ); + }, 1 ); + } + }; + + isReady( this ); + + return this; + } + }; + + // Extend constructor prototype to instance prototype + // Allows chaining methods to instances + Popcorn.p.init.prototype = Popcorn.p; + + 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 ) { + + var disabled = instance.data.disabled; + + if ( disabled.indexOf( plugin ) === -1 ) { + disabled.push( plugin ); + } + + return instance; + }, + enable: function( instance, plugin ) { + + var disabled = instance.data.disabled, + index = disabled.indexOf( plugin ); + + if ( index > -1 ) { + disabled.splice( index, 1 ); + } + + return instance; + }, + destroy: function( instance ) { + var events = instance.data.events, + singleEvent, item, fn; + + // Iterate through all events and remove them + for ( item in events ) { + singleEvent = events[ item ]; + for ( fn in singleEvent ) { + delete singleEvent[ fn ]; + } + events[ item ] = null; + } + + if ( !instance.isDestroyed ) { + instance.media.removeEventListener( "timeupdate", instance.data.timeUpdateFunction, 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 ) { + + 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 ) { + + this.media[ name ] = 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 -~this.media.currentTime; + }, + + // Attach an event to a single point in time + exec: function( time, fn ) { + + // Creating a one second track event with an empty end + Popcorn.addTrackEvent( this, { + start: time, + end: time + 1, + _running: false, + _natives: { + start: fn || Popcorn.nop, + end: Popcorn.nop, + type: "exec" + } + }); + + 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.trigger( 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.indexOf( plugin ) > -1 ? "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 " + + "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 + Popcorn.forEach( [ "trigger", "listen", "unlisten" ], function( key ) { + Popcorn.p[ key ] = Popcorn.events.fn[ key ]; + }); + + // Internal Only - Adds track events to the instance object + Popcorn.addTrackEvent = function( obj, 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 ] ) ) { + + track = Popcorn.extend( {}, obj.options.defaults[ track._natives.type ], track ); + } + + if ( track._natives ) { + // Supports user defined track event id + track._id = !track.id ? Popcorn.guid( track._natives.type ) : track.id; + + // 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, + idx; + + for ( idx = byStart.length - 1; idx >= 0; idx-- ) { + + if ( track.start >= byStart[ idx ].start ) { + byStart.splice( idx + 1, 0, track ); + break; + } + } + + for ( idx = byEnd.length - 1; idx >= 0; idx-- ) { + + if ( track.end > byEnd[ idx ].end ) { + byEnd.splice( idx + 1, 0, track ); + break; + } + } + + this.timeUpdate( obj, null ); + + // Store references to user added trackevents in ref table + if ( track._id ) { + Popcorn.addTrackEvent.ref( obj, 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, trackId ) { + + var historyLen = obj.data.history.length, + indexWasAt = 0, + byStart = [], + byEnd = [], + animating = [], + history = []; + + Popcorn.forEach( obj.data.trackEvents.byStart, function( o, i, context ) { + // Preserve the original start/end trackEvents + if ( !o._id ) { + byStart.push( obj.data.trackEvents.byStart[i] ); + byEnd.push( obj.data.trackEvents.byEnd[i] ); + } + + // Filter for user track events (vs system track events) + if ( o._id ) { + + // Filter for the trackevent to remove + if ( o._id !== trackId ) { + byStart.push( obj.data.trackEvents.byStart[i] ); + byEnd.push( obj.data.trackEvents.byEnd[i] ); + } + + // Capture the position of the track being removed. + if ( o._id === trackId ) { + indexWasAt = i; + o._natives._teardown && o._natives._teardown.call( obj, o ); + } + } + + }); + + if ( obj.data.trackEvents.animating.length ) { + Popcorn.forEach( obj.data.trackEvents.animating, function( o, i, context ) { + // Preserve the original start/end trackEvents + if ( !o._id ) { + animating.push( obj.data.trackEvents.animating[i] ); + } + + // Filter for user track events (vs system track events) + if ( o._id ) { + // Filter for the trackevent to remove + if ( o._id !== trackId ) { + animating.push( obj.data.trackEvents.animating[i] ); + } + } + }); + } + + // 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 ] !== trackId ) { + history.push( obj.data.history[ i ] ); + } + } + + // Update ordered history array + obj.data.history = history; + + // Update track event references + Popcorn.removeTrackEvent.ref( obj, trackId ); + }; + + // Internal Only - Removes track event references from instance object's trackRefs hash table + Popcorn.removeTrackEvent.ref = function( obj, trackId ) { + delete obj.data.trackRefs[ trackId ]; + + 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, + animating = tracks.animating, + end = tracks.endIndex, + start = tracks.startIndex, + animIndex = 0, + + registryByName = Popcorn.registryByName, + + byEnd, byStart, byAnimate, natives, type; + + // 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; + natives.end.call( obj, event, 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 && + obj.data.disabled.indexOf( type ) === -1 ) { + + byStart._running = true; + natives.start.call( obj, event, byStart ); + + // If the `frameAnimation` option is used, + // push the current byStart object into the `animating` cue + if ( obj.options.frameAnimation && + ( byStart && byStart._running && byStart._natives.frame ) ) { + + animating.push( byStart ); + } + } + start++; + } else { + // remove track event + Popcorn.removeTrackEvent( obj, byStart._id ); + return; + } + } + + // If the `frameAnimation` option is used, iterate the animating track + // and execute the `frame` callback + if ( obj.options.frameAnimation ) { + while ( animIndex < animating.length ) { + + byAnimate = animating[ animIndex ]; + + if ( !byAnimate._running ) { + animating.splice( animIndex, 1 ); + } else { + byAnimate._natives.frame.call( obj, event, byAnimate, currentTime ); + animIndex++; + } + } + } + + // 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; + natives.end.call( obj, event, 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 && + obj.data.disabled.indexOf( type ) === -1 ) { + + byEnd._running = true; + natives.start.call( obj, event, byEnd ); + + // If the `frameAnimation` option is used, + // push the current byEnd object into the `animating` cue + if ( obj.options.frameAnimation && + ( byEnd && byEnd._running && byEnd._natives.frame ) ) { + + animating.push( byEnd ); + } + } + end--; + } else { + // remove track event + Popcorn.removeTrackEvent( obj, byEnd._id ); + return; + } + } + + // If the `frameAnimation` option is used, iterate the animating track + // and execute the `frame` callback + if ( obj.options.frameAnimation ) { + while ( animIndex < animating.length ) { + + byAnimate = animating[ animIndex ]; + + if ( !byAnimate._running ) { + animating.splice( animIndex, 1 ); + } else { + byAnimate._natives.frame.call( obj, event, byAnimate, currentTime ); + animIndex++; + } + } + } + // time bar is not moving ( video is paused ) + } + + tracks.endIndex = end; + tracks.startIndex = start; + tracks.previousUpdateTime = currentTime; + }; + + // 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; + } + + // Storing the plugin natives + var natives = options._natives = {}, + compose = "", + defaults, originalOpts, manifestOpts, mergedSetupOpts; + + Popcorn.extend( natives, setup ); + + options._natives.type = name; + options._running = false; + + natives.start = natives.start || natives[ "in" ]; + natives.end = natives.end || natives[ "out" ]; + + // Check for previously set default options + defaults = this.options.defaults && this.options.defaults[ options._natives && options._natives.type ]; + + // 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 ( !( "end" in options ) ) { + options.end = options[ "out" ] || this.duration() || Number.MAX_VALUE; + } + + // Merge with defaults if they exist, make sure per call is prioritized + mergedSetupOpts = defaults ? Popcorn.extend( {}, defaults, options ) : + options; + + // Resolves 239, 241, 242 + if ( !mergedSetupOpts.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; + + mergedSetupOpts.target = manifestOpts && "target" in manifestOpts && manifestOpts.target; + } + + // Trigger _setup method if exists + options._natives._setup && options._natives._setup.call( this, mergedSetupOpts ); + + // Create new track event for this instance + Popcorn.addTrackEvent( this, Popcorn.extend( mergedSetupOpts, 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.listen( type, callback ); + } + } + + }, this ); + + return this; + }; + + // Assign new named definition + plugin[ name ] = function( options ) { + return pluginFn.call( this, isfn ? definition.call( this, options ) : definition, + options ); + }; + + // Extend Popcorn.p with new named definition + Popcorn.extend( Popcorn.p, plugin ); + + // 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() { + 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.trigger( "error", Popcorn.plugin.errors ); + } + }; + } + + // Debug-mode flag for plugin development + Popcorn.plugin.debug = false; + + // 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 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 ) && + ( byEnd[ idx ] && byEnd[ idx ]._natives && byEnd[ idx ]._natives.type === name ) ) { + + byStart[ idx ]._natives._teardown && byStart[ idx ]._natives._teardown.call( obj, byStart[ idx ] ); + + byStart.splice( idx, 1 ); + byEnd.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--; + } + } + } + + //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; + + // stores parsers keyed on filetype + Popcorn.parsers = {}; + + // An interface for extending Popcorn + // with parser functionality + Popcorn.parser = function( name, type, definition ) { + + if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) { + Popcorn.error( "'" + name + "' is a protected function name" ); + return; + } + + // fixes parameters for overloaded function call + if ( typeof type === "function" && !definition ) { + definition = type; + type = ""; + } + + if ( typeof definition !== "function" || typeof type !== "string" ) { + return; + } + + // Provides some sugar, but ultimately extends + // the definition into Popcorn.p + + var natives = Popcorn.events.all, + parseFn, + parser = {}; + + parseFn = function( filename, callback ) { + + if ( !filename ) { + return this; + } + + var that = this; + + Popcorn.xhr({ + url: filename, + dataType: type, + success: function( data ) { + + var tracksObject = definition( data ), + tracksData, + tracksDataLen, + tracksDef, + idx = 0; + + tracksData = tracksObject.data || []; + tracksDataLen = tracksData.length; + tracksDef = null; + + // If no tracks to process, return immediately + if ( !tracksDataLen ) { + return; + } + + // Create tracks out of parsed object + for ( ; idx < tracksDataLen; idx++ ) { + + tracksDef = tracksData[ idx ]; + + for ( var key in tracksDef ) { + + if ( hasOwn.call( tracksDef, key ) && !!that[ key ] ) { + + that[ key ]( tracksDef[ key ] ); + } + } + } + if ( callback ) { + callback(); + } + } + }); + + return this; + }; + + // Assign new named definition + parser[ name ] = parseFn; + + // Extend Popcorn.p with new named definition + Popcorn.extend( Popcorn.p, parser ); + + // keys the function name by filetype extension + //Popcorn.parsers[ name ] = true; + + return parser; + }; + + Popcorn.player = function( name, player ) { + + player = player || {}; + + var playerFn = function( target, src, options ) { + + options = options || {}; + + // List of events + var date = new Date() / 1000, + baselineTime = date, + currentTime = 0, + events = {}, + + // The container div of the resource + container = document.getElementById( rIdExp.exec( target ) && rIdExp.exec( target )[ 2 ] ) || + document.getElementById( target ) || + target, + basePlayer = {}, + timeout, + popcorn; + + // copies a div into the media object + for( var val in container ) { + + if ( typeof container[ val ] === "object" ) { + + basePlayer[ val ] = container[ val ]; + } else if ( typeof container[ val ] === "function" ) { + + basePlayer[ val ] = (function( value ) { + + return function() { + + return container[ value ].apply( container, arguments ); + }; + }( val )); + } else { + + Popcorn.player.defineProperty( basePlayer, val, { + get: (function( value ) { + + return function() { + + return container[ value ]; + }; + }( val )), + set: Popcorn.nop, + configurable: true + }); + } + } + + var timeupdate = function() { + + date = new Date() / 1000; + + if ( !basePlayer.paused ) { + + basePlayer.currentTime = basePlayer.currentTime + ( date - baselineTime ); + basePlayer.dispatchEvent( "timeupdate" ); + timeout = setTimeout( timeupdate, 10 ); + } + + baselineTime = date; + }; + + basePlayer.play = function() { + + this.paused = false; + + if ( basePlayer.readyState >= 4 ) { + + baselineTime = new Date() / 1000; + basePlayer.dispatchEvent( "play" ); + timeupdate(); + } + }; + + basePlayer.pause = function() { + + this.paused = true; + basePlayer.dispatchEvent( "pause" ); + }; + + Popcorn.player.defineProperty( basePlayer, "currentTime", { + get: function() { + + return currentTime; + }, + set: function( val ) { + + // make sure val is a number + currentTime = +val; + basePlayer.dispatchEvent( "timeupdate" ); + return currentTime; + }, + configurable: true + }); + + // Adds an event listener to the object + basePlayer.addEventListener = function( evtName, fn ) { + + if ( !events[ evtName ] ) { + + events[ evtName ] = []; + } + + events[ evtName ].push( fn ); + return fn; + }; + + // Can take event object or simple string + basePlayer.dispatchEvent = function( oEvent ) { + + var evt, + self = this, + eventInterface, + eventName = oEvent.type; + + // A string was passed, create event object + if ( !eventName ) { + + eventName = oEvent; + eventInterface = Popcorn.events.getInterface( eventName ); + + if ( eventInterface ) { + + evt = document.createEvent( eventInterface ); + evt.initEvent( eventName, true, true, window, 1 ); + } + } + + Popcorn.forEach( events[ eventName ], function( val ) { + + val.call( self, evt, self ); + }); + }; + + // Attempt to get src from playerFn parameter + basePlayer.src = src || ""; + basePlayer.readyState = 0; + basePlayer.duration = 0; + basePlayer.paused = true; + basePlayer.ended = 0; + + // basePlayer has no concept of sound + basePlayer.volume = 1; + basePlayer.muted = false; + + if ( player._setup ) { + + player._setup.call( basePlayer, options ); + } else { + + // there is no setup, which means there is nothing to load + basePlayer.readyState = 4; + basePlayer.dispatchEvent( 'load' ); + } + + popcorn = new Popcorn.p.init( basePlayer, options ); + + return popcorn; + }; + + Popcorn[ name ] = Popcorn[ name ] || playerFn; + }; + + Popcorn.player.defineProperty = Object.defineProperty || function( object, description, options ) { + + object.__defineGetter__( description, options.get || Popcorn.nop ); + object.__defineSetter__( description, options.set || Popcorn.nop ); + }; + + // 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; + + 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 + }; + + // 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" ), + paramStr = url.split( "?" )[ 1 ], + isFired = false, + params = [], + callback, parts, callparam; + + if ( paramStr && !isScript ) { + params = paramStr.split( "&" ); + } + + if ( params.length ) { + parts = params[ params.length - 1 ].split( "=" ); + } + + callback = params.length ? ( parts[ 1 ] ? parts[ 1 ] : parts[ 0 ] ) : "jsonp"; + + if ( !paramStr && !isScript ) { + url += "?callback=" + callback; + } + + if ( callback && !isScript ) { + + // If a callback name already exists + if ( !!window[ callback ] ) { + // Create a new unique callback name + callback = Popcorn.guid( callback ); + } + + // Define the JSONP success callback globally + window[ callback ] = function( data ) { + // Fire success callbacks + success && success( data ); + isFired = true; + }; + + // Replace callback param and callback name + url = url.replace( parts.join( "=" ), parts[ 0 ] + "=" + callback ); + } + + script.onload = 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 ); + }; + + 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 ]; + } + }; + + + // Initialize locale data + // Based on http://en.wikipedia.org/wiki/Language_localisation#Language_tags_and_codes + function initLocale( arg ) { + + var locale = typeof arg === "string" ? arg : [ arg.language, arg.region ].join( "-" ), + parts = locale.split( "-" ); + + // Setup locale data table + return { + iso6391: locale, + language: parts[ 0 ] || "", + region: parts[ 1 ] || "" + }; + } + + // Declare locale data table + var localeData = initLocale( global.navigator.userLanguage || global.navigator.language ); + + Popcorn.locale = { + + // Popcorn.locale.get() + // returns reference to privately + // defined localeData + get: function() { + return localeData; + }, + + // Popcorn.locale.set( string|object ); + set: function( arg ) { + + localeData = initLocale( arg ); + + Popcorn.locale.broadcast(); + + return localeData; + }, + + // Popcorn.locale.broadcast( type ) + // Sends events to all popcorn media instances that are + // listening for locale events + broadcast: function( type ) { + + var instances = Popcorn.instances, + length = instances.length, + idx = 0, + instance; + + type = type || "locale:changed"; + + // Iterate all current instances + for ( ; idx < length; idx++ ) { + instance = instances[ idx ]; + + // For those instances with locale event listeners, + // trigger a locale change event + if ( type in instance.data.events ) { + instance.trigger( type ); + } + } + } + }; + + // alias for exec function + Popcorn.p.cue = Popcorn.p.exec; + + // Protected API methods + Popcorn.protect = { + natives: Object.keys( Popcorn.p ).join( "," ).toLowerCase().split( "," ) + }; + + // Exposes Popcorn to global context + global.Popcorn = Popcorn; + +})(window, window.document);