diff -r ec4f33084f8d -r f6232b308fbd web/static/res/js/popcorn-complete.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/static/res/js/popcorn-complete.js Thu Oct 04 20:39:57 2012 +0200 @@ -0,0 +1,9167 @@ +/* + * popcorn.js version 1.3 + * http://popcornjs.org + * + * Copyright 2011, Mozilla Foundation + * Licensed under the MIT license + */ + +(function(global, document) { + + // Popcorn.js does not support archaic browsers + if ( !document.addEventListener ) { + global.Popcorn = { + isSupported: false + }; + + var methods = ( "byId forEach extend effects error guid sizeOf isArray nop position disable enable destroy" + + "addTrackEvent removeTrackEvent getTrackEvents getTrackEvent getLastTrackEventId " + + "timeUpdate plugin removePlugin compose effect xhr getJSONP getScript" ).split(/\s+/); + + while ( methods.length ) { + global.Popcorn[ methods.shift() ] = function() {}; + } + return; + } + + var + + AP = Array.prototype, + OP = Object.prototype, + + forEach = AP.forEach, + slice = AP.slice, + hasOwn = OP.hasOwnProperty, + toString = OP.toString, + + // Copy global Popcorn (may not exist) + _Popcorn = global.Popcorn, + + // Ready fn cache + readyStack = [], + readyBound = false, + readyFired = false, + + // Non-public internal data object + internal = { + events: { + hash: {}, + apis: {} + } + }, + + // Non-public `requestAnimFrame` + // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + requestAnimFrame = (function(){ + return global.requestAnimationFrame || + global.webkitRequestAnimationFrame || + global.mozRequestAnimationFrame || + global.oRequestAnimationFrame || + global.msRequestAnimationFrame || + function( callback, element ) { + global.setTimeout( callback, 16 ); + }; + }()), + + // Non-public `getKeys`, return an object's keys as an array + getKeys = function( obj ) { + return Object.keys ? Object.keys( obj ) : (function( obj ) { + var item, + list = []; + + for ( item in obj ) { + if ( hasOwn.call( obj, item ) ) { + list.push( item ); + } + } + return list; + })( obj ); + }, + + // Declare constructor + // Returns an instance object. + Popcorn = function( entity, options ) { + // Return new Popcorn object + return new Popcorn.p.init( entity, options || null ); + }; + + // Popcorn API version, automatically inserted via build system. + Popcorn.version = "1.3"; + + // Boolean flag allowing a client to determine if Popcorn can be supported + Popcorn.isSupported = true; + + // Instance caching + Popcorn.instances = []; + + // Declare a shortcut (Popcorn.p) to and a definition of + // the new prototype for our Popcorn constructor + Popcorn.p = Popcorn.prototype = { + + init: function( entity, options ) { + + var matches, nodeName, + self = this; + + // Supports Popcorn(function () { /../ }) + // Originally proposed by Daniel Brooks + + if ( typeof entity === "function" ) { + + // If document ready has already fired + if ( document.readyState === "complete" ) { + + entity( document, Popcorn ); + + return; + } + // Add `entity` fn to ready stack + readyStack.push( entity ); + + // This process should happen once per page load + if ( !readyBound ) { + + // set readyBound flag + readyBound = true; + + var DOMContentLoaded = function() { + + readyFired = true; + + // Remove global DOM ready listener + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // Execute all ready function in the stack + for ( var i = 0, readyStackLength = readyStack.length; i < readyStackLength; i++ ) { + + readyStack[ i ].call( document, Popcorn ); + + } + // GC readyStack + readyStack = null; + }; + + // Register global DOM ready listener + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + } + + return; + } + + if ( typeof entity === "string" ) { + try { + matches = document.querySelector( entity ); + } catch( e ) { + throw new Error( "Popcorn.js Error: Invalid media element selector: " + entity ); + } + } + + // Get media element by id or object reference + this.media = matches || entity; + + // inner reference to this media element's nodeName string value + nodeName = ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video"; + + // Create an audio or video element property reference + this[ nodeName ] = this.media; + + this.options = options || {}; + + // Resolve custom ID or default prefixed ID + this.id = this.options.id || Popcorn.guid( nodeName ); + + // Throw if an attempt is made to use an ID that already exists + if ( Popcorn.byId( this.id ) ) { + throw new Error( "Popcorn.js Error: Cannot use duplicate ID (" + this.id + ")" ); + } + + this.isDestroyed = false; + + this.data = { + + // data structure of all + running: { + cue: [] + }, + + // Executed by either timeupdate event or in rAF loop + timeUpdate: Popcorn.nop, + + // Allows disabling a plugin per instance + disabled: {}, + + // Stores DOM event queues by type + events: {}, + + // Stores Special event hooks data + hooks: {}, + + // Store track event history data + history: [], + + // Stores ad-hoc state related data] + state: { + volume: this.media.volume + }, + + // Store track event object references by trackId + trackRefs: {}, + + // Playback track event queues + trackEvents: { + byStart: [{ + + start: -1, + end: -1 + }], + byEnd: [{ + start: -1, + end: -1 + }], + animating: [], + startIndex: 0, + endIndex: 0, + previousUpdateTime: -1 + } + }; + + // Register new instance + Popcorn.instances.push( this ); + + // function to fire when video is ready + var isReady = function() { + + // chrome bug: http://code.google.com/p/chromium/issues/detail?id=119598 + // it is possible the video's time is less than 0 + // this has the potential to call track events more than once, when they should not + // start: 0, end: 1 will start, end, start again, when it should just start + // just setting it to 0 if it is below 0 fixes this issue + if ( self.media.currentTime < 0 ) { + + self.media.currentTime = 0; + } + + self.media.removeEventListener( "loadeddata", isReady, false ); + + var duration, videoDurationPlus, + runningPlugins, runningPlugin, rpLength, rpNatives; + + // Adding padding to the front and end of the arrays + // this is so we do not fall off either end + duration = self.media.duration; + + // Check for no duration info (NaN) + videoDurationPlus = duration != duration ? Number.MAX_VALUE : duration + 1; + + Popcorn.addTrackEvent( self, { + start: videoDurationPlus, + end: videoDurationPlus + }); + + if ( self.options.frameAnimation ) { + + // if Popcorn is created with frameAnimation option set to true, + // requestAnimFrame is used instead of "timeupdate" media event. + // This is for greater frame time accuracy, theoretically up to + // 60 frames per second as opposed to ~4 ( ~every 15-250ms) + self.data.timeUpdate = function () { + + Popcorn.timeUpdate( self, {} ); + + // fire frame for each enabled active plugin of every type + Popcorn.forEach( Popcorn.manifest, function( key, val ) { + + runningPlugins = self.data.running[ val ]; + + // ensure there are running plugins on this type on this instance + if ( runningPlugins ) { + + rpLength = runningPlugins.length; + for ( var i = 0; i < rpLength; i++ ) { + + runningPlugin = runningPlugins[ i ]; + rpNatives = runningPlugin._natives; + rpNatives && rpNatives.frame && + rpNatives.frame.call( self, {}, runningPlugin, self.currentTime() ); + } + } + }); + + self.emit( "timeupdate" ); + + !self.isDestroyed && requestAnimFrame( self.data.timeUpdate ); + }; + + !self.isDestroyed && requestAnimFrame( self.data.timeUpdate ); + + } else { + + self.data.timeUpdate = function( event ) { + Popcorn.timeUpdate( self, event ); + }; + + if ( !self.isDestroyed ) { + self.media.addEventListener( "timeupdate", self.data.timeUpdate, false ); + } + } + }; + + Object.defineProperty( this, "error", { + get: function() { + + return self.media.error; + } + }); + + if ( self.media.readyState >= 2 ) { + + isReady(); + } else { + + self.media.addEventListener( "loadeddata", isReady, false ); + } + + return this; + } + }; + + // Extend constructor prototype to instance prototype + // Allows chaining methods to instances + Popcorn.p.init.prototype = Popcorn.p; + + Popcorn.byId = function( str ) { + var instances = Popcorn.instances, + length = instances.length, + i = 0; + + for ( ; i < length; i++ ) { + if ( instances[ i ].id === str ) { + return instances[ i ]; + } + } + + return null; + }; + + Popcorn.forEach = function( obj, fn, context ) { + + if ( !obj || !fn ) { + return {}; + } + + context = context || this; + + var key, len; + + // Use native whenever possible + if ( forEach && obj.forEach === forEach ) { + return obj.forEach( fn, context ); + } + + if ( toString.call( obj ) === "[object NodeList]" ) { + for ( key = 0, len = obj.length; key < len; key++ ) { + fn.call( context, obj[ key ], key, obj ); + } + return obj; + } + + for ( key in obj ) { + if ( hasOwn.call( obj, key ) ) { + fn.call( context, obj[ key ], key, obj ); + } + } + return obj; + }; + + Popcorn.extend = function( obj ) { + var dest = obj, src = slice.call( arguments, 1 ); + + Popcorn.forEach( src, function( copy ) { + for ( var prop in copy ) { + dest[ prop ] = copy[ prop ]; + } + }); + + return dest; + }; + + + // A Few reusable utils, memoized onto Popcorn + Popcorn.extend( Popcorn, { + noConflict: function( deep ) { + + if ( deep ) { + global.Popcorn = _Popcorn; + } + + return Popcorn; + }, + error: function( msg ) { + throw new Error( msg ); + }, + guid: function( prefix ) { + Popcorn.guid.counter++; + return ( prefix ? prefix : "" ) + ( +new Date() + Popcorn.guid.counter ); + }, + sizeOf: function( obj ) { + var size = 0; + + for ( var prop in obj ) { + size++; + } + + return size; + }, + isArray: Array.isArray || function( array ) { + return toString.call( array ) === "[object Array]"; + }, + + nop: function() {}, + + position: function( elem ) { + + var clientRect = elem.getBoundingClientRect(), + bounds = {}, + doc = elem.ownerDocument, + docElem = document.documentElement, + body = document.body, + clientTop, clientLeft, scrollTop, scrollLeft, top, left; + + // Determine correct clientTop/Left + clientTop = docElem.clientTop || body.clientTop || 0; + clientLeft = docElem.clientLeft || body.clientLeft || 0; + + // Determine correct scrollTop/Left + scrollTop = ( global.pageYOffset && docElem.scrollTop || body.scrollTop ); + scrollLeft = ( global.pageXOffset && docElem.scrollLeft || body.scrollLeft ); + + // Temp top/left + top = Math.ceil( clientRect.top + scrollTop - clientTop ); + left = Math.ceil( clientRect.left + scrollLeft - clientLeft ); + + for ( var p in clientRect ) { + bounds[ p ] = Math.round( clientRect[ p ] ); + } + + return Popcorn.extend({}, bounds, { top: top, left: left }); + }, + + disable: function( instance, plugin ) { + + if ( !instance.data.disabled[ plugin ] ) { + + instance.data.disabled[ plugin ] = true; + + for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) { + + event = instance.data.running[ plugin ][ i ]; + event._natives.end.call( instance, null, event ); + } + } + + return instance; + }, + enable: function( instance, plugin ) { + + if ( instance.data.disabled[ plugin ] ) { + + instance.data.disabled[ plugin ] = false; + + for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) { + + event = instance.data.running[ plugin ][ i ]; + event._natives.start.call( instance, null, event ); + } + } + + return instance; + }, + destroy: function( instance ) { + var events = instance.data.events, + trackEvents = instance.data.trackEvents, + singleEvent, item, fn, plugin; + + // Iterate through all events and remove them + for ( item in events ) { + singleEvent = events[ item ]; + for ( fn in singleEvent ) { + delete singleEvent[ fn ]; + } + events[ item ] = null; + } + + // remove all plugins off the given instance + for ( plugin in Popcorn.registryByName ) { + Popcorn.removePlugin( instance, plugin ); + } + + // Remove all data.trackEvents #1178 + trackEvents.byStart.length = 0; + trackEvents.byEnd.length = 0; + + if ( !instance.isDestroyed ) { + instance.data.timeUpdate && instance.media.removeEventListener( "timeupdate", instance.data.timeUpdate, false ); + instance.isDestroyed = true; + } + } + }); + + // Memoized GUID Counter + Popcorn.guid.counter = 1; + + // Factory to implement getters, setters and controllers + // as Popcorn instance methods. The IIFE will create and return + // an object with defined methods + Popcorn.extend(Popcorn.p, (function() { + + var methods = "load play pause currentTime playbackRate volume duration preload playbackRate " + + "autoplay loop controls muted buffered readyState seeking paused played seekable ended", + ret = {}; + + + // Build methods, store in object that is returned and passed to extend + Popcorn.forEach( methods.split( /\s+/g ), function( name ) { + + ret[ name ] = function( arg ) { + var previous; + + if ( typeof this.media[ name ] === "function" ) { + + // Support for shorthanded play(n)/pause(n) jump to currentTime + // If arg is not null or undefined and called by one of the + // allowed shorthandable methods, then set the currentTime + // Supports time as seconds or SMPTE + if ( arg != null && /play|pause/.test( name ) ) { + this.media.currentTime = Popcorn.util.toSeconds( arg ); + } + + this.media[ name ](); + + return this; + } + + if ( arg != null ) { + // Capture the current value of the attribute property + previous = this.media[ name ]; + + // Set the attribute property with the new value + this.media[ name ] = arg; + + // If the new value is not the same as the old value + // emit an "attrchanged event" + if ( previous !== arg ) { + this.emit( "attrchange", { + attribute: name, + previousValue: previous, + currentValue: arg + }); + } + return this; + } + + return this.media[ name ]; + }; + }); + + return ret; + + })() + ); + + Popcorn.forEach( "enable disable".split(" "), function( method ) { + Popcorn.p[ method ] = function( plugin ) { + return Popcorn[ method ]( this, plugin ); + }; + }); + + Popcorn.extend(Popcorn.p, { + + // Rounded currentTime + roundTime: function() { + return Math.round( this.media.currentTime ); + }, + + // Attach an event to a single point in time + exec: function( id, time, fn ) { + var length = arguments.length, + trackEvent, sec; + + // Check if first could possibly be a SMPTE string + // p.cue( "smpte string", fn ); + // try/catch avoid awful throw in Popcorn.util.toSeconds + // TODO: Get rid of that, replace with NaN return? + try { + sec = Popcorn.util.toSeconds( id ); + } catch ( e ) {} + + // If it can be converted into a number then + // it's safe to assume that the string was SMPTE + if ( typeof sec === "number" ) { + id = sec; + } + + // Shift arguments based on use case + // + // Back compat for: + // p.cue( time, fn ); + if ( typeof id === "number" && length === 2 ) { + fn = time; + time = id; + id = Popcorn.guid( "cue" ); + } else { + // Support for new forms + + // p.cue( "empty-cue" ); + if ( length === 1 ) { + // Set a time for an empty cue. It's not important what + // the time actually is, because the cue is a no-op + time = -1; + + } else { + + // Get the trackEvent that matches the given id. + trackEvent = this.getTrackEvent( id ); + + if ( trackEvent ) { + + // p.cue( "my-id", 12 ); + // p.cue( "my-id", function() { ... }); + if ( typeof id === "string" && length === 2 ) { + + // p.cue( "my-id", 12 ); + // The path will update the cue time. + if ( typeof time === "number" ) { + // Re-use existing trackEvent start callback + fn = trackEvent._natives.start; + } + + // p.cue( "my-id", function() { ... }); + // The path will update the cue function + if ( typeof time === "function" ) { + fn = time; + // Re-use existing trackEvent start time + time = trackEvent.start; + } + } + } else { + + if ( length >= 2 ) { + + // p.cue( "a", "00:00:00"); + if ( typeof time === "string" ) { + try { + sec = Popcorn.util.toSeconds( time ); + } catch ( e ) {} + + time = sec; + } + + // p.cue( "b", 11 ); + if ( typeof time === "number" ) { + fn = Popcorn.nop(); + } + + // p.cue( "c", function() {}); + if ( typeof time === "function" ) { + fn = time; + time = -1; + } + } + } + } + } + + // Creating a one second track event with an empty end + // Or update an existing track event with new values + Popcorn.addTrackEvent( this, { + id: id, + start: time, + end: time + 1, + _running: false, + _natives: { + start: fn || Popcorn.nop, + end: Popcorn.nop, + type: "cue" + } + }); + + return this; + }, + + // Mute the calling media, optionally toggle + mute: function( toggle ) { + + var event = toggle == null || toggle === true ? "muted" : "unmuted"; + + // If `toggle` is explicitly `false`, + // unmute the media and restore the volume level + if ( event === "unmuted" ) { + this.media.muted = false; + this.media.volume = this.data.state.volume; + } + + // If `toggle` is either null or undefined, + // save the current volume and mute the media element + if ( event === "muted" ) { + this.data.state.volume = this.media.volume; + this.media.muted = true; + } + + // Trigger either muted|unmuted event + this.emit( event ); + + return this; + }, + + // Convenience method, unmute the calling media + unmute: function( toggle ) { + + return this.mute( toggle == null ? false : !toggle ); + }, + + // Get the client bounding box of an instance element + position: function() { + return Popcorn.position( this.media ); + }, + + // Toggle a plugin's playback behaviour (on or off) per instance + toggle: function( plugin ) { + return Popcorn[ this.data.disabled[ plugin ] ? "enable" : "disable" ]( this, plugin ); + }, + + // Set default values for plugin options objects per instance + defaults: function( plugin, defaults ) { + + // If an array of default configurations is provided, + // iterate and apply each to this instance + if ( Popcorn.isArray( plugin ) ) { + + Popcorn.forEach( plugin, function( obj ) { + for ( var name in obj ) { + this.defaults( name, obj[ name ] ); + } + }, this ); + + return this; + } + + if ( !this.options.defaults ) { + this.options.defaults = {}; + } + + if ( !this.options.defaults[ plugin ] ) { + this.options.defaults[ plugin ] = {}; + } + + Popcorn.extend( this.options.defaults[ plugin ], defaults ); + + return this; + } + }); + + Popcorn.Events = { + UIEvents: "blur focus focusin focusout load resize scroll unload", + MouseEvents: "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave click dblclick", + Events: "loadstart progress suspend emptied stalled play pause error " + + "loadedmetadata loadeddata waiting playing canplay canplaythrough " + + "seeking seeked timeupdate ended ratechange durationchange volumechange" + }; + + Popcorn.Events.Natives = Popcorn.Events.UIEvents + " " + + Popcorn.Events.MouseEvents + " " + + Popcorn.Events.Events; + + internal.events.apiTypes = [ "UIEvents", "MouseEvents", "Events" ]; + + // Privately compile events table at load time + (function( events, data ) { + + var apis = internal.events.apiTypes, + eventsList = events.Natives.split( /\s+/g ), + idx = 0, len = eventsList.length, prop; + + for( ; idx < len; idx++ ) { + data.hash[ eventsList[idx] ] = true; + } + + apis.forEach(function( val, idx ) { + + data.apis[ val ] = {}; + + var apiEvents = events[ val ].split( /\s+/g ), + len = apiEvents.length, + k = 0; + + for ( ; k < len; k++ ) { + data.apis[ val ][ apiEvents[ k ] ] = true; + } + }); + })( Popcorn.Events, internal.events ); + + Popcorn.events = { + + isNative: function( type ) { + return !!internal.events.hash[ type ]; + }, + getInterface: function( type ) { + + if ( !Popcorn.events.isNative( type ) ) { + return false; + } + + var eventApi = internal.events, + apis = eventApi.apiTypes, + apihash = eventApi.apis, + idx = 0, len = apis.length, api, tmp; + + for ( ; idx < len; idx++ ) { + tmp = apis[ idx ]; + + if ( apihash[ tmp ][ type ] ) { + api = tmp; + break; + } + } + return api; + }, + // Compile all native events to single array + all: Popcorn.Events.Natives.split( /\s+/g ), + // Defines all Event handling static functions + fn: { + trigger: function( type, data ) { + + var eventInterface, evt; + // setup checks for custom event system + if ( this.data.events[ type ] && Popcorn.sizeOf( this.data.events[ type ] ) ) { + + eventInterface = Popcorn.events.getInterface( type ); + + if ( eventInterface ) { + + evt = document.createEvent( eventInterface ); + evt.initEvent( type, true, true, global, 1 ); + + this.media.dispatchEvent( evt ); + + return this; + } + + // Custom events + Popcorn.forEach( this.data.events[ type ], function( obj, key ) { + + obj.call( this, data ); + + }, this ); + + } + + return this; + }, + listen: function( type, fn ) { + + var self = this, + hasEvents = true, + eventHook = Popcorn.events.hooks[ type ], + origType = type, + tmp; + + if ( !this.data.events[ type ] ) { + this.data.events[ type ] = {}; + hasEvents = false; + } + + // Check and setup event hooks + if ( eventHook ) { + + // Execute hook add method if defined + if ( eventHook.add ) { + eventHook.add.call( this, {}, fn ); + } + + // Reassign event type to our piggyback event type if defined + if ( eventHook.bind ) { + type = eventHook.bind; + } + + // Reassign handler if defined + if ( eventHook.handler ) { + tmp = fn; + + fn = function wrapper( event ) { + eventHook.handler.call( self, event, tmp ); + }; + } + + // assume the piggy back event is registered + hasEvents = true; + + // Setup event registry entry + if ( !this.data.events[ type ] ) { + this.data.events[ type ] = {}; + // Toggle if the previous assumption was untrue + hasEvents = false; + } + } + + // Register event and handler + this.data.events[ type ][ fn.name || ( fn.toString() + Popcorn.guid() ) ] = fn; + + // only attach one event of any type + if ( !hasEvents && Popcorn.events.all.indexOf( type ) > -1 ) { + + this.media.addEventListener( type, function( event ) { + + Popcorn.forEach( self.data.events[ type ], function( obj, key ) { + if ( typeof obj === "function" ) { + obj.call( self, event ); + } + }); + + }, false); + } + return this; + }, + unlisten: function( type, fn ) { + + if ( this.data.events[ type ] && this.data.events[ type ][ fn ] ) { + + delete this.data.events[ type ][ fn ]; + + return this; + } + + this.data.events[ type ] = null; + + return this; + } + }, + hooks: { + canplayall: { + bind: "canplaythrough", + add: function( event, callback ) { + + var state = false; + + if ( this.media.readyState ) { + + callback.call( this, event ); + + state = true; + } + + this.data.hooks.canplayall = { + fired: state + }; + }, + // declare special handling instructions + handler: function canplayall( event, callback ) { + + if ( !this.data.hooks.canplayall.fired ) { + // trigger original user callback once + callback.call( this, event ); + + this.data.hooks.canplayall.fired = true; + } + } + } + } + }; + + // Extend Popcorn.events.fns (listen, unlisten, trigger) to all Popcorn instances + // Extend aliases (on, off, emit) + Popcorn.forEach( [ [ "trigger", "emit" ], [ "listen", "on" ], [ "unlisten", "off" ] ], function( key ) { + Popcorn.p[ key[ 0 ] ] = Popcorn.p[ key[ 1 ] ] = Popcorn.events.fn[ key[ 0 ] ]; + }); + + // Internal Only - Adds track events to the instance object + Popcorn.addTrackEvent = function( obj, track ) { + var trackEvent, isUpdate, eventType; + + // Do a lookup for existing trackevents with this id + if ( track.id ) { + trackEvent = obj.getTrackEvent( track.id ); + } + + // If a track event by this id currently exists, modify it + if ( trackEvent ) { + isUpdate = true; + // Create a new object with the existing trackEvent + // Extend with new track properties + track = Popcorn.extend( {}, trackEvent, track ); + + // Remove the existing track from the instance + obj.removeTrackEvent( track.id ); + } + + // Determine if this track has default options set for it + // If so, apply them to the track object + if ( track && track._natives && track._natives.type && + ( obj.options.defaults && obj.options.defaults[ track._natives.type ] ) ) { + + track = Popcorn.extend( {}, obj.options.defaults[ track._natives.type ], track ); + } + + if ( track._natives ) { + // Supports user defined track event id + track._id = track.id || track._id || Popcorn.guid( track._natives.type ); + + // Push track event ids into the history + obj.data.history.push( track._id ); + } + + track.start = Popcorn.util.toSeconds( track.start, obj.options.framerate ); + track.end = Popcorn.util.toSeconds( track.end, obj.options.framerate ); + + // Store this definition in an array sorted by times + var byStart = obj.data.trackEvents.byStart, + byEnd = obj.data.trackEvents.byEnd, + startIndex, endIndex; + + for ( startIndex = byStart.length - 1; startIndex >= 0; startIndex-- ) { + + if ( track.start >= byStart[ startIndex ].start ) { + byStart.splice( startIndex + 1, 0, track ); + break; + } + } + + for ( endIndex = byEnd.length - 1; endIndex >= 0; endIndex-- ) { + + if ( track.end > byEnd[ endIndex ].end ) { + byEnd.splice( endIndex + 1, 0, track ); + break; + } + } + + // Display track event immediately if it's enabled and current + if ( track.end > obj.media.currentTime && + track.start <= obj.media.currentTime ) { + + track._running = true; + obj.data.running[ track._natives.type ].push( track ); + + if ( !obj.data.disabled[ track._natives.type ] ) { + + track._natives.start.call( obj, null, track ); + } + } + + // update startIndex and endIndex + if ( startIndex <= obj.data.trackEvents.startIndex && + track.start <= obj.data.trackEvents.previousUpdateTime ) { + + obj.data.trackEvents.startIndex++; + } + + if ( endIndex <= obj.data.trackEvents.endIndex && + track.end < obj.data.trackEvents.previousUpdateTime ) { + + obj.data.trackEvents.endIndex++; + } + + this.timeUpdate( obj, null, true ); + + // Store references to user added trackevents in ref table + if ( track._id ) { + Popcorn.addTrackEvent.ref( obj, track ); + } + + // If the call to addTrackEvent was an update/modify call, fire an event + if ( isUpdate ) { + + // Determine appropriate event type to trigger + // they are identical in function, but the naming + // adds some level of intuition for the end developer + // to rely on + if ( track._natives.type === "cue" ) { + eventType = "cuechange"; + } else { + eventType = "trackchange"; + } + + // Fire an event with change information + obj.emit( eventType, { + id: track.id, + previousValue: { + time: trackEvent.start, + fn: trackEvent._natives.start + }, + currentValue: { + time: track.start, + fn: track._natives.start + } + }); + } + }; + + // Internal Only - Adds track event references to the instance object's trackRefs hash table + Popcorn.addTrackEvent.ref = function( obj, track ) { + obj.data.trackRefs[ track._id ] = track; + + return obj; + }; + + Popcorn.removeTrackEvent = function( obj, removeId ) { + + var start, end, animate, + historyLen = obj.data.history.length, + length = obj.data.trackEvents.byStart.length, + index = 0, + indexWasAt = 0, + byStart = [], + byEnd = [], + animating = [], + history = []; + + while ( --length > -1 ) { + start = obj.data.trackEvents.byStart[ index ]; + end = obj.data.trackEvents.byEnd[ index ]; + + // Padding events will not have _id properties. + // These should be safely pushed onto the front and back of the + // track event array + if ( !start._id ) { + byStart.push( start ); + byEnd.push( end ); + } + + // Filter for user track events (vs system track events) + if ( start._id ) { + + // If not a matching start event for removal + if ( start._id !== removeId ) { + byStart.push( start ); + } + + // If not a matching end event for removal + if ( end._id !== removeId ) { + byEnd.push( end ); + } + + // If the _id is matched, capture the current index + if ( start._id === removeId ) { + indexWasAt = index; + + // If a _teardown function was defined, + // enforce for track event removals + if ( start._natives._teardown ) { + start._natives._teardown.call( obj, start ); + } + } + } + // Increment the track index + index++; + } + + // Reset length to be used by the condition below to determine + // if animating track events should also be filtered for removal. + // Reset index below to be used by the reverse while as an + // incrementing counter + length = obj.data.trackEvents.animating.length; + index = 0; + + if ( length ) { + while ( --length > -1 ) { + animate = obj.data.trackEvents.animating[ index ]; + + // Padding events will not have _id properties. + // These should be safely pushed onto the front and back of the + // track event array + if ( !animate._id ) { + animating.push( animate ); + } + + // If not a matching animate event for removal + if ( animate._id && animate._id !== removeId ) { + animating.push( animate ); + } + // Increment the track index + index++; + } + } + + // Update + if ( indexWasAt <= obj.data.trackEvents.startIndex ) { + obj.data.trackEvents.startIndex--; + } + + if ( indexWasAt <= obj.data.trackEvents.endIndex ) { + obj.data.trackEvents.endIndex--; + } + + obj.data.trackEvents.byStart = byStart; + obj.data.trackEvents.byEnd = byEnd; + obj.data.trackEvents.animating = animating; + + for ( var i = 0; i < historyLen; i++ ) { + if ( obj.data.history[ i ] !== removeId ) { + history.push( obj.data.history[ i ] ); + } + } + + // Update ordered history array + obj.data.history = history; + + // Update track event references + Popcorn.removeTrackEvent.ref( obj, removeId ); + }; + + // Internal Only - Removes track event references from instance object's trackRefs hash table + Popcorn.removeTrackEvent.ref = function( obj, removeId ) { + delete obj.data.trackRefs[ removeId ]; + + return obj; + }; + + // Return an array of track events bound to this instance object + Popcorn.getTrackEvents = function( obj ) { + + var trackevents = [], + refs = obj.data.trackEvents.byStart, + length = refs.length, + idx = 0, + ref; + + for ( ; idx < length; idx++ ) { + ref = refs[ idx ]; + // Return only user attributed track event references + if ( ref._id ) { + trackevents.push( ref ); + } + } + + return trackevents; + }; + + // Internal Only - Returns an instance object's trackRefs hash table + Popcorn.getTrackEvents.ref = function( obj ) { + return obj.data.trackRefs; + }; + + // Return a single track event bound to this instance object + Popcorn.getTrackEvent = function( obj, trackId ) { + return obj.data.trackRefs[ trackId ]; + }; + + // Internal Only - Returns an instance object's track reference by track id + Popcorn.getTrackEvent.ref = function( obj, trackId ) { + return obj.data.trackRefs[ trackId ]; + }; + + Popcorn.getLastTrackEventId = function( obj ) { + return obj.data.history[ obj.data.history.length - 1 ]; + }; + + Popcorn.timeUpdate = function( obj, event ) { + + var currentTime = obj.media.currentTime, + previousTime = obj.data.trackEvents.previousUpdateTime, + tracks = obj.data.trackEvents, + end = tracks.endIndex, + start = tracks.startIndex, + byStartLen = tracks.byStart.length, + byEndLen = tracks.byEnd.length, + registryByName = Popcorn.registryByName, + trackstart = "trackstart", + trackend = "trackend", + + byEnd, byStart, byAnimate, natives, type, runningPlugins; + + // Playbar advancing + if ( previousTime <= currentTime ) { + + while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end <= currentTime ) { + + byEnd = tracks.byEnd[ end ]; + natives = byEnd._natives; + type = natives && natives.type; + + // If plugin does not exist on this instance, remove it + if ( !natives || + ( !!registryByName[ type ] || + !!obj[ type ] ) ) { + + if ( byEnd._running === true ) { + + byEnd._running = false; + runningPlugins = obj.data.running[ type ]; + runningPlugins.splice( runningPlugins.indexOf( byEnd ), 1 ); + + if ( !obj.data.disabled[ type ] ) { + + natives.end.call( obj, event, byEnd ); + + obj.emit( trackend, + Popcorn.extend({}, byEnd, { + plugin: type, + type: trackend + }) + ); + } + } + + end++; + } else { + // remove track event + Popcorn.removeTrackEvent( obj, byEnd._id ); + return; + } + } + + while ( tracks.byStart[ start ] && tracks.byStart[ start ].start <= currentTime ) { + + byStart = tracks.byStart[ start ]; + natives = byStart._natives; + type = natives && natives.type; + + // If plugin does not exist on this instance, remove it + if ( !natives || + ( !!registryByName[ type ] || + !!obj[ type ] ) ) { + + if ( byStart.end > currentTime && + byStart._running === false ) { + + byStart._running = true; + obj.data.running[ type ].push( byStart ); + + if ( !obj.data.disabled[ type ] ) { + + natives.start.call( obj, event, byStart ); + + obj.emit( trackstart, + Popcorn.extend({}, byStart, { + plugin: type, + type: trackstart + }) + ); + } + } + start++; + } else { + // remove track event + Popcorn.removeTrackEvent( obj, byStart._id ); + return; + } + } + + // Playbar receding + } else if ( previousTime > currentTime ) { + + while ( tracks.byStart[ start ] && tracks.byStart[ start ].start > currentTime ) { + + byStart = tracks.byStart[ start ]; + natives = byStart._natives; + type = natives && natives.type; + + // if plugin does not exist on this instance, remove it + if ( !natives || + ( !!registryByName[ type ] || + !!obj[ type ] ) ) { + + if ( byStart._running === true ) { + + byStart._running = false; + runningPlugins = obj.data.running[ type ]; + runningPlugins.splice( runningPlugins.indexOf( byStart ), 1 ); + + if ( !obj.data.disabled[ type ] ) { + + natives.end.call( obj, event, byStart ); + + obj.emit( trackend, + Popcorn.extend({}, byStart, { + plugin: type, + type: trackend + }) + ); + } + } + start--; + } else { + // remove track event + Popcorn.removeTrackEvent( obj, byStart._id ); + return; + } + } + + while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end > currentTime ) { + + byEnd = tracks.byEnd[ end ]; + natives = byEnd._natives; + type = natives && natives.type; + + // if plugin does not exist on this instance, remove it + if ( !natives || + ( !!registryByName[ type ] || + !!obj[ type ] ) ) { + + if ( byEnd.start <= currentTime && + byEnd._running === false ) { + + byEnd._running = true; + obj.data.running[ type ].push( byEnd ); + + if ( !obj.data.disabled[ type ] ) { + + natives.start.call( obj, event, byEnd ); + + obj.emit( trackstart, + Popcorn.extend({}, byEnd, { + plugin: type, + type: trackstart + }) + ); + } + } + end--; + } else { + // remove track event + Popcorn.removeTrackEvent( obj, byEnd._id ); + return; + } + } + } + + tracks.endIndex = end; + tracks.startIndex = start; + tracks.previousUpdateTime = currentTime; + + //enforce index integrity if trackRemoved + tracks.byStart.length < byStartLen && tracks.startIndex--; + tracks.byEnd.length < byEndLen && tracks.endIndex--; + + }; + + // Map and Extend TrackEvent functions to all Popcorn instances + Popcorn.extend( Popcorn.p, { + + getTrackEvents: function() { + return Popcorn.getTrackEvents.call( null, this ); + }, + + getTrackEvent: function( id ) { + return Popcorn.getTrackEvent.call( null, this, id ); + }, + + getLastTrackEventId: function() { + return Popcorn.getLastTrackEventId.call( null, this ); + }, + + removeTrackEvent: function( id ) { + + Popcorn.removeTrackEvent.call( null, this, id ); + return this; + }, + + removePlugin: function( name ) { + Popcorn.removePlugin.call( null, this, name ); + return this; + }, + + timeUpdate: function( event ) { + Popcorn.timeUpdate.call( null, this, event ); + return this; + }, + + destroy: function() { + Popcorn.destroy.call( null, this ); + return this; + } + }); + + // Plugin manifests + Popcorn.manifest = {}; + // Plugins are registered + Popcorn.registry = []; + Popcorn.registryByName = {}; + // An interface for extending Popcorn + // with plugin functionality + Popcorn.plugin = function( name, definition, manifest ) { + + if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) { + Popcorn.error( "'" + name + "' is a protected function name" ); + return; + } + + // Provides some sugar, but ultimately extends + // the definition into Popcorn.p + var reserved = [ "start", "end" ], + plugin = {}, + setup, + isfn = typeof definition === "function", + methods = [ "_setup", "_teardown", "start", "end", "frame" ]; + + // combines calls of two function calls into one + var combineFn = function( first, second ) { + + first = first || Popcorn.nop; + second = second || Popcorn.nop; + + return function() { + first.apply( this, arguments ); + second.apply( this, arguments ); + }; + }; + + // If `manifest` arg is undefined, check for manifest within the `definition` object + // If no `definition.manifest`, an empty object is a sufficient fallback + Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {}; + + // apply safe, and empty default functions + methods.forEach(function( method ) { + definition[ method ] = safeTry( definition[ method ] || Popcorn.nop, name ); + }); + + var pluginFn = function( setup, options ) { + + if ( !options ) { + return this; + } + + // When the "ranges" property is set and its value is an array, short-circuit + // the pluginFn definition to recall itself with an options object generated from + // each range object in the ranges array. (eg. { start: 15, end: 16 } ) + if ( options.ranges && Popcorn.isArray(options.ranges) ) { + Popcorn.forEach( options.ranges, function( range ) { + // Create a fresh object, extend with current options + // and start/end range object's properties + // Works with in/out as well. + var opts = Popcorn.extend( {}, options, range ); + + // Remove the ranges property to prevent infinitely + // entering this condition + delete opts.ranges; + + // Call the plugin with the newly created opts object + this[ name ]( opts ); + }, this); + + // Return the Popcorn instance to avoid creating an empty track event + return this; + } + + // Storing the plugin natives + var natives = options._natives = {}, + compose = "", + originalOpts, manifestOpts; + + Popcorn.extend( natives, setup ); + + options._natives.type = name; + options._running = false; + + natives.start = natives.start || natives[ "in" ]; + natives.end = natives.end || natives[ "out" ]; + + if ( options.once ) { + natives.end = combineFn( natives.end, function() { + this.removeTrackEvent( options._id ); + }); + } + + // extend teardown to always call end if running + natives._teardown = combineFn(function() { + + var args = slice.call( arguments ), + runningPlugins = this.data.running[ natives.type ]; + + // end function signature is not the same as teardown, + // put null on the front of arguments for the event parameter + args.unshift( null ); + + // only call end if event is running + args[ 1 ]._running && + runningPlugins.splice( runningPlugins.indexOf( options ), 1 ) && + natives.end.apply( this, args ); + }, natives._teardown ); + + // default to an empty string if no effect exists + // split string into an array of effects + options.compose = options.compose && options.compose.split( " " ) || []; + options.effect = options.effect && options.effect.split( " " ) || []; + + // join the two arrays together + options.compose = options.compose.concat( options.effect ); + + options.compose.forEach(function( composeOption ) { + + // if the requested compose is garbage, throw it away + compose = Popcorn.compositions[ composeOption ] || {}; + + // extends previous functions with compose function + methods.forEach(function( method ) { + natives[ method ] = combineFn( natives[ method ], compose[ method ] ); + }); + }); + + // Ensure a manifest object, an empty object is a sufficient fallback + options._natives.manifest = manifest; + + // Checks for expected properties + if ( !( "start" in options ) ) { + options.start = options[ "in" ] || 0; + } + + if ( !options.end && options.end !== 0 ) { + options.end = options[ "out" ] || Number.MAX_VALUE; + } + + // Use hasOwn to detect non-inherited toString, since all + // objects will receive a toString - its otherwise undetectable + if ( !hasOwn.call( options, "toString" ) ) { + options.toString = function() { + var props = [ + "start: " + options.start, + "end: " + options.end, + "id: " + (options.id || options._id) + ]; + + // Matches null and undefined, allows: false, 0, "" and truthy + if ( options.target != null ) { + props.push( "target: " + options.target ); + } + + return name + " ( " + props.join(", ") + " )"; + }; + } + + // Resolves 239, 241, 242 + if ( !options.target ) { + + // Sometimes the manifest may be missing entirely + // or it has an options object that doesn't have a `target` property + manifestOpts = "options" in manifest && manifest.options; + + options.target = manifestOpts && "target" in manifestOpts && manifestOpts.target; + } + + if ( options._natives ) { + // ensure an initial id is there before setup is called + options._id = Popcorn.guid( options._natives.type ); + } + + // Trigger _setup method if exists + options._natives._setup && options._natives._setup.call( this, options ); + + // Create new track event for this instance + Popcorn.addTrackEvent( this, options ); + + // Future support for plugin event definitions + // for all of the native events + Popcorn.forEach( setup, function( callback, type ) { + + if ( type !== "type" ) { + + if ( reserved.indexOf( type ) === -1 ) { + + this.on( type, callback ); + } + } + + }, this ); + + return this; + }; + + // Extend Popcorn.p with new named definition + // Assign new named definition + Popcorn.p[ name ] = plugin[ name ] = function( id, options ) { + var length = arguments.length, + trackEvent, defaults, mergedSetupOpts; + + // Shift arguments based on use case + // + // Back compat for: + // p.plugin( options ); + if ( id && !options ) { + options = id; + id = null; + } else { + + // Get the trackEvent that matches the given id. + trackEvent = this.getTrackEvent( id ); + + // If the track event does not exist, ensure that the options + // object has a proper id + if ( !trackEvent ) { + options.id = id; + + // If the track event does exist, merge the updated properties + } else { + + options = Popcorn.extend( {}, trackEvent, options ); + + Popcorn.addTrackEvent( this, options ); + + return this; + } + } + + this.data.running[ name ] = this.data.running[ name ] || []; + + // Merge with defaults if they exist, make sure per call is prioritized + defaults = ( this.options.defaults && this.options.defaults[ name ] ) || {}; + mergedSetupOpts = Popcorn.extend( {}, defaults, options ); + + return pluginFn.call( this, isfn ? definition.call( this, mergedSetupOpts ) : definition, + mergedSetupOpts ); + }; + + // if the manifest parameter exists we should extend it onto the definition object + // so that it shows up when calling Popcorn.registry and Popcorn.registryByName + if ( manifest ) { + Popcorn.extend( definition, { + manifest: manifest + }); + } + + // Push into the registry + var entry = { + fn: plugin[ name ], + definition: definition, + base: definition, + parents: [], + name: name + }; + Popcorn.registry.push( + Popcorn.extend( plugin, entry, { + type: name + }) + ); + Popcorn.registryByName[ name ] = entry; + + return plugin; + }; + + // Storage for plugin function errors + Popcorn.plugin.errors = []; + + // Returns wrapped plugin function + function safeTry( fn, pluginName ) { + return function() { + + // When Popcorn.plugin.debug is true, do not suppress errors + if ( Popcorn.plugin.debug ) { + return fn.apply( this, arguments ); + } + + try { + return fn.apply( this, arguments ); + } catch ( 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); +/*! + * Popcorn.sequence + * + * Copyright 2011, Rick Waldron + * Licensed under MIT license. + * + */ + +/* jslint forin: true, maxerr: 50, indent: 4, es5: true */ +/* global Popcorn: true */ + +// Requires Popcorn.js +(function( global, Popcorn ) { + + // TODO: as support increases, migrate to element.dataset + var doc = global.document, + location = global.location, + rprotocol = /:\/\//, + // TODO: better solution to this sucky stop-gap + lochref = location.href.replace( location.href.split("/").slice(-1)[0], "" ), + // privately held + range = function(start, stop, step) { + + start = start || 0; + stop = ( stop || start || 0 ) + 1; + step = step || 1; + + var len = Math.ceil((stop - start) / step) || 0, + idx = 0, + range = []; + + range.length = len; + + while (idx < len) { + range[idx++] = start; + start += step; + } + return range; + }; + + Popcorn.sequence = function( parent, list ) { + return new Popcorn.sequence.init( parent, list ); + }; + + Popcorn.sequence.init = function( parent, list ) { + + // Video element + this.parent = doc.getElementById( parent ); + + // Store ref to a special ID + this.seqId = Popcorn.guid( "__sequenced" ); + + // List of HTMLVideoElements + this.queue = []; + + // List of Popcorn objects + this.playlist = []; + + // Lists of in/out points + this.inOuts = { + + // Stores the video in/out times for each video in sequence + ofVideos: [], + + // Stores the clip in/out times for each clip in sequences + ofClips: [] + + }; + + // Store first video dimensions + this.dims = { + width: 0, //this.video.videoWidth, + height: 0 //this.video.videoHeight + }; + + this.active = 0; + this.cycling = false; + this.playing = false; + + this.times = { + last: 0 + }; + + // Store event pointers and queues + this.events = { + + }; + + var self = this, + clipOffset = 0; + + // Create `video` elements + Popcorn.forEach( list, function( media, idx ) { + + var video = doc.createElement( "video" ); + + video.preload = "auto"; + + // Setup newly created video element + video.controls = true; + + // If the first, show it, if the after, hide it + video.style.display = ( idx && "none" ) || "" ; + + // Seta registered sequence id + video.id = self.seqId + "-" + idx ; + + // Push this video into the sequence queue + self.queue.push( video ); + + var //satisfy lint + mIn = media["in"], + mOut = media["out"]; + + // Push the in/out points into sequence ioVideos + self.inOuts.ofVideos.push({ + "in": ( mIn !== undefined && mIn ) || 1, + "out": ( mOut !== undefined && mOut ) || 0 + }); + + self.inOuts.ofVideos[ idx ]["out"] = self.inOuts.ofVideos[ idx ]["out"] || self.inOuts.ofVideos[ idx ]["in"] + 2; + + // Set the sources + video.src = !rprotocol.test( media.src ) ? lochref + media.src : media.src; + + // Set some squence specific data vars + video.setAttribute("data-sequence-owner", parent ); + video.setAttribute("data-sequence-guid", self.seqId ); + video.setAttribute("data-sequence-id", idx ); + video.setAttribute("data-sequence-clip", [ self.inOuts.ofVideos[ idx ]["in"], self.inOuts.ofVideos[ idx ]["out"] ].join(":") ); + + // Append the video to the parent element + self.parent.appendChild( video ); + + + self.playlist.push( Popcorn("#" + video.id ) ); + + }); + + self.inOuts.ofVideos.forEach(function( obj ) { + + var clipDuration = obj["out"] - obj["in"], + offs = { + "in": clipOffset, + "out": clipOffset + clipDuration + }; + + self.inOuts.ofClips.push( offs ); + + clipOffset = offs["out"] + 1; + }); + + Popcorn.forEach( this.queue, function( media, idx ) { + + function canPlayThrough( event ) { + + // If this is idx zero, use it as dimension for all + if ( !idx ) { + self.dims.width = media.videoWidth; + self.dims.height = media.videoHeight; + } + + media.currentTime = self.inOuts.ofVideos[ idx ]["in"] - 0.5; + + media.removeEventListener( "canplaythrough", canPlayThrough, false ); + + return true; + } + + // Hook up event listeners for managing special playback + media.addEventListener( "canplaythrough", canPlayThrough, false ); + + // TODO: consolidate & DRY + media.addEventListener( "play", function( event ) { + + self.playing = true; + + }, false ); + + media.addEventListener( "pause", function( event ) { + + self.playing = false; + + }, false ); + + media.addEventListener( "timeupdate", function( event ) { + + var target = event.srcElement || event.target, + seqIdx = +( (target.dataset && target.dataset.sequenceId) || target.getAttribute("data-sequence-id") ), + floor = Math.floor( media.currentTime ); + + if ( self.times.last !== floor && + seqIdx === self.active ) { + + self.times.last = floor; + + if ( floor === self.inOuts.ofVideos[ seqIdx ]["out"] ) { + + Popcorn.sequence.cycle.call( self, seqIdx ); + } + } + }, false ); + }); + + return this; + }; + + Popcorn.sequence.init.prototype = Popcorn.sequence.prototype; + + // + Popcorn.sequence.cycle = function( idx ) { + + if ( !this.queue ) { + Popcorn.error("Popcorn.sequence.cycle is not a public method"); + } + + var // Localize references + queue = this.queue, + ioVideos = this.inOuts.ofVideos, + current = queue[ idx ], + nextIdx = 0, + next, clip; + + + var // Popcorn instances + $popnext, + $popprev; + + + if ( queue[ idx + 1 ] ) { + nextIdx = idx + 1; + } + + // Reset queue + if ( !queue[ idx + 1 ] ) { + + nextIdx = 0; + this.playlist[ idx ].pause(); + + } else { + + next = queue[ nextIdx ]; + clip = ioVideos[ nextIdx ]; + + // Constrain dimentions + Popcorn.extend( next, { + width: this.dims.width, + height: this.dims.height + }); + + $popnext = this.playlist[ nextIdx ]; + $popprev = this.playlist[ idx ]; + + // When not resetting to 0 + current.pause(); + + this.active = nextIdx; + this.times.last = clip["in"] - 1; + + // Play the next video in the sequence + $popnext.currentTime( clip["in"] ); + + $popnext[ nextIdx ? "play" : "pause" ](); + + // Trigger custom cycling event hook + this.trigger( "cycle", { + + position: { + previous: idx, + current: nextIdx + } + + }); + + // Set the previous back to it's beginning time + // $popprev.currentTime( ioVideos[ idx ].in ); + + if ( nextIdx ) { + // Hide the currently ending video + current.style.display = "none"; + // Show the next video in the sequence + next.style.display = ""; + } + + this.cycling = false; + } + + return this; + }; + + var excludes = [ "timeupdate", "play", "pause" ]; + + // Sequence object prototype + Popcorn.extend( Popcorn.sequence.prototype, { + + // Returns Popcorn object from sequence at index + eq: function( idx ) { + return this.playlist[ idx ]; + }, + // Remove a sequence from it's playback display container + remove: function() { + this.parent.innerHTML = null; + }, + // Returns Clip object from sequence at index + clip: function( idx ) { + return this.inOuts.ofVideos[ idx ]; + }, + // Returns sum duration for all videos in sequence + duration: function() { + + var ret = 0, + seq = this.inOuts.ofClips, + idx = 0; + + for ( ; idx < seq.length; idx++ ) { + ret += seq[ idx ]["out"] - seq[ idx ]["in"] + 1; + } + + return ret - 1; + }, + + play: function() { + + this.playlist[ this.active ].play(); + + return this; + }, + // Attach an event to a single point in time + exec: function ( time, fn ) { + + var index = this.active; + + this.inOuts.ofClips.forEach(function( off, idx ) { + if ( time >= off["in"] && time <= off["out"] ) { + index = idx; + } + }); + + //offsetBy = time - self.inOuts.ofVideos[ index ].in; + + time += this.inOuts.ofVideos[ index ]["in"] - this.inOuts.ofClips[ index ]["in"]; + + // Creating a one second track event with an empty end + Popcorn.addTrackEvent( this.playlist[ index ], { + start: time - 1, + end: time, + _running: false, + _natives: { + start: fn || Popcorn.nop, + end: Popcorn.nop, + type: "exec" + } + }); + + return this; + }, + // Binds event handlers that fire only when all + // videos in sequence have heard the event + listen: function( type, callback ) { + + var self = this, + seq = this.playlist, + total = seq.length, + count = 0, + fnName; + + if ( !callback ) { + callback = Popcorn.nop; + } + + // Handling for DOM and Media events + if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) { + Popcorn.forEach( seq, function( video ) { + + video.listen( type, function( event ) { + + event.active = self; + + if ( excludes.indexOf( type ) > -1 ) { + + callback.call( video, event ); + + } else { + if ( ++count === total ) { + callback.call( video, event ); + } + } + }); + }); + + } else { + + // If no events registered with this name, create a cache + if ( !this.events[ type ] ) { + this.events[ type ] = {}; + } + + // Normalize a callback name key + fnName = callback.name || Popcorn.guid( "__" + type ); + + // Store in event cache + this.events[ type ][ fnName ] = callback; + } + + // Return the sequence object + return this; + }, + unlisten: function( type, name ) { + // TODO: finish implementation + }, + trigger: function( type, data ) { + var self = this; + + // Handling for DOM and Media events + if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) { + + // find the active video and trigger api events on that video. + return; + + } else { + + // Only proceed if there are events of this type + // currently registered on the sequence + if ( this.events[ type ] ) { + + Popcorn.forEach( this.events[ type ], function( callback, name ) { + callback.call( self, { type: type }, data ); + }); + + } + } + + return this; + } + }); + + + Popcorn.forEach( Popcorn.manifest, function( obj, plugin ) { + + // Implement passthrough methods to plugins + Popcorn.sequence.prototype[ plugin ] = function( options ) { + + // console.log( this, options ); + var videos = {}, assignTo = [], + idx, off, inOuts, inIdx, outIdx, keys, clip, clipInOut, clipRange; + + for ( idx = 0; idx < this.inOuts.ofClips.length; idx++ ) { + // store reference + off = this.inOuts.ofClips[ idx ]; + // array to test against + inOuts = range( off["in"], off["out"] ); + + inIdx = inOuts.indexOf( options.start ); + outIdx = inOuts.indexOf( options.end ); + + if ( inIdx > -1 ) { + videos[ idx ] = Popcorn.extend( {}, off, { + start: inOuts[ inIdx ], + clipIdx: inIdx + }); + } + + if ( outIdx > -1 ) { + videos[ idx ] = Popcorn.extend( {}, off, { + end: inOuts[ outIdx ], + clipIdx: outIdx + }); + } + } + + keys = Object.keys( videos ).map(function( val ) { + return +val; + }); + + assignTo = range( keys[ 0 ], keys[ 1 ] ); + + //console.log( "PLUGIN CALL MAPS: ", videos, keys, assignTo ); + for ( idx = 0; idx < assignTo.length; idx++ ) { + + var compile = {}, + play = assignTo[ idx ], + vClip = videos[ play ]; + + if ( vClip ) { + + // has instructions + clip = this.inOuts.ofVideos[ play ]; + clipInOut = vClip.clipIdx; + clipRange = range( clip["in"], clip["out"] ); + + if ( vClip.start ) { + compile.start = clipRange[ clipInOut ]; + compile.end = clipRange[ clipRange.length - 1 ]; + } + + if ( vClip.end ) { + compile.start = clipRange[ 0 ]; + compile.end = clipRange[ clipInOut ]; + } + + //compile.start += 0.1; + //compile.end += 0.9; + + } else { + + compile.start = this.inOuts.ofVideos[ play ]["in"]; + compile.end = this.inOuts.ofVideos[ play ]["out"]; + + //compile.start += 0.1; + //compile.end += 0.9; + + } + + // Handling full clip persistance + //if ( compile.start === compile.end ) { + //compile.start -= 0.1; + //compile.end += 0.9; + //} + + // Call the plugin on the appropriate Popcorn object in the playlist + // Merge original options object & compiled (start/end) object into + // a new fresh object + this.playlist[ play ][ plugin ]( + + Popcorn.extend( {}, options, compile ) + + ); + + } + + // Return the sequence object + return this; + }; + + }); +})( this, Popcorn ); +(function( Popcorn ) { + document.addEventListener( "DOMContentLoaded", function() { + + // Supports non-specific elements + var dataAttr = "data-timeline-sources", + medias = document.querySelectorAll( "[" + dataAttr + "]" ); + + Popcorn.forEach( medias, function( idx, key ) { + + var media = medias[ key ], + hasDataSources = false, + dataSources, data, popcornMedia; + + // Ensure that the DOM has an id + if ( !media.id ) { + + media.id = Popcorn.guid( "__popcorn" ); + } + + // Ensure we're looking at a dom node + if ( media.nodeType && media.nodeType === 1 ) { + + popcornMedia = Popcorn( "#" + media.id ); + + dataSources = ( media.getAttribute( dataAttr ) || "" ).split( "," ); + + if ( dataSources[ 0 ] ) { + + Popcorn.forEach( dataSources, function( source ) { + + // split the parser and data as parser!file + data = source.split( "!" ); + + // if no parser is defined for the file, assume "parse" + file extension + if ( data.length === 1 ) { + + // parse a relative URL for the filename, split to get extension + data = source.match( /(.*)[\/\\]([^\/\\]+\.\w+)$/ )[ 2 ].split( "." ); + + data[ 0 ] = "parse" + data[ 1 ].toUpperCase(); + data[ 1 ] = source; + } + + // If the media has data sources and the correct parser is registered, continue to load + if ( dataSources[ 0 ] && popcornMedia[ data[ 0 ] ] ) { + + // Set up the media and load in the datasources + popcornMedia[ data[ 0 ] ]( data[ 1 ] ); + + } + }); + + } + + // Only play the media if it was specified to do so + if ( !!popcornMedia.autoplay() ) { + popcornMedia.play(); + } + + } + }); + }, false ); + +})( Popcorn );(function( global, Popcorn ) { + + var navigator = global.navigator; + + // 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( navigator.userLanguage || 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 ); + } + } + } + }; +})( this, this.Popcorn );(function( Popcorn ) { + + var + + AP = Array.prototype, + OP = Object.prototype, + + forEach = AP.forEach, + slice = AP.slice, + hasOwn = OP.hasOwnProperty, + toString = OP.toString; + + // 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 );(function( Popcorn ) { + + // 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 ); + }; + }; + + // ID string matching + var rIdExp = /^(#([\w\-\_\.]+))$/; + + var audioExtensions = "ogg|oga|aac|mp3|wav", + videoExtensions = "ogg|ogv|mp4|webm", + mediaExtensions = audioExtensions + "|" + videoExtensions; + + var audioExtensionRegexp = new RegExp( "^.*\\.(" + audioExtensions + ")($|\\?)" ), + mediaExtensionRegexp = new RegExp( "^.*\\.(" + mediaExtensions + ")($|\\?)" ); + + Popcorn.player = function( name, player ) { + + // return early if a player already exists under this name + if ( Popcorn[ name ] ) { + + return; + } + + player = player || {}; + + var playerFn = function( target, src, options ) { + + options = options || {}; + + // List of events + var date = new Date() / 1000, + baselineTime = date, + currentTime = 0, + readyState = 0, + volume = 1, + muted = false, + events = {}, + + // The container div of the resource + container = typeof target === "string" ? Popcorn.dom.find( target ) : target, + basePlayer = {}, + timeout, + popcorn; + + if ( !Object.prototype.__defineGetter__ ) { + + basePlayer = container || document.createElement( "div" ); + } + + // copies a div into the media object + for( var val in container ) { + + // don't copy properties if using container as baseplayer + if ( val in basePlayer ) { + + continue; + } + + if ( typeof container[ val ] === "object" ) { + + basePlayer[ val ] = container[ val ]; + } else if ( typeof container[ val ] === "function" ) { + + basePlayer[ val ] = (function( value ) { + + // this is a stupid ugly kludgy hack in honour of Safari + // in Safari a NodeList is a function, not an object + if ( "length" in container[ value ] && !container[ value ].call ) { + + return container[ value ]; + } else { + + 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 + }); + + Popcorn.player.defineProperty( basePlayer, "volume", { + get: function() { + + return volume; + }, + set: function( val ) { + + // make sure val is a number + volume = +val; + basePlayer.dispatchEvent( "volumechange" ); + return volume; + }, + configurable: true + }); + + Popcorn.player.defineProperty( basePlayer, "muted", { + get: function() { + + return muted; + }, + set: function( val ) { + + // make sure val is a number + muted = +val; + basePlayer.dispatchEvent( "volumechange" ); + return muted; + }, + configurable: true + }); + + Popcorn.player.defineProperty( basePlayer, "readyState", { + get: function() { + + return readyState; + }, + set: function( val ) { + + readyState = val; + return readyState; + }, + 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; + }; + + // Removes an event listener from the object + basePlayer.removeEventListener = function( evtName, fn ) { + + var i, + listeners = events[ evtName ]; + + if ( !listeners ){ + + return; + } + + // walk backwards so we can safely splice + for ( i = events[ evtName ].length - 1; i >= 0; i-- ) { + + if( fn === listeners[ i ] ) { + + listeners.splice(i, 1); + } + } + + 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 ); + } + } + + if ( events[ eventName ] ) { + + for ( var i = events[ eventName ].length - 1; i >= 0; i-- ) { + + events[ eventName ][ i ].call( self, evt, self ); + } + } + }; + + // Attempt to get src from playerFn parameter + basePlayer.src = src || ""; + basePlayer.duration = 0; + basePlayer.paused = true; + basePlayer.ended = 0; + + options && options.events && Popcorn.forEach( options.events, function( val, key ) { + + basePlayer.addEventListener( key, val, false ); + }); + + // true and undefined returns on canPlayType means we should attempt to use it, + // false means we cannot play this type + if ( player._canPlayType( container.nodeName, src ) !== 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( "loadedmetadata" ); + basePlayer.dispatchEvent( "loadeddata" ); + basePlayer.dispatchEvent( "canplaythrough" ); + } + } else { + + // Asynchronous so that users can catch this event + setTimeout( function() { + basePlayer.dispatchEvent( "error" ); + }, 0 ); + } + + popcorn = new Popcorn.p.init( basePlayer, options ); + + if ( player._teardown ) { + + popcorn.destroy = combineFn( popcorn.destroy, function() { + + player._teardown.call( basePlayer, options ); + }); + } + + return popcorn; + }; + + playerFn.canPlayType = player._canPlayType = player._canPlayType || Popcorn.nop; + + Popcorn[ name ] = Popcorn.player.registry[ name ] = playerFn; + }; + + Popcorn.player.registry = {}; + + Popcorn.player.defineProperty = Object.defineProperty || function( object, description, options ) { + + object.__defineGetter__( description, options.get || Popcorn.nop ); + object.__defineSetter__( description, options.set || Popcorn.nop ); + }; + + // player queue is to help players queue things like play and pause + // HTML5 video's play and pause are asynch, but do fire in sequence + // play() should really mean "requestPlay()" or "queuePlay()" and + // stash a callback that will play the media resource when it's ready to be played + Popcorn.player.playerQueue = function() { + + var _queue = [], + _running = false; + + return { + next: function() { + + _running = false; + _queue.shift(); + _queue[ 0 ] && _queue[ 0 ](); + }, + add: function( callback ) { + + _queue.push(function() { + + _running = true; + callback && callback(); + }); + + // if there is only one item on the queue, start it + !_running && _queue[ 0 ](); + } + }; + }; + + // smart will attempt to find you a match, if it does not find a match, + // it will attempt to create a video element with the source, + // if that failed, it will throw. + Popcorn.smart = function( target, src, options ) { + var playerType, + elementTypes = [ "AUDIO", "VIDEO" ], + sourceNode, + firstSrc, + node = Popcorn.dom.find( target ), + i, srcResult, + canPlayTypeTester = document.createElement( "video" ), + canPlayTypes = { + "ogg": "video/ogg", + "ogv": "video/ogg", + "oga": "audio/ogg", + "webm": "video/webm", + "mp4": "video/mp4", + "mp3": "audio/mp3" + }; + + var canPlayType = function( type ) { + + return canPlayTypeTester.canPlayType( canPlayTypes[ type ] ); + }; + + var canPlaySrc = function( src ) { + + srcResult = mediaExtensionRegexp.exec( src ); + + if ( !srcResult || !srcResult[ 1 ] ) { + return false; + } + + return canPlayType( srcResult[ 1 ] ); + }; + + if ( !node ) { + + Popcorn.error( "Specified target " + target + " was not found." ); + return; + } + + // For when no src is defined. + // Usually this is a video element with a src already on it. + if ( elementTypes.indexOf( node.nodeName ) > -1 && !src ) { + + if ( typeof src === "object" ) { + + options = src; + src = undefined; + } + + return Popcorn( node, options ); + } + + // if our src is not an array, create an array of one. + if ( typeof( src ) === "string" ) { + + src = [ src ]; + } + + // go through each src, and find the first playable. + // this only covers player sources popcorn knows of, + // and not things like a youtube src that is private. + // it will still consider a private youtube video to be playable. + for ( i = 0, srcLength = src.length; i < srcLength; i++ ) { + + // src is a playable HTML5 video, we don't need to check custom players. + if ( canPlaySrc( src[ i ] ) ) { + + src = src[ i ]; + break; + } + + // for now we loop through and use the first valid player we find. + for ( var key in Popcorn.player.registry ) { + + if ( Popcorn.player.registry.hasOwnProperty( key ) ) { + + if ( Popcorn.player.registry[ key ].canPlayType( node.nodeName, src[ i ] ) ) { + + // Popcorn.smart( player, src, /* options */ ) + return Popcorn[ key ]( node, src[ i ], options ); + } + } + } + } + + // Popcorn.smart( div, src, /* options */ ) + // attempting to create a video in a container + if ( elementTypes.indexOf( node.nodeName ) === -1 ) { + + firstSrc = typeof( src ) === "string" ? src : src.length ? src[ 0 ] : src; + + target = document.createElement( !!audioExtensionRegexp.exec( firstSrc ) ? elementTypes[ 0 ] : elementTypes[ 1 ] ); + + // Controls are defaulted to being present + target.controls = true; + + node.appendChild( target ); + node = target; + } + + options && options.events && options.events.error && node.addEventListener( "error", options.events.error, false ); + node.src = src; + + return Popcorn( node, options ); + + }; +})( Popcorn ); +// PLUGIN: mediaspawner +/** + * mediaspawner Popcorn Plugin. + * Adds Video/Audio to the page using Popcorns players + * Start is the time that you want this plug-in to execute + * End is the time that you want this plug-in to stop executing + * + * @param {HTML} options + * + * Example: + var p = Popcorn('#video') + .mediaspawner( { + source: "http://www.youtube.com/watch?v=bUB1L3zGVvc", + target: "mediaspawnerdiv", + start: 1, + end: 10, + caption: "This is a test. We are assuming conrol. We are assuming control." + }) + * + */ +(function ( Popcorn, global ) { + var PLAYER_URL = "http://popcornjs.org/code/modules/player/popcorn.player.js", + urlRegex = /(?:http:\/\/www\.|http:\/\/|www\.|\.|^)(youtu|vimeo|soundcloud|baseplayer)/, + forEachPlayer, + playerTypeLoading = {}, + playerTypesLoaded = { + "vimeo": false, + "youtube": false, + "soundcloud": false, + "module": false + }; + + Object.defineProperty( playerTypeLoading, forEachPlayer, { + get: function() { + return playerTypesLoaded[ forEachPlayer ]; + }, + set: function( val ) { + playerTypesLoaded[ forEachPlayer ] = val; + } + }); + + Popcorn.plugin( "mediaspawner", { + manifest: { + about: { + name: "Popcorn Media Spawner Plugin", + version: "0.1", + author: "Matthew Schranz, @mjschranz", + website: "mschranz.wordpress.com" + }, + options: { + source: { + elem: "input", + type: "text", + label: "Media Source", + "default": "http://www.youtube.com/watch?v=CXDstfD9eJ0" + }, + caption: { + elem: "input", + type: "text", + label: "Media Caption", + "default": "Popcorn Popping", + optional: true + }, + target: "mediaspawner-container", + start: { + elem: "input", + type: "number", + label: "Start" + }, + end: { + elem: "input", + type: "number", + label: "End" + }, + autoplay: { + elem: "input", + type: "checkbox", + label: "Autoplay Video", + optional: true + }, + width: { + elem: "input", + type: "number", + label: "Media Width", + "default": 400, + units: "px", + optional: true + }, + height: { + elem: "input", + type: "number", + label: "Media Height", + "default": 200, + units: "px", + optional: true + } + } + }, + _setup: function( options ) { + var target = document.getElementById( options.target ) || {}, + mediaType, + container, + capContainer, + regexResult; + + regexResult = urlRegex.exec( options.source ); + if ( regexResult ) { + mediaType = regexResult[ 1 ]; + // our regex only handles youtu ( incase the url looks something like youtu.be ) + if ( mediaType === "youtu" ) { + mediaType = "youtube"; + } + } + else { + // if the regex didn't return anything we know it's an HTML5 source + mediaType = "HTML5"; + } + + // Store Reference to Type for use in end + options._type = mediaType; + + // Create separate container for plugin + options._container = document.createElement( "div" ); + container = options._container; + container.id = "mediaSpawnerdiv-" + Popcorn.guid(); + + // Default width and height of media + options.width = options.width || 400; + options.height = options.height || 200; + + // Captions now need to be in their own container, due to the problem with flash players + // described in start/end + if ( options.caption ) { + capContainer = document.createElement( "div" ); + capContainer.innerHTML = options.caption; + capContainer.style.display = "none"; + options._capCont = capContainer; + container.appendChild( capContainer ); + } + + target && target.appendChild( container ); + + function constructMedia(){ + + function checkPlayerTypeLoaded() { + if ( mediaType !== "HTML5" && !window.Popcorn[ mediaType ] ) { + setTimeout( function() { + checkPlayerTypeLoaded(); + }, 300 ); + } else { + options.id = options._container.id; + // Set the width/height of the container before calling Popcorn.smart + // Allows youtube to pickup on the specified height an create the player + // with specified dimensions + options._container.style.width = options.width + "px"; + options._container.style.height = options.height + "px"; + options.popcorn = Popcorn.smart( "#" + options.id, options.source ); + + if ( mediaType === "HTML5" ) { + options.popcorn.controls( true ); + } + + // Set them to 0 now so it is hidden + options._container.style.width = "0px"; + options._container.style.height = "0px"; + options._container.style.visibility = "hidden"; + options._container.style.overflow = "hidden"; + } + } + + if ( mediaType !== "HTML5" && !window.Popcorn[ mediaType ] && !playerTypeLoading[ mediaType ] ) { + playerTypeLoading[ mediaType ] = true; + Popcorn.getScript( "http://popcornjs.org/code/players/" + mediaType + "/popcorn." + mediaType + ".js", function() { + checkPlayerTypeLoaded(); + }); + } + else { + checkPlayerTypeLoaded(); + } + + } + + // If Player script needed to be loaded, keep checking until it is and then fire readycallback + function isPlayerReady() { + if ( !window.Popcorn.player ) { + setTimeout( function () { + isPlayerReady(); + }, 300 ); + } else { + constructMedia(); + } + } + + // If player script isn't present, retrieve script + if ( !window.Popcorn.player && !playerTypeLoading.module ) { + playerTypeLoading.module = true; + Popcorn.getScript( PLAYER_URL, isPlayerReady ); + } else { + isPlayerReady(); + } + + }, + start: function( event, options ) { + if( options._capCont ) { + options._capCont.style.display = ""; + } + + /* Using this style for Start/End is required because of the flash players + * Without it on end an internal cleanup is called, causing the flash players + * to be out of sync with Popcorn, as they are then rebuilt. + */ + options._container.style.width = options.width + "px"; + options._container.style.height = options.height + "px"; + options._container.style.visibility = "visible"; + options._container.style.overflow = "visible"; + + if ( options.autoplay ) { + options.popcorn.play(); + } + }, + end: function( event, options ) { + if( options._capCont ) { + options._capCont.style.display = "none"; + } + + /* Using this style for Start/End is required because of the flash players + * Without it on end an internal cleanup is called, causing the flash players + * to be out of sync with Popcorn, as they are then rebuilt. + */ + options._container.style.width = "0px"; + options._container.style.height = "0px"; + options._container.style.visibility = "hidden"; + options._container.style.overflow = "hidden"; + + // Pause all popcorn instances on exit + options.popcorn.pause(); + + }, + _teardown: function( options ) { + if ( options.popcorn && options.popcorn.destory ) { + options.popcorn.destroy(); + } + document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( options._container ); + } + }); +})( Popcorn, this ); +// PLUGIN: Code + +(function ( Popcorn ) { + + /** + * Code Popcorn Plug-in + * + * Adds the ability to run arbitrary code (JavaScript functions) according to video timing. + * + * @param {Object} options + * + * Required parameters: start, end, template, data, and target. + * Optional parameter: static. + * + * start: the time in seconds when the mustache template should be rendered + * in the target div. + * + * end: the time in seconds when the rendered mustache template should be + * removed from the target div. + * + * onStart: the function to be run when the start time is reached. + * + * onFrame: [optional] a function to be run on each paint call + * (e.g., called ~60 times per second) between the start and end times. + * + * onEnd: [optional] a function to be run when the end time is reached. + * + * Example: + var p = Popcorn('#video') + + // onStart function only + .code({ + start: 1, + end: 4, + onStart: function( options ) { + // called on start + } + }) + + // onStart + onEnd only + .code({ + start: 6, + end: 8, + onStart: function( options ) { + // called on start + }, + onEnd: function ( options ) { + // called on end + } + }) + + // onStart, onEnd, onFrame + .code({ + start: 10, + end: 14, + onStart: function( options ) { + // called on start + }, + onFrame: function ( options ) { + // called on every paint frame between start and end. + // uses mozRequestAnimationFrame, webkitRequestAnimationFrame, + // or setTimeout with 16ms window. + }, + onEnd: function ( options ) { + // called on end + } + }); + * + */ + + Popcorn.plugin( "code" , function( options ) { + var running = false, + instance = this; + + // Setup a proper frame interval function (60fps), favouring paint events. + var step = (function() { + + var buildFrameRunner = function( runner ) { + return function( f, options ) { + + var _f = function() { + running && f.call( instance, options ); + running && runner( _f ); + }; + + _f(); + }; + }; + + // Figure out which level of browser support we have for this + if ( window.webkitRequestAnimationFrame ) { + return buildFrameRunner( window.webkitRequestAnimationFrame ); + } else if ( window.mozRequestAnimationFrame ) { + return buildFrameRunner( window.mozRequestAnimationFrame ); + } else { + return buildFrameRunner( function( f ) { + window.setTimeout( f, 16 ); + }); + } + + })(); + + if ( !options.onStart || typeof options.onStart !== "function" ) { + + options.onStart = Popcorn.nop; + } + + if ( options.onEnd && typeof options.onEnd !== "function" ) { + + options.onEnd = undefined; + } + + if ( options.onFrame && typeof options.onFrame !== "function" ) { + + options.onFrame = undefined; + } + + return { + start: function( event, options ) { + options.onStart.call( instance, options ); + + if ( options.onFrame ) { + running = true; + step( options.onFrame, options ); + } + }, + + end: function( event, options ) { + if ( options.onFrame ) { + running = false; + } + + if ( options.onEnd ) { + options.onEnd.call( instance, options ); + } + } + }; + }, + { + about: { + name: "Popcorn Code Plugin", + version: "0.1", + author: "David Humphrey (@humphd)", + website: "http://vocamus.net/dave" + }, + options: { + start: { + elem: "input", + type: "number", + label: "Start" + }, + end: { + elem: "input", + type: "number", + label: "End" + }, + onStart: { + elem: "input", + type: "function", + label: "onStart" + }, + onFrame: { + elem: "input", + type: "function", + label: "onFrame", + optional: true + }, + onEnd: { + elem: "input", + type: "function", + label: "onEnd" + } + } + }); +})( Popcorn ); +// PLUGIN: Flickr +(function (Popcorn) { + + /** + * Flickr popcorn plug-in + * Appends a users Flickr images to an element on the page. + * Options parameter will need a start, end, target and userid or username and api_key. + * Optional parameters are numberofimages, height, width, padding, and border + * Start is the time that you want this plug-in to execute (in seconds) + * End is the time that you want this plug-in to stop executing (in seconds) + * Userid is the id of who's Flickr images you wish to show + * Tags is a mutually exclusive list of image descriptor tags + * Username is the username of who's Flickr images you wish to show + * using both userid and username is redundant + * an api_key is required when using username + * Apikey is your own api key provided by Flickr + * Target is the id of the document element that the images are + * appended to, this target element must exist on the DOM + * Numberofimages specify the number of images to retreive from flickr, defaults to 4 + * Height the height of the image, defaults to '50px' + * Width the width of the image, defaults to '50px' + * Padding number of pixels between images, defaults to '5px' + * Border border size in pixels around images, defaults to '0px' + * + * @param {Object} options + * + * Example: + var p = Popcorn('#video') + .flickr({ + start: 5, // seconds, mandatory + end: 15, // seconds, mandatory + userid: '35034346917@N01', // optional + tags: 'dogs,cats', // optional + numberofimages: '8', // optional + height: '50px', // optional + width: '50px', // optional + padding: '5px', // optional + border: '0px', // optional + target: 'flickrdiv' // mandatory + } ) + * + */ + + var idx = 0; + + Popcorn.plugin( "flickr" , function( options ) { + var containerDiv, + target = document.getElementById( options.target ), + _userid, + _uri, + _link, + _image, + _count = options.numberofimages || 4, + _height = options.height || "50px", + _width = options.width || "50px", + _padding = options.padding || "5px", + _border = options.border || "0px"; + + // create a new div this way anything in the target div is left intact + // this is later populated with Flickr images + containerDiv = document.createElement( "div" ); + containerDiv.id = "flickr" + idx; + containerDiv.style.width = "100%"; + containerDiv.style.height = "100%"; + containerDiv.style.display = "none"; + idx++; + + target && target.appendChild( containerDiv ); + + // get the userid from Flickr API by using the username and apikey + var isUserIDReady = function() { + if ( !_userid ) { + + _uri = "http://api.flickr.com/services/rest/?method=flickr.people.findByUsername&"; + _uri += "username=" + options.username + "&api_key=" + options.apikey + "&format=json&jsoncallback=flickr"; + Popcorn.getJSONP( _uri, function( data ) { + _userid = data.user.nsid; + getFlickrData(); + }); + + } else { + + setTimeout(function () { + isUserIDReady(); + }, 5 ); + } + }; + + // get the photos from Flickr API by using the user_id and/or tags + var getFlickrData = function() { + + _uri = "http://api.flickr.com/services/feeds/photos_public.gne?"; + + if ( _userid ) { + _uri += "id=" + _userid + "&"; + } + if ( options.tags ) { + _uri += "tags=" + options.tags + "&"; + } + + _uri += "lang=en-us&format=json&jsoncallback=flickr"; + + Popcorn.xhr.getJSONP( _uri, function( data ) { + + var fragment = document.createElement( "div" ); + + fragment.innerHTML = "

" + data.title + "

"; + + Popcorn.forEach( data.items, function ( item, i ) { + if ( i < _count ) { + + _link = document.createElement( "a" ); + _link.setAttribute( "href", item.link ); + _link.setAttribute( "target", "_blank" ); + _image = document.createElement( "img" ); + _image.setAttribute( "src", item.media.m ); + _image.setAttribute( "height",_height ); + _image.setAttribute( "width", _width ); + _image.setAttribute( "style", "border:" + _border + ";padding:" + _padding ); + _link.appendChild( _image ); + fragment.appendChild( _link ); + + } else { + + return false; + } + }); + + containerDiv.appendChild( fragment ); + }); + }; + + if ( options.username && options.apikey ) { + isUserIDReady(); + } + else { + _userid = options.userid; + getFlickrData(); + } + return { + /** + * @member flickr + * The start function will be executed when the currentTime + * of the video reaches the start time provided by the + * options variable + */ + start: function( event, options ) { + containerDiv.style.display = "inline"; + }, + /** + * @member flickr + * The end function will be executed when the currentTime + * of the video reaches the end time provided by the + * options variable + */ + end: function( event, options ) { + containerDiv.style.display = "none"; + }, + _teardown: function( options ) { + document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( containerDiv ); + } + }; + }, + { + about: { + name: "Popcorn Flickr Plugin", + version: "0.2", + author: "Scott Downe, Steven Weerdenburg, Annasob", + website: "http://scottdowne.wordpress.com/" + }, + options: { + start: { + elem: "input", + type: "number", + label: "Start" + }, + end: { + elem: "input", + type: "number", + label: "End" + }, + userid: { + elem: "input", + type: "text", + label: "User ID", + optional: true + }, + tags: { + elem: "input", + type: "text", + label: "Tags" + }, + username: { + elem: "input", + type: "text", + label: "Username", + optional: true + }, + apikey: { + elem: "input", + type: "text", + label: "API Key", + optional: true + }, + target: "flickr-container", + height: { + elem: "input", + type: "text", + label: "Height", + "default": "50px", + optional: true + }, + width: { + elem: "input", + type: "text", + label: "Width", + "default": "50px", + optional: true + }, + padding: { + elem: "input", + type: "text", + label: "Padding", + optional: true + }, + border: { + elem: "input", + type: "text", + label: "Border", + "default": "5px", + optional: true + }, + numberofimages: { + elem: "input", + type: "number", + "default": 4, + label: "Number of Images" + } + } + }); +})( Popcorn ); +// PLUGIN: Footnote/Text + +(function ( Popcorn ) { + + /** + * Footnote popcorn plug-in + * Adds text to an element on the page. + * Options parameter will need a start, end, target and text. + * Start is the time that you want this plug-in to execute + * End is the time that you want this plug-in to stop executing + * Text is the text that you want to appear in the target + * Target is the id of the document element that the text needs to be + * attached to, this target element must exist on the DOM + * + * @param {Object} options + * + * Example: + * var p = Popcorn('#video') + * .footnote({ + * start: 5, // seconds + * end: 15, // seconds + * text: 'This video made exclusively for drumbeat.org', + * target: 'footnotediv' + * }); + **/ + + Popcorn.plugin( "footnote", { + + manifest: { + about: { + name: "Popcorn Footnote Plugin", + version: "0.2", + author: "@annasob, @rwaldron", + website: "annasob.wordpress.com" + }, + options: { + start: { + elem: "input", + type: "number", + label: "Start" + }, + end: { + elem: "input", + type: "number", + label: "End" + }, + text: { + elem: "input", + type: "text", + label: "Text" + }, + target: "footnote-container" + } + }, + + _setup: function( options ) { + + var target = Popcorn.dom.find( options.target ); + + options._container = document.createElement( "div" ); + options._container.style.display = "none"; + options._container.innerHTML = options.text; + + target.appendChild( options._container ); + }, + + /** + * @member footnote + * The start function will be executed when the currentTime + * of the video reaches the start time provided by the + * options variable + */ + start: function( event, options ){ + options._container.style.display = "inline"; + }, + + /** + * @member footnote + * The end function will be executed when the currentTime + * of the video reaches the end time provided by the + * options variable + */ + end: function( event, options ){ + options._container.style.display = "none"; + }, + + _teardown: function( options ) { + var target = Popcorn.dom.find( options.target ); + if ( target ) { + target.removeChild( options._container ); + } + } + + }); +})( Popcorn ); +// PLUGIN: Text + +(function ( Popcorn ) { + + /** + * Text Popcorn plug-in + * + * Places text in an element on the page. Plugin options include: + * Options parameter will need a start, end. + * Start: Is the time that you want this plug-in to execute + * End: Is the time that you want this plug-in to stop executing + * Text: Is the text that you want to appear in the target + * Escape: {true|false} Whether to escape the text (e.g., html strings) + * Multiline: {true|false} Whether newlines should be turned into
s + * Target: Is the ID of the element where the text should be placed. An empty target + * will be placed on top of the media element + * + * @param {Object} options + * + * Example: + * var p = Popcorn('#video') + * + * // Simple text + * .text({ + * start: 5, // seconds + * end: 15, // seconds + * text: 'This video made exclusively for drumbeat.org', + * target: 'textdiv' + * }) + * + * // HTML text, rendered as HTML + * .text({ + * start: 15, // seconds + * end: 20, // seconds + * text: '

This video made exclusively for drumbeat.org

', + * target: 'textdiv' + * }) + * + * // HTML text, escaped and rendered as plain text + * .text({ + * start: 20, // seconds + * end: 25, // seconds + * text: 'This is an HTML p element:

paragraph

', + * escape: true, + * target: 'textdiv' + * }) + * + * // Multi-Line HTML text, escaped and rendered as plain text + * .text({ + * start: 25, // seconds + * end: 30, // seconds + * text: 'This is an HTML p element:

paragraph

\nThis is an HTML b element: bold', + * escape: true, + * multiline: true, + * target: 'textdiv' + * }); + * + * // Subtitle text + * .text({ + * start: 30, // seconds + * end: 40, // seconds + * text: 'This will be overlayed on the video', + * }) + **/ + + /** + * HTML escape code from mustache.js, used under MIT Licence + * https://github.com/janl/mustache.js/blob/master/mustache.js + **/ + var escapeMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''' + }; + + function escapeHTML( string, multiline ) { + return String( string ).replace( /&(?!\w+;)|[<>"']/g, function ( s ) { + return escapeMap[ s ] || s; + }); + } + + function newlineToBreak( string ) { + // Deal with both \r\n and \n + return string.replace( /\r?\n/gm, "
" ); + } + + // Subtitle specific functionality + function createSubtitleContainer( context, id ) { + + var ctxContainer = context.container = document.createElement( "div" ), + style = ctxContainer.style, + media = context.media; + + var updatePosition = function() { + var position = context.position(); + // the video element must have height and width defined + style.fontSize = "18px"; + style.width = media.offsetWidth + "px"; + style.top = position.top + media.offsetHeight - ctxContainer.offsetHeight - 40 + "px"; + style.left = position.left + "px"; + + setTimeout( updatePosition, 10 ); + }; + + ctxContainer.id = id || ""; + style.position = "absolute"; + style.color = "white"; + style.textShadow = "black 2px 2px 6px"; + style.fontWeight = "bold"; + style.textAlign = "center"; + + updatePosition(); + + context.media.parentNode.appendChild( ctxContainer ); + + return ctxContainer; + } + + Popcorn.plugin( "text", { + + manifest: { + about: { + name: "Popcorn Text Plugin", + version: "0.1", + author: "@humphd" + }, + options: { + start: { + elem: "input", + type: "number", + label: "Start" + }, + end: { + elem: "input", + type: "number", + label: "End" + }, + text: { + elem: "input", + type: "text", + label: "Text", + "default": "Popcorn.js" + }, + escape: { + elem: "input", + type: "checkbox", + label: "Escape" + }, + multiline: { + elem: "input", + type: "checkbox", + label: "Multiline" + } + } + }, + + _setup: function( options ) { + + var target, + text, + container = options._container = document.createElement( "div" ); + + container.style.display = "none"; + + if ( options.target ) { + // Try to use supplied target + target = Popcorn.dom.find( options.target ); + + if ( !target ) { + target = createSubtitleContainer( this, options.target ); + } + else if ( [ "VIDEO", "AUDIO" ].indexOf( target.nodeName ) > -1 ) { + target = createSubtitleContainer( this, options.target + "-overlay" ); + } + + } else if ( !this.container ) { + // Creates a div for all subtitles to use + target = createSubtitleContainer( this ); + + } else { + // Use subtitle container + target = this.container; + } + + // cache reference to actual target container + options._target = target; + + // Escape HTML text if requested + text = !!options.escape ? escapeHTML( options.text ) : + options.text; + + // Swap newline for
if requested + text = !!options.multiline ? newlineToBreak ( text ) : text; + container.innerHTML = text || ""; + + target.appendChild( container ); + }, + + /** + * @member text + * The start function will be executed when the currentTime + * of the video reaches the start time provided by the + * options variable + */ + start: function( event, options ) { + options._container.style.display = "inline"; + }, + + /** + * @member text + * The end function will be executed when the currentTime + * of the video reaches the end time provided by the + * options variable + */ + end: function( event, options ) { + options._container.style.display = "none"; + }, + + _teardown: function( options ) { + var target = options._target; + if ( target ) { + target.removeChild( options._container ); + } + } + }); +})( Popcorn ); + +// PLUGIN: Google Maps +var googleCallback; +(function ( Popcorn ) { + + var i = 1, + _mapFired = false, + _mapLoaded = false, + geocoder, loadMaps; + //google api callback + googleCallback = function ( data ) { + // ensure all of the maps functions needed are loaded + // before setting _maploaded to true + if ( typeof google !== "undefined" && google.maps && google.maps.Geocoder && google.maps.LatLng ) { + geocoder = new google.maps.Geocoder(); + Popcorn.getScript( "//maps.stamen.com/js/tile.stamen.js", function() { + _mapLoaded = true; + }); + } else { + setTimeout(function () { + googleCallback( data ); + }, 1); + } + }; + // function that loads the google api + loadMaps = function () { + // for some reason the Google Map API adds content to the body + if ( document.body ) { + _mapFired = true; + Popcorn.getScript( "//maps.google.com/maps/api/js?sensor=false&callback=googleCallback" ); + } else { + setTimeout(function () { + loadMaps(); + }, 1); + } + }; + + function buildMap( options, location, mapDiv ) { + var type = options.type ? options.type.toUpperCase() : "HYBRID", + layer; + + // See if we need to make a custom Stamen map layer + if ( type === "STAMEN-WATERCOLOR" || + type === "STAMEN-TERRAIN" || + type === "STAMEN-TONER" ) { + // Stamen types are lowercase + layer = type.replace("STAMEN-", "").toLowerCase(); + } + + var map = new google.maps.Map( mapDiv, { + // If a custom layer was specified, use that, otherwise use type + mapTypeId: layer ? layer : google.maps.MapTypeId[ type ], + // Hide the layer selection UI + mapTypeControlOptions: { mapTypeIds: [] } + }); + + if ( layer ) { + map.mapTypes.set( layer, new google.maps.StamenMapType( layer )); + } + map.getDiv().style.display = "none"; + + return map; + } + + /** + * googlemap popcorn plug-in + * Adds a map to the target div centered on the location specified by the user + * Options parameter will need a start, end, target, type, zoom, lat and lng, and location + * -Start is the time that you want this plug-in to execute + * -End is the time that you want this plug-in to stop executing + * -Target is the id of the DOM element that you want the map to appear in. This element must be in the DOM + * -Type [optional] either: HYBRID (default), ROADMAP, SATELLITE, TERRAIN, STREETVIEW, or one of the + * Stamen custom map types (http://http://maps.stamen.com): STAMEN-TONER, + * STAMEN-WATERCOLOR, or STAMEN-TERRAIN. + * -Zoom [optional] defaults to 0 + * -Heading [optional] STREETVIEW orientation of camera in degrees relative to true north (0 north, 90 true east, ect) + * -Pitch [optional] STREETVIEW vertical orientation of the camera (between 1 and 3 is recommended) + * -Lat and Lng: the coordinates of the map must be present if location is not specified. + * -Height [optional] the height of the map, in "px" or "%". Defaults to "100%". + * -Width [optional] the width of the map, in "px" or "%". Defaults to "100%". + * -Location: the adress you want the map to display, must be present if lat and lng are not specified. + * Note: using location requires extra loading time, also not specifying both lat/lng and location will + * cause and error. + * + * Tweening works using the following specifications: + * -location is the start point when using an auto generated route + * -tween when used in this context is a string which specifies the end location for your route + * Note that both location and tween must be present when using an auto generated route, or the map will not tween + * -interval is the speed in which the tween will be executed, a reasonable time is 1000 ( time in milliseconds ) + * Heading, Zoom, and Pitch streetview values are also used in tweening with the autogenerated route + * + * -tween is an array of objects, each containing data for one frame of a tween + * -position is an object with has two paramaters, lat and lng, both which are mandatory for a tween to work + * -pov is an object which houses heading, pitch, and zoom paramters, which are all optional, if undefined, these values default to 0 + * -interval is the speed in which the tween will be executed, a reasonable time is 1000 ( time in milliseconds ) + * + * @param {Object} options + * + * Example: + var p = Popcorn("#video") + .googlemap({ + start: 5, // seconds + end: 15, // seconds + type: "ROADMAP", + target: "map", + lat: 43.665429, + lng: -79.403323 + } ) + * + */ + Popcorn.plugin( "googlemap", function ( options ) { + var newdiv, map, location, + target = document.getElementById( options.target ); + + options.type = options.type || "ROADMAP"; + options.zoom = options.zoom || 1; + options.lat = options.lat || 0; + options.lng = options.lng || 0; + + // if this is the firest time running the plugins + // call the function that gets the sctipt + if ( !_mapFired ) { + loadMaps(); + } + + // create a new div this way anything in the target div is left intact + // this is later passed on to the maps api + newdiv = document.createElement( "div" ); + newdiv.id = "actualmap" + i; + newdiv.style.width = options.width || "100%"; + + // height is a little more complicated than width. + if ( options.height ) { + newdiv.style.height = options.height; + } else if ( target && target.clientHeight ) { + newdiv.style.height = target.clientHeight + "px"; + } else { + newdiv.style.height = "100%"; + } + + i++; + + target && target.appendChild( newdiv ); + + // ensure that google maps and its functions are loaded + // before setting up the map parameters + var isMapReady = function () { + if ( _mapLoaded ) { + if ( newdiv ) { + if ( options.location ) { + // calls an anonymous google function called on separate thread + geocoder.geocode({ + "address": options.location + }, function ( results, status ) { + // second check for newdiv since it could have disappeared before + // this callback is actual run + if ( newdiv && status === google.maps.GeocoderStatus.OK ) { + options.lat = results[ 0 ].geometry.location.lat(); + options.lng = results[ 0 ].geometry.location.lng(); + location = new google.maps.LatLng( options.lat, options.lng ); + map = buildMap( options, location, newdiv ); + } + }); + } else { + location = new google.maps.LatLng( options.lat, options.lng ); + map = buildMap( options, location, newdiv ); + } + } + } else { + setTimeout(function () { + isMapReady(); + }, 5); + } + }; + + isMapReady(); + + return { + /** + * @member webpage + * The start function will be executed when the currentTime + * of the video reaches the start time provided by the + * options variable + */ + start: function( event, options ) { + var that = this, + sView; + + // ensure the map has been initialized in the setup function above + var isMapSetup = function() { + if ( map ) { + options._map = map; + + map.getDiv().style.display = "block"; + // reset the location and zoom just in case the user plaid with the map + google.maps.event.trigger( map, "resize" ); + map.setCenter( location ); + + // make sure options.zoom is a number + if ( options.zoom && typeof options.zoom !== "number" ) { + options.zoom = +options.zoom; + } + + map.setZoom( options.zoom ); + + //Make sure heading is a number + if ( options.heading && typeof options.heading !== "number" ) { + options.heading = +options.heading; + } + //Make sure pitch is a number + if ( options.pitch && typeof options.pitch !== "number" ) { + options.pitch = +options.pitch; + } + + if ( options.type === "STREETVIEW" ) { + // Switch this map into streeview mode + map.setStreetView( + // Pass a new StreetViewPanorama instance into our map + + sView = new google.maps.StreetViewPanorama( newdiv, { + position: location, + pov: { + heading: options.heading = options.heading || 0, + pitch: options.pitch = options.pitch || 0, + zoom: options.zoom + } + }) + ); + + // Function to handle tweening using a set timeout + var tween = function( rM, t ) { + + var computeHeading = google.maps.geometry.spherical.computeHeading; + setTimeout(function() { + + var current_time = that.media.currentTime; + + // Checks whether this is a generated route or not + if ( typeof options.tween === "object" ) { + + for ( var i = 0, m = rM.length; i < m; i++ ) { + + var waypoint = rM[ i ]; + + // Checks if this position along the tween should be displayed or not + if ( current_time >= ( waypoint.interval * ( i + 1 ) ) / 1000 && + ( current_time <= ( waypoint.interval * ( i + 2 ) ) / 1000 || + current_time >= waypoint.interval * ( m ) / 1000 ) ) { + + sView3.setPosition( new google.maps.LatLng( waypoint.position.lat, waypoint.position.lng ) ); + + sView3.setPov({ + heading: waypoint.pov.heading || computeHeading( waypoint, rM[ i + 1 ] ) || 0, + zoom: waypoint.pov.zoom || 0, + pitch: waypoint.pov.pitch || 0 + }); + } + } + + // Calls the tween function again at the interval set by the user + tween( rM, rM[ 0 ].interval ); + } else { + + for ( var k = 0, l = rM.length; k < l; k++ ) { + + var interval = options.interval; + + if( current_time >= (interval * ( k + 1 ) ) / 1000 && + ( current_time <= (interval * ( k + 2 ) ) / 1000 || + current_time >= interval * ( l ) / 1000 ) ) { + + sView2.setPov({ + heading: computeHeading( rM[ k ], rM[ k + 1 ] ) || 0, + zoom: options.zoom, + pitch: options.pitch || 0 + }); + sView2.setPosition( checkpoints[ k ] ); + } + } + + tween( checkpoints, options.interval ); + } + }, t ); + }; + + // Determines if we should use hardcoded values ( using options.tween ), + // or if we should use a start and end location and let google generate + // the route for us + if ( options.location && typeof options.tween === "string" ) { + + // Creating another variable to hold the streetview map for tweening, + // Doing this because if there was more then one streetview map, the tweening would sometimes appear in other maps + var sView2 = sView; + + // Create an array to store all the lat/lang values along our route + var checkpoints = []; + + // Creates a new direction service, later used to create a route + var directionsService = new google.maps.DirectionsService(); + + // Creates a new direction renderer using the current map + // This enables us to access all of the route data that is returned to us + var directionsDisplay = new google.maps.DirectionsRenderer( sView2 ); + + var request = { + origin: options.location, + destination: options.tween, + travelMode: google.maps.TravelMode.DRIVING + }; + + // Create the route using the direction service and renderer + directionsService.route( request, function( response, status ) { + + if ( status == google.maps.DirectionsStatus.OK ) { + directionsDisplay.setDirections( response ); + showSteps( response, that ); + } + + }); + + var showSteps = function ( directionResult, that ) { + + // Push new google map lat and lng values into an array from our list of lat and lng values + var routes = directionResult.routes[ 0 ].overview_path; + for ( var j = 0, k = routes.length; j < k; j++ ) { + checkpoints.push( new google.maps.LatLng( routes[ j ].lat(), routes[ j ].lng() ) ); + } + + // Check to make sure the interval exists, if not, set to a default of 1000 + options.interval = options.interval || 1000; + tween( checkpoints, 10 ); + + }; + } else if ( typeof options.tween === "object" ) { + + // Same as the above to stop streetview maps from overflowing into one another + var sView3 = sView; + + for ( var i = 0, l = options.tween.length; i < l; i++ ) { + + // Make sure interval exists, if not, set to 1000 + options.tween[ i ].interval = options.tween[ i ].interval || 1000; + tween( options.tween, 10 ); + } + } + } + + if ( options.onmaploaded ) { + options.onmaploaded( options, map ); + } + + } else { + setTimeout(function () { + isMapSetup(); + }, 13); + } + + }; + isMapSetup(); + }, + /** + * @member webpage + * The end function will be executed when the currentTime + * of the video reaches the end time provided by the + * options variable + */ + end: function ( event, options ) { + // if the map exists hide it do not delete the map just in + // case the user seeks back to time b/w start and end + if ( map ) { + map.getDiv().style.display = "none"; + } + }, + _teardown: function ( options ) { + + var target = document.getElementById( options.target ); + + // the map must be manually removed + target && target.removeChild( newdiv ); + newdiv = map = location = null; + + options._map = null; + } + }; + }, { + about: { + name: "Popcorn Google Map Plugin", + version: "0.1", + author: "@annasob", + website: "annasob.wordpress.com" + }, + options: { + start: { + elem: "input", + type: "start", + label: "Start" + }, + end: { + elem: "input", + type: "start", + label: "End" + }, + target: "map-container", + type: { + elem: "select", + options: [ "ROADMAP", "SATELLITE", "STREETVIEW", "HYBRID", "TERRAIN", "STAMEN-WATERCOLOR", "STAMEN-TERRAIN", "STAMEN-TONER" ], + label: "Map Type", + optional: true + }, + zoom: { + elem: "input", + type: "text", + label: "Zoom", + "default": 0, + optional: true + }, + lat: { + elem: "input", + type: "text", + label: "Lat", + optional: true + }, + lng: { + elem: "input", + type: "text", + label: "Lng", + optional: true + }, + location: { + elem: "input", + type: "text", + label: "Location", + "default": "Toronto, Ontario, Canada" + }, + heading: { + elem: "input", + type: "text", + label: "Heading", + "default": 0, + optional: true + }, + pitch: { + elem: "input", + type: "text", + label: "Pitch", + "default": 1, + optional: true + } + } + }); +})( Popcorn ); + +// PLUGIN: IMAGE + +(function ( Popcorn ) { + +/** + * Images popcorn plug-in + * Shows an image element + * Options parameter will need a start, end, href, target and src. + * Start is the time that you want this plug-in to execute + * End is the time that you want this plug-in to stop executing + * href is the url of the destination of a anchor - optional + * Target is the id of the document element that the iframe needs to be attached to, + * this target element must exist on the DOM + * Src is the url of the image that you want to display + * text is the overlayed text on the image - optional + * + * @param {Object} options + * + * Example: + var p = Popcorn('#video') + .image({ + start: 5, // seconds + end: 15, // seconds + href: 'http://www.drumbeat.org/', + src: 'http://www.drumbeat.org/sites/default/files/domain-2/drumbeat_logo.png', + text: 'DRUMBEAT', + target: 'imagediv' + } ) + * + */ + + var VIDEO_OVERLAY_Z = 2000, + CHECK_INTERVAL_DURATION = 10; + + function trackMediaElement( mediaElement ) { + var checkInterval = -1, + container = document.createElement( "div" ), + videoZ = getComputedStyle( mediaElement ).zIndex; + + container.setAttribute( "data-popcorn-helper-container", true ); + + container.style.position = "absolute"; + + if ( !isNaN( videoZ ) ) { + container.style.zIndex = videoZ + 1; + } + else { + container.style.zIndex = VIDEO_OVERLAY_Z; + } + + document.body.appendChild( container ); + + function check() { + var mediaRect = mediaElement.getBoundingClientRect(), + containerRect = container.getBoundingClientRect(); + + if ( containerRect.left !== mediaRect.left ) { + container.style.left = mediaRect.left + "px"; + } + if ( containerRect.top !== mediaRect.top ) { + container.style.top = mediaRect.top + "px"; + } + } + + return { + element: container, + start: function() { + checkInterval = setInterval( check, CHECK_INTERVAL_DURATION ); + }, + stop: function() { + clearInterval( checkInterval ); + checkInterval = -1; + }, + destroy: function() { + document.body.removeChild( container ); + if ( checkInterval !== -1 ) { + clearInterval( checkInterval ); + } + } + }; + } + + Popcorn.plugin( "image", { + manifest: { + about: { + name: "Popcorn image Plugin", + version: "0.1", + author: "Scott Downe", + website: "http://scottdowne.wordpress.com/" + }, + options: { + start: { + elem: "input", + type: "number", + label: "Start" + }, + end: { + elem: "input", + type: "number", + label: "End" + }, + src: { + elem: "input", + type: "url", + label: "Image URL", + "default": "http://mozillapopcorn.org/wp-content/themes/popcorn/images/for_developers.png" + }, + href: { + elem: "input", + type: "url", + label: "Link", + "default": "http://mozillapopcorn.org/wp-content/themes/popcorn/images/for_developers.png", + optional: true + }, + target: "image-container", + text: { + elem: "input", + type: "text", + label: "Caption", + "default": "Popcorn.js", + optional: true + } + } + }, + _setup: function( options ) { + var img = document.createElement( "img" ), + target = document.getElementById( options.target ); + + options.anchor = document.createElement( "a" ); + options.anchor.style.position = "relative"; + options.anchor.style.textDecoration = "none"; + options.anchor.style.display = "none"; + + // add the widget's div to the target div. + // if target is