Skip to content

Instantly share code, notes, and snippets.

@cadecairos
Created July 28, 2011 20:35
Show Gist options
  • Save cadecairos/1112489 to your computer and use it in GitHub Desktop.
Save cadecairos/1112489 to your computer and use it in GitHub Desktop.
popcorn complete
/*
* popcorn.js version abd0127
* http://popcornjs.org
*
* Copyright 2011, Mozilla Foundation
* Licensed under the MIT license
*/
(function(global, document) {
// Cache refs to speed up calls to native utils
var
AP = Array.prototype,
OP = Object.prototype,
forEach = AP.forEach,
slice = AP.slice,
hasOwn = OP.hasOwnProperty,
toString = OP.toString,
// ID string matching
rIdExp = /^(#([\w\-\_\.]+))$/,
// Ready fn cache
readyStack = [],
readyBound = false,
readyFired = false,
// Non-public internal data object
internal = {
events: {
hash: {},
apis: {}
}
},
// Declare constructor
// Returns an instance object.
Popcorn = function( entity, options ) {
// Return new Popcorn object
return new Popcorn.p.init( entity, options || null );
};
// Instance caching
Popcorn.instances = [];
Popcorn.instanceIds = {};
Popcorn.removeInstance = function( instance ) {
// If called prior to any instances being created
// Return early to avoid splicing on nothing
if ( !Popcorn.instances.length ) {
return;
}
// Remove instance from Popcorn.instances
Popcorn.instances.splice( Popcorn.instanceIds[ instance.id ], 1 );
// Delete the instance id key
delete Popcorn.instanceIds[ instance.id ];
// Return current modified instances
return Popcorn.instances;
};
// Addes a Popcorn instance to the Popcorn instance array
Popcorn.addInstance = function( instance ) {
var instanceLen = Popcorn.instances.length,
instanceId = instance.media.id && instance.media.id;
// If the media element has its own `id` use it, otherwise provide one
// Ensure that instances have unique ids and unique entries
// Uses `in` operator to avoid false positives on 0
instance.id = !( instanceId in Popcorn.instanceIds ) && instanceId ||
"__popcorn" + instanceLen;
// Create a reference entry for this instance
Popcorn.instanceIds[ instance.id ] = instanceLen;
// Add this instance to the cache
Popcorn.instances.push( instance );
// Return the current modified instances
return Popcorn.instances;
};
// Request Popcorn object instance by id
Popcorn.getInstanceById = function( id ) {
return Popcorn.instances[ Popcorn.instanceIds[ id ] ];
};
// Remove Popcorn object instance by id
Popcorn.removeInstanceById = function( id ) {
return Popcorn.removeInstance( Popcorn.instances[ Popcorn.instanceIds[ id ] ] );
};
// Declare a shortcut (Popcorn.p) to and a definition of
// the new prototype for our Popcorn constructor
Popcorn.p = Popcorn.prototype = {
init: function( entity, options ) {
var matches;
// Supports Popcorn(function () { /../ })
// Originally proposed by Daniel Brooks
if ( typeof entity === "function" ) {
// If document ready has already fired
if ( document.readyState === "interactive" || document.readyState === "complete" ) {
entity( document, Popcorn );
return;
}
// Add `entity` fn to ready stack
readyStack.push( entity );
// This process should happen once per page load
if ( !readyBound ) {
// set readyBound flag
readyBound = true;
var DOMContentLoaded = function() {
readyFired = true;
// Remove global DOM ready listener
document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
// Execute all ready function in the stack
for ( var i = 0, readyStackLength = readyStack.length; i < readyStackLength; i++ ) {
readyStack[ i ].call( document, Popcorn );
}
// GC readyStack
readyStack = null;
};
// Register global DOM ready listener
document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
}
return;
}
// Check if entity is a valid string id
matches = rIdExp.exec( entity );
// Get media element by id or object reference
this.media = matches && matches.length && matches[ 2 ] ?
document.getElementById( matches[ 2 ] ) :
entity;
// Create an audio or video element property reference
this[ ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video" ] = this.media;
// Register new instance
Popcorn.addInstance( this );
this.options = options || {};
this.data = {
// Allows disabling a plugin per instance
disabled: [],
// Stores DOM event queues by type
events: {},
// Stores Special event hooks data
hooks: {},
// Store track event history data
history: [],
// Store track event object references by trackId
trackRefs: {},
// Playback track event queues
trackEvents: {
byStart: [{
start: -1,
end: -1
}],
byEnd: [{
start: -1,
end: -1
}],
startIndex: 0,
endIndex: 0,
previousUpdateTime: -1
}
};
// Wrap true ready check
var isReady = function( that ) {
if ( that.media.readyState >= 2 ) {
// Adding padding to the front and end of the arrays
// this is so we do not fall off either end
var duration = that.media.duration,
// Check for no duration info (NaN)
videoDurationPlus = duration != duration ? Number.MAX_VALUE : duration + 1;
Popcorn.addTrackEvent( that, {
start: videoDurationPlus,
end: videoDurationPlus
});
that.media.addEventListener( "timeupdate", function( event ) {
var currentTime = this.currentTime,
previousTime = that.data.trackEvents.previousUpdateTime,
tracks = that.data.trackEvents,
tracksByEnd = tracks.byEnd,
tracksByStart = tracks.byStart;
// Playbar advancing
if ( previousTime < currentTime ) {
while ( tracksByEnd[ tracks.endIndex ] && tracksByEnd[ tracks.endIndex ].end <= currentTime ) {
// If plugin does not exist on this instance, remove it
if ( !tracksByEnd[ tracks.endIndex ]._natives || !!that[ tracksByEnd[ tracks.endIndex ]._natives.type ] ) {
if ( tracksByEnd[ tracks.endIndex ]._running === true ) {
tracksByEnd[ tracks.endIndex ]._running = false;
tracksByEnd[ tracks.endIndex ]._natives.end.call( that, event, tracksByEnd[ tracks.endIndex ] );
}
tracks.endIndex++;
} else {
// remove track event
Popcorn.removeTrackEvent( that, tracksByEnd[ tracks.endIndex ]._id );
return;
}
}
while ( tracksByStart[ tracks.startIndex ] && tracksByStart[ tracks.startIndex ].start <= currentTime ) {
// If plugin does not exist on this instance, remove it
if ( !tracksByStart[ tracks.startIndex ]._natives || !!that[ tracksByStart[ tracks.startIndex ]._natives.type ] ) {
if ( tracksByStart[ tracks.startIndex ].end > currentTime &&
tracksByStart[ tracks.startIndex ]._running === false &&
that.data.disabled.indexOf( tracksByStart[ tracks.startIndex ]._natives.type ) === -1 ) {
tracksByStart[ tracks.startIndex ]._running = true;
tracksByStart[ tracks.startIndex ]._natives.start.call( that, event, tracksByStart[ tracks.startIndex ] );
}
tracks.startIndex++;
} else {
// remove track event
Popcorn.removeTrackEvent( that, tracksByStart[ tracks.startIndex ]._id );
return;
}
}
// Playbar receding
} else if ( previousTime > currentTime ) {
while ( tracksByStart[ tracks.startIndex ] && tracksByStart[ tracks.startIndex ].start > currentTime ) {
// if plugin does not exist on this instance, remove it
if ( !tracksByStart[ tracks.startIndex ]._natives || !!that[ tracksByStart[ tracks.startIndex ]._natives.type ] ) {
if ( tracksByStart[ tracks.startIndex ]._running === true ) {
tracksByStart[ tracks.startIndex ]._running = false;
tracksByStart[ tracks.startIndex ]._natives.end.call( that, event, tracksByStart[ tracks.startIndex ] );
}
tracks.startIndex--;
} else {
// remove track event
Popcorn.removeTrackEvent( that, tracksByStart[ tracks.startIndex ]._id );
return;
}
}
while ( tracksByEnd[ tracks.endIndex ] && tracksByEnd[ tracks.endIndex ].end > currentTime ) {
// if plugin does not exist on this instance, remove it
if ( !tracksByEnd[ tracks.endIndex ]._natives || !!that[ tracksByEnd[ tracks.endIndex ]._natives.type ] ) {
if ( tracksByEnd[ tracks.endIndex ].start <= currentTime &&
tracksByEnd[ tracks.endIndex ]._running === false &&
that.data.disabled.indexOf( tracksByEnd[ tracks.endIndex ]._natives.type ) === -1 ) {
tracksByEnd[ tracks.endIndex ]._running = true;
tracksByEnd[ tracks.endIndex ]._natives.start.call( that, event, tracksByEnd[tracks.endIndex] );
}
tracks.endIndex--;
} else {
// remove track event
Popcorn.removeTrackEvent( that, tracksByEnd[ tracks.endIndex ]._id );
return;
}
}
}
tracks.previousUpdateTime = currentTime;
}, false );
} else {
global.setTimeout(function() {
isReady( that );
}, 1 );
}
};
isReady( this );
return this;
}
};
// Extend constructor prototype to instance prototype
// Allows chaining methods to instances
Popcorn.p.init.prototype = Popcorn.p;
Popcorn.forEach = function( obj, fn, context ) {
if ( !obj || !fn ) {
return {};
}
context = context || this;
var key, len;
// Use native whenever possible
if ( forEach && obj.forEach === forEach ) {
return obj.forEach( fn, context );
}
if ( toString.call( obj ) === "[object NodeList]" ) {
for ( key = 0, len = obj.length; key < len; key++ ) {
fn.call( context, obj[ key ], key, obj );
}
return obj;
}
for ( key in obj ) {
if ( hasOwn.call( obj, key ) ) {
fn.call( context, obj[ key ], key, obj );
}
}
return obj;
};
Popcorn.extend = function( obj ) {
var dest = obj, src = slice.call( arguments, 1 );
Popcorn.forEach( src, function( copy ) {
for ( var prop in copy ) {
dest[ prop ] = copy[ prop ];
}
});
return dest;
};
// A Few reusable utils, memoized onto Popcorn
Popcorn.extend( Popcorn, {
error: function( msg ) {
throw new Error( msg );
},
guid: function( prefix ) {
Popcorn.guid.counter++;
return ( prefix ? prefix : "" ) + ( +new Date() + Popcorn.guid.counter );
},
sizeOf: function( obj ) {
var size = 0;
for ( var prop in obj ) {
size++;
}
return size;
},
isArray: Array.isArray || function( array ) {
return toString.call( array ) === "[object Array]";
},
nop: function() {},
position: function( elem ) {
var clientRect = elem.getBoundingClientRect(),
bounds = {},
doc = elem.ownerDocument,
docElem = document.documentElement,
body = document.body,
clientTop, clientLeft, scrollTop, scrollLeft, top, left;
// Determine correct clientTop/Left
clientTop = docElem.clientTop || body.clientTop || 0;
clientLeft = docElem.clientLeft || body.clientLeft || 0;
// Determine correct scrollTop/Left
scrollTop = ( global.pageYOffset && docElem.scrollTop || body.scrollTop );
scrollLeft = ( global.pageXOffset && docElem.scrollLeft || body.scrollLeft );
// Temp top/left
top = Math.ceil( clientRect.top + scrollTop - clientTop );
left = Math.ceil( clientRect.left + scrollLeft - clientLeft );
for ( var p in clientRect ) {
bounds[ p ] = Math.round( clientRect[ p ] );
}
return Popcorn.extend({}, bounds, { top: top, left: left });
},
disable: function( instance, plugin ) {
var disabled = instance.data.disabled;
if ( disabled.indexOf( plugin ) === -1 ) {
disabled.push( plugin );
}
return instance;
},
enable: function( instance, plugin ) {
var disabled = instance.data.disabled,
index = disabled.indexOf( plugin );
if ( index > -1 ) {
disabled.splice( index, 1 );
}
return instance;
}
});
// 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 mute volume duration",
ret = {};
// Build methods, store in object that is returned and passed to extend
Popcorn.forEach( methods.split( /\s+/g ), function( name ) {
ret[ name ] = function( arg ) {
if ( typeof this.media[ name ] === "function" ) {
this.media[ name ]();
return this;
}
if ( arg !== false && arg !== null && typeof arg !== "undefined" ) {
this.media[ name ] = arg;
return this;
}
return this.media[ name ];
};
});
return ret;
})()
);
Popcorn.forEach( "enable disable".split(" "), function( method ) {
Popcorn.p[ method ] = function( plugin ) {
return Popcorn[ method ]( this, plugin );
};
});
Popcorn.extend(Popcorn.p, {
// Rounded currentTime
roundTime: function() {
return -~this.media.currentTime;
},
// Attach an event to a single point in time
exec: function( time, fn ) {
// Creating a one second track event with an empty end
Popcorn.addTrackEvent( this, {
start: time,
end: time + 1,
_running: false,
_natives: {
start: fn || Popcorn.nop,
end: Popcorn.nop,
type: "exec"
}
});
return this;
},
// Get the client bounding box of an instance element
position: function() {
return Popcorn.position( this.media );
},
// Toggle a plugin's playback behaviour (on or off) per instance
toggle: function( plugin ) {
return Popcorn[ this.data.disabled.indexOf( plugin ) > -1 ? "enable" : "disable" ]( this, plugin );
},
// Set default values for plugin options objects per instance
defaults: function( plugin, defaults ) {
// If an array of default configurations is provided,
// iterate and apply each to this instance
if ( Popcorn.isArray( plugin ) ) {
Popcorn.forEach( plugin, function( obj ) {
for ( var name in obj ) {
this.defaults( name, obj[ name ] );
}
}, this );
return this;
}
if ( !this.options.defaults ) {
this.options.defaults = {};
}
if ( !this.options.defaults[ plugin ] ) {
this.options.defaults[ plugin ] = {};
}
Popcorn.extend( this.options.defaults[ plugin ], defaults );
return this;
}
});
Popcorn.Events = {
UIEvents: "blur focus focusin focusout load resize scroll unload",
MouseEvents: "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave click dblclick",
Events: "loadstart progress suspend emptied stalled play pause " +
"loadedmetadata loadeddata waiting playing canplay canplaythrough " +
"seeking seeked timeupdate ended ratechange durationchange volumechange"
};
Popcorn.Events.Natives = Popcorn.Events.UIEvents + " " +
Popcorn.Events.MouseEvents + " " +
Popcorn.Events.Events;
internal.events.apiTypes = [ "UIEvents", "MouseEvents", "Events" ];
// Privately compile events table at load time
(function( events, data ) {
var apis = internal.events.apiTypes,
eventsList = events.Natives.split( /\s+/g ),
idx = 0, len = eventsList.length, prop;
for( ; idx < len; idx++ ) {
data.hash[ eventsList[idx] ] = true;
}
apis.forEach(function( val, idx ) {
data.apis[ val ] = {};
var apiEvents = events[ val ].split( /\s+/g ),
len = apiEvents.length,
k = 0;
for ( ; k < len; k++ ) {
data.apis[ val ][ apiEvents[ k ] ] = true;
}
});
})( Popcorn.Events, internal.events );
Popcorn.events = {
isNative: function( type ) {
return !!internal.events.hash[ type ];
},
getInterface: function( type ) {
if ( !Popcorn.events.isNative( type ) ) {
return false;
}
var eventApi = internal.events,
apis = eventApi.apiTypes,
apihash = eventApi.apis,
idx = 0, len = apis.length, api, tmp;
for ( ; idx < len; idx++ ) {
tmp = apis[ idx ];
if ( apihash[ tmp ][ type ] ) {
api = tmp;
break;
}
}
return api;
},
// Compile all native events to single array
all: Popcorn.Events.Natives.split( /\s+/g ),
// Defines all Event handling static functions
fn: {
trigger: function( type, data ) {
var eventInterface, evt;
// setup checks for custom event system
if ( this.data.events[ type ] && Popcorn.sizeOf( this.data.events[ type ] ) ) {
eventInterface = Popcorn.events.getInterface( type );
if ( eventInterface ) {
evt = document.createEvent( eventInterface );
evt.initEvent( type, true, true, global, 1 );
this.media.dispatchEvent( evt );
return this;
}
// Custom events
Popcorn.forEach( this.data.events[ type ], function( obj, key ) {
obj.call( this, data );
}, this );
}
return this;
},
listen: function( type, fn ) {
var self = this,
hasEvents = true,
eventHook = Popcorn.events.hooks[ type ],
origType = type,
tmp;
if ( !this.data.events[ type ] ) {
this.data.events[ type ] = {};
hasEvents = false;
}
// Check and setup event hooks
if ( eventHook ) {
// Execute hook add method if defined
if ( eventHook.add ) {
eventHook.add.call( this, {}, fn );
}
// Reassign event type to our piggyback event type if defined
if ( eventHook.bind ) {
type = eventHook.bind;
}
// Reassign handler if defined
if ( eventHook.handler ) {
tmp = fn;
fn = function wrapper( event ) {
eventHook.handler.call( self, event, tmp );
};
}
// assume the piggy back event is registered
hasEvents = true;
// Setup event registry entry
if ( !this.data.events[ type ] ) {
this.data.events[ type ] = {};
// Toggle if the previous assumption was untrue
hasEvents = false;
}
}
// Register event and handler
this.data.events[ type ][ fn.name || ( fn.toString() + Popcorn.guid() ) ] = fn;
// only attach one event of any type
if ( !hasEvents && Popcorn.events.all.indexOf( type ) > -1 ) {
this.media.addEventListener( type, function( event ) {
Popcorn.forEach( self.data.events[ type ], function( obj, key ) {
if ( typeof obj === "function" ) {
obj.call( self, event );
}
});
}, false);
}
return this;
},
unlisten: function( type, fn ) {
if ( this.data.events[ type ] && this.data.events[ type ][ fn ] ) {
delete this.data.events[ type ][ fn ];
return this;
}
this.data.events[ type ] = null;
return this;
}
},
hooks: {
canplayall: {
bind: "canplaythrough",
add: function( event, callback ) {
var state = false;
if ( this.media.readyState ) {
callback.call( this, event );
state = true;
}
this.data.hooks.canplayall = {
fired: state
};
},
// declare special handling instructions
handler: function canplayall( event, callback ) {
if ( !this.data.hooks.canplayall.fired ) {
// trigger original user callback once
callback.call( this, event );
this.data.hooks.canplayall.fired = true;
}
}
}
}
};
// Extend Popcorn.events.fns (listen, unlisten, trigger) to all Popcorn instances
Popcorn.forEach( [ "trigger", "listen", "unlisten" ], function( key ) {
Popcorn.p[ key ] = Popcorn.events.fn[ key ];
});
// Protected API methods
Popcorn.protect = {
natives: "load play pause currentTime playbackRate mute volume duration removePlugin roundTime trigger listen unlisten exec".toLowerCase().split( /\s+/ )
};
// Internal Only - Adds track events to the instance object
Popcorn.addTrackEvent = function( obj, track ) {
// Determine if this track has default options set for it
// If so, apply them to the track object
if ( track && track._natives && track._natives.type &&
( obj.options.defaults && obj.options.defaults[ track._natives.type ] ) ) {
track = Popcorn.extend( {}, obj.options.defaults[ track._natives.type ], track );
}
if ( track._natives ) {
// Supports user defined track event id
track._id = !track.id ? Popcorn.guid( track._natives.type ) : track.id;
// Push track event ids into the history
obj.data.history.push( track._id );
}
track.start = Popcorn.util.toSeconds( track.start, obj.options.framerate );
track.end = Popcorn.util.toSeconds( track.end, obj.options.framerate );
// Store this definition in an array sorted by times
var byStart = obj.data.trackEvents.byStart,
byEnd = obj.data.trackEvents.byEnd,
idx;
for ( idx = byStart.length - 1; idx >= 0; idx-- ) {
if ( track.start >= byStart[ idx ].start ) {
byStart.splice( idx + 1, 0, track );
break;
}
}
for ( idx = byEnd.length - 1; idx >= 0; idx-- ) {
if ( track.end > byEnd[ idx ].end ) {
byEnd.splice( idx + 1, 0, track );
break;
}
}
// Store references to user added trackevents in ref table
if ( track._id ) {
Popcorn.addTrackEvent.ref( obj, track );
}
};
// Internal Only - Adds track event references to the instance object's trackRefs hash table
Popcorn.addTrackEvent.ref = function( obj, track ) {
obj.data.trackRefs[ track._id ] = track;
return obj;
};
Popcorn.removeTrackEvent = function( obj, trackId ) {
var historyLen = obj.data.history.length,
indexWasAt = 0,
byStart = [],
byEnd = [],
history = [];
Popcorn.forEach( obj.data.trackEvents.byStart, function( o, i, context ) {
// Preserve the original start/end trackEvents
if ( !o._id ) {
byStart.push( obj.data.trackEvents.byStart[i] );
byEnd.push( obj.data.trackEvents.byEnd[i] );
}
// Filter for user track events (vs system track events)
if ( o._id ) {
// Filter for the trackevent to remove
if ( o._id !== trackId ) {
byStart.push( obj.data.trackEvents.byStart[i] );
byEnd.push( obj.data.trackEvents.byEnd[i] );
}
// Capture the position of the track being removed.
if ( o._id === trackId ) {
indexWasAt = i;
o._natives._teardown && o._natives._teardown.call( obj, o );
}
}
});
// 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;
for ( var i = 0; i < historyLen; i++ ) {
if ( obj.data.history[ i ] !== trackId ) {
history.push( obj.data.history[ i ] );
}
}
// Update ordered history array
obj.data.history = history;
// Update track event references
Popcorn.removeTrackEvent.ref( obj, trackId );
};
// Internal Only - Removes track event references from instance object's trackRefs hash table
Popcorn.removeTrackEvent.ref = function( obj, trackId ) {
delete obj.data.trackRefs[ trackId ];
return obj;
};
// Return an array of track events bound to this instance object
Popcorn.getTrackEvents = function( obj ) {
var trackevents = [],
refs = obj.data.trackEvents.byStart,
length = refs.length,
idx = 0,
ref;
for ( ; idx < length; idx++ ) {
ref = refs[ idx ];
// Return only user attributed track event references
if ( ref._id ) {
trackevents.push( ref );
}
}
return trackevents;
};
// Internal Only - Returns an instance object's trackRefs hash table
Popcorn.getTrackEvents.ref = function( obj ) {
return obj.data.trackRefs;
};
// Return a single track event bound to this instance object
Popcorn.getTrackEvent = function( obj, trackId ) {
return obj.data.trackRefs[ trackId ];
};
// Internal Only - Returns an instance object's track reference by track id
Popcorn.getTrackEvent.ref = function( obj, trackId ) {
return obj.data.trackRefs[ trackId ];
};
Popcorn.getLastTrackEventId = function( obj ) {
return obj.data.history[ obj.data.history.length - 1 ];
};
// 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;
}
});
// 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" ];
// 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
if ( !manifest ) {
manifest = definition.manifest || {};
}
// apply safe, and empty default functions
methods.forEach(function( method ) {
definition[ method ] = definition[ method ] || Popcorn.nop;
});
var pluginFn = function( setup, options ) {
if ( !options ) {
return this;
}
// Storing the plugin natives
var natives = options._natives = {},
compose = "",
defaults, originalOpts, manifestOpts, mergedSetupOpts;
Popcorn.extend( natives, setup );
options._natives.type = name;
options._running = false;
// Check for previously set default options
defaults = this.options.defaults && this.options.defaults[ options._natives && options._natives.type ];
// default to an empty string if no effect exists
// split string into an array of effects
options.compose = options.compose && options.compose.split( " " ) || [];
options.effect = options.effect && options.effect.split( " " ) || [];
// join the two arrays together
options.compose = options.compose.concat( options.effect );
options.compose.forEach(function( composeOption ) {
// if the requested compose is garbage, throw it away
compose = Popcorn.compositions[ composeOption ] || {};
// extends previous functions with compose function
methods.forEach(function( method ) {
natives[ method ] = combineFn( natives[ method ], compose[ method ] );
});
});
// Ensure a manifest object, an empty object is a sufficient fallback
options._natives.manifest = manifest;
// Checks for expected properties
if ( !( "start" in options ) ) {
options.start = 0;
}
if ( !( "end" in options ) ) {
options.end = this.duration() || Number.MAX_VALUE;
}
// Merge with defaults if they exist, make sure per call is prioritized
mergedSetupOpts = defaults ? Popcorn.extend( {}, defaults, options ) :
options;
// Resolves 239, 241, 242
if ( !mergedSetupOpts.target ) {
// Sometimes the manifest may be missing entirely
// or it has an options object that doesn't have a `target` property
manifestOpts = "options" in manifest && manifest.options;
mergedSetupOpts.target = manifestOpts && "target" in manifestOpts && manifestOpts.target;
}
// Trigger _setup method if exists
options._natives._setup && options._natives._setup.call( this, mergedSetupOpts );
// Create new track event for this instance
Popcorn.addTrackEvent( this, Popcorn.extend( mergedSetupOpts, options ) );
// Future support for plugin event definitions
// for all of the native events
Popcorn.forEach( setup, function( callback, type ) {
if ( type !== "type" ) {
if ( reserved.indexOf( type ) === -1 ) {
this.listen( type, callback );
}
}
}, this );
return this;
};
// Augment the manifest object
if ( manifest || ( "manifest" in definition ) ) {
Popcorn.manifest[ name ] = manifest || definition.manifest;
}
// Assign new named definition
plugin[ name ] = function( options ) {
return pluginFn.call( this, isfn ? definition.call( this, options ) : definition,
options );
};
// Extend Popcorn.p with new named definition
Popcorn.extend( Popcorn.p, plugin );
// Push into the registry
var entry = {
fn: plugin[ name ],
definition: definition,
base: definition,
parents: [],
name: name
};
Popcorn.registry.push(
Popcorn.extend( plugin, entry, {
type: name
})
);
Popcorn.registryByName[ name ] = entry;
return plugin;
};
Popcorn.plugin.debug = false;
// removePlugin( type ) removes all tracks of that from all instances of popcorn
// removePlugin( obj, type ) removes all tracks of type from obj, where obj is a single instance of popcorn
Popcorn.removePlugin = function( obj, name ) {
// Check if we are removing plugin from an instance or from all of Popcorn
if ( !name ) {
// Fix the order
name = obj;
obj = Popcorn.p;
if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
Popcorn.error( "'" + name + "' is a protected function name" );
return;
}
var registryLen = Popcorn.registry.length,
registryIdx;
// remove plugin reference from registry
for ( registryIdx = 0; registryIdx < registryLen; registryIdx++ ) {
if ( Popcorn.registry[ registryIdx ].name === name ) {
Popcorn.registry.splice( registryIdx, 1 );
delete Popcorn.registryByName[ name ];
// delete the plugin
delete obj[ name ];
// plugin found and removed, stop checking, we are done
return;
}
}
}
var byStart = obj.data.trackEvents.byStart,
byEnd = obj.data.trackEvents.byEnd,
idx, sl;
// remove all trackEvents
for ( idx = 0, sl = byStart.length; idx < sl; idx++ ) {
if ( ( byStart[ idx ] && byStart[ idx ]._natives && byStart[ idx ]._natives.type === name ) &&
( byEnd[ idx ] && byEnd[ idx ]._natives && byEnd[ idx ]._natives.type === name ) ) {
byStart[ idx ]._natives._teardown && byStart[ idx ]._natives._teardown.call( obj, byStart[ idx ] );
byStart.splice( idx, 1 );
byEnd.splice( idx, 1 );
// update for loop if something removed, but keep checking
idx--; sl--;
if ( obj.data.trackEvents.startIndex <= idx ) {
obj.data.trackEvents.startIndex--;
obj.data.trackEvents.endIndex--;
}
}
}
};
Popcorn.compositions = {};
// Plugin inheritance
Popcorn.compose = function( name, definition, manifest ) {
// If `manifest` arg is undefined, check for manifest within the `definition` object
// If no `definition.manifest`, an empty object is a sufficient fallback
Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {};
// register the effect by name
Popcorn.compositions[ name ] = definition;
};
Popcorn.plugin.effect = Popcorn.effect = Popcorn.compose;
// stores parsers keyed on filetype
Popcorn.parsers = {};
// An interface for extending Popcorn
// with parser functionality
Popcorn.parser = function( name, type, definition ) {
if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
Popcorn.error( "'" + name + "' is a protected function name" );
return;
}
// fixes parameters for overloaded function call
if ( typeof type === "function" && !definition ) {
definition = type;
type = "";
}
if ( typeof definition !== "function" || typeof type !== "string" ) {
return;
}
// Provides some sugar, but ultimately extends
// the definition into Popcorn.p
var natives = Popcorn.events.all,
parseFn,
parser = {};
parseFn = function( filename, callback ) {
if ( !filename ) {
return this;
}
var that = this;
Popcorn.xhr({
url: filename,
dataType: type,
success: function( data ) {
var tracksObject = definition( data ),
tracksData,
tracksDataLen,
tracksDef,
idx = 0;
tracksData = tracksObject.data || [];
tracksDataLen = tracksData.length;
tracksDef = null;
// If no tracks to process, return immediately
if ( !tracksDataLen ) {
return;
}
// Create tracks out of parsed object
for ( ; idx < tracksDataLen; idx++ ) {
tracksDef = tracksData[ idx ];
for ( var key in tracksDef ) {
if ( hasOwn.call( tracksDef, key ) && !!that[ key ] ) {
that[ key ]( tracksDef[ key ] );
}
}
}
if ( callback ) {
callback();
}
}
});
return this;
};
// Assign new named definition
parser[ name ] = parseFn;
// Extend Popcorn.p with new named definition
Popcorn.extend( Popcorn.p, parser );
// keys the function name by filetype extension
//Popcorn.parsers[ name ] = true;
return parser;
};
// Cache references to reused RegExps
var rparams = /\?/,
// XHR Setup object
setup = {
url: "",
data: "",
dataType: "",
success: Popcorn.nop,
type: "GET",
async: true,
xhr: function() {
return new global.XMLHttpRequest();
}
};
Popcorn.xhr = function( options ) {
options.dataType = options.dataType && options.dataType.toLowerCase() || null;
if ( options.dataType &&
( options.dataType === "jsonp" || options.dataType === "script" ) ) {
Popcorn.xhr.getJSONP(
options.url,
options.success,
options.dataType === "script"
);
return;
}
var settings = Popcorn.extend( {}, setup, options );
// Create new XMLHttpRequest object
settings.ajax = settings.xhr();
if ( settings.ajax ) {
if ( settings.type === "GET" && settings.data ) {
// append query string
settings.url += ( rparams.test( settings.url ) ? "&" : "?" ) + settings.data;
// Garbage collect and reset settings.data
settings.data = null;
}
settings.ajax.open( settings.type, settings.url, settings.async );
settings.ajax.send( settings.data || null );
return Popcorn.xhr.httpData( settings );
}
};
Popcorn.xhr.httpData = function( settings ) {
var data, json = null;
settings.ajax.onreadystatechange = function() {
if ( settings.ajax.readyState === 4 ) {
try {
json = JSON.parse( settings.ajax.responseText );
} catch( e ) {
//suppress
}
data = {
xml: settings.ajax.responseXML,
text: settings.ajax.responseText,
json: json
};
// If a dataType was specified, return that type of data
if ( settings.dataType ) {
data = data[ settings.dataType ];
}
settings.success.call( settings.ajax, data );
}
};
return data;
};
Popcorn.xhr.getJSONP = function( url, success, isScript ) {
// If this is a script request, ensure that we do not call something that has already been loaded
if ( isScript ) {
var scripts = document.querySelectorAll( "script[src=\"" + url + "\"]" );
// If there are scripts with this url loaded, early return
if ( scripts.length ) {
// Execute success callback and pass "exists" flag
success && success( true );
return;
}
}
var head = document.head || document.getElementsByTagName( "head" )[ 0 ] || document.documentElement,
script = document.createElement( "script" ),
paramStr = url.split( "?" )[ 1 ],
isFired = false,
params = [],
callback, parts, callparam;
if ( paramStr && !isScript ) {
params = paramStr.split( "&" );
}
if ( params.length ) {
parts = params[ params.length - 1 ].split( "=" );
}
callback = params.length ? ( parts[ 1 ] ? parts[ 1 ] : parts[ 0 ] ) : "jsonp";
if ( !paramStr && !isScript ) {
url += "?callback=" + callback;
}
if ( callback && !isScript ) {
// If a callback name already exists
if ( !!window[ callback ] ) {
// Create a new unique callback name
callback = Popcorn.guid( callback );
}
// Define the JSONP success callback globally
window[ callback ] = function( data ) {
success && success( data );
isFired = true;
};
// Replace callback param and callback name
url = url.replace( parts.join( "=" ), parts[ 0 ] + "=" + callback );
}
script.onload = script.onreadystatechange = function() {
if ( !script.readyState || /loaded|complete/.test( script.readyState ) ) {
// Handling remote script loading callbacks
if ( isScript ) {
// getScript
success && success();
}
// Executing for JSONP requests
if ( isFired ) {
// Garbage collect the callback
delete window[ callback ];
// Garbage collect the script resource
head.removeChild( script );
}
}
};
script.src = url;
head.insertBefore( script, head.firstChild );
return;
};
Popcorn.getJSONP = Popcorn.xhr.getJSONP;
Popcorn.getScript = Popcorn.xhr.getScript = function( url, success ) {
return Popcorn.xhr.getJSONP( url, success, true );
};
Popcorn.util = {
// Simple function to parse a timestamp into seconds
// Acceptable formats are:
// HH:MM:SS.MMM
// HH:MM:SS;FF
// Hours and minutes are optional. They default to 0
toSeconds: function( timeStr, framerate ) {
//Hours and minutes are optional
//Seconds must be specified
//Seconds can be followed by milliseconds OR by the frame information
var validTimeFormat = /^([0-9]+:){0,2}[0-9]+([.;][0-9]+)?$/,
errorMessage = "Invalid time format";
if ( typeof timeStr === "number" ) {
return timeStr;
} else if ( typeof timeStr === "string" ) {
if ( ! validTimeFormat.test( timeStr ) ) {
Popcorn.error( errorMessage );
}
} else {
Popcorn.error( errorMessage );
}
var t = timeStr.split( ":" ),
lastIndex = t.length - 1,
lastElement = t[ lastIndex ];
//Fix last element:
if ( lastElement.indexOf( ";" ) > -1 ) {
var frameInfo = lastElement.split( ";" ),
frameTime = 0;
if ( framerate && ( typeof framerate === "number" ) ) {
frameTime = parseFloat( frameInfo[ 1 ], 10 ) / framerate;
}
t[ lastIndex ] =
parseInt( frameInfo[ 0 ], 10 ) + frameTime;
}
if ( t.length === 1 ) {
return parseFloat( t[ 0 ], 10 );
} else if ( t.length === 2 ) {
return ( parseInt( t[ 0 ], 10 ) * 60 ) + parseFloat( t[ 1 ], 10 );
} else if ( t.length === 3 ) {
return ( parseInt( t[ 0 ], 10 ) * 3600 ) +
( parseInt( t[ 1 ], 10 ) * 60 ) +
parseFloat( t[ 2 ], 10 );
}
}
};
// Exposes Popcorn to global context
global.Popcorn = 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 ) {
data = source.split( "." );
data[ 0 ] = "parse" + data[ data.length - 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 );
})(window, window.document);
// PLUGIN: Mustache
(function (Popcorn) {
/**
* Mustache Popcorn Plug-in
*
* Adds the ability to render JSON using templates via the Mustache templating library.
*
* @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.
*
* target: a String -- the target div's id.
*
* template: the mustache template for the plugin to use when rendering. This can be
* a String containing the template, or a Function that returns the template's
* String.
*
* data: the data to be rendered using the mustache template. This can be a JSON String,
* a JavaScript Object literal, or a Function returning a String or Literal.
*
* dynamic: an optional argument indicating that the template and json data are dynamic
* and need to be loaded dynamically on every use. Defaults to True.
*
* Example:
var p = Popcorn('#video')
// Example using template and JSON strings.
.mustache({
start: 5, // seconds
end: 15, // seconds
target: 'mustache',
template: '<h1>{{header}}</h1>' +
'{{#bug}}' +
'{{/bug}}' +
'' +
'{{#items}}' +
' {{#first}}' +
' <li><strong>{{name}}</strong></li>' +
' {{/first}}' +
' {{#link}}' +
' <li><a href="{{url}}">{{name}}</a></li>' +
' {{/link}}' +
'{{/items}}' +
'' +
'{{#empty}}' +
' <p>The list is empty.</p>' +
'{{/empty}}' ,
data: '{' +
' "header": "Colors", ' +
' "items": [ ' +
' {"name": "red", "first": true, "url": "#Red"}, ' +
' {"name": "green", "link": true, "url": "#Green"}, ' +
' {"name": "blue", "link": true, "url": "#Blue"} ' +
' ],' +
' 'empty': false' +
'}',
dynamic: false // The json is not going to change, load it early.
} )
// Example showing Functions instead of Strings.
.mustache({
start: 20, // seconds
end: 25, // seconds
target: 'mustache',
template: function(instance, options) {
var template = // load your template file here...
return template;
},
data: function(instance, options) {
var json = // load your json here...
return json;
}
} );
*
*/
Popcorn.plugin( 'mustache' , function( options ) {
var getData, data, getTemplate, template;
Popcorn.getScript('https://github.com/janl/mustache.js/raw/master/mustache.js');
var shouldReload = !!options.dynamic,
typeOfTemplate = typeof options.template,
typeOfData = typeof options.data,
target = document.getElementById( options.target );
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
options.container = target || document.createElement( "div" );
if ( typeOfTemplate === 'function' ) {
if ( !shouldReload ) {
template = options.template( options );
} else {
getTemplate = options.template;
}
} else if ( typeOfTemplate === 'string' ) {
template = options.template;
} else if ( Popcorn.plugin.debug ) {
throw new Error( 'Mustache Plugin Error: options.template must be a String or a Function.' );
} else {
template = "";
}
if ( typeOfData === 'function' ) {
if ( !shouldReload ) {
data = options.data(options);
} else {
getData = options.data;
}
} else if ( typeOfData === 'string' ) {
data = JSON.parse( options.data );
} else if ( typeOfData === 'object' ) {
data = options.data;
} else if ( Popcorn.plugin.debug ) {
throw new Error( 'Mustache Plugin Error: options.data must be a String, Object, or Function.' );
} else {
data = "";
}
return {
start: function( event, options ) {
var interval = function() {
if( !window.Mustache ) {
setTimeout( function() {
interval();
}, 10 );
} else {
// if dynamic, freshen json data on every call to start, just in case.
if ( getData ) {
data = getData( options );
}
if ( getTemplate ) {
template = getTemplate( options );
}
var html = Mustache.to_html( template,
data
).replace( /^\s*/mg, '' );
options.container.innerHTML = html;
}
};
interval();
},
end: function( event, options ) {
options.container.innerHTML = '';
},
_teardown: function( options ) {
getData = data = getTemplate = template = null;
}
};
},
{
about: {
name: 'Popcorn Mustache Plugin',
version: '0.1',
author: 'David Humphrey (@humphd)',
website: 'http://vocamus.net/dave'
},
options: {
start: {elem:'input', type:'text', label:'In'},
end: {elem:'input', type:'text', label:'Out'},
target: 'mustache-container',
template: {elem:'input', type:'text', label:'Template'},
data: {elem:'input', type:'text', label:'Data'},
/* TODO: how to show a checkbox/boolean? */
dynamic: {elem:'input', type:'text', label:'Dynamic'}
}
});
})( Popcorn );
//PLUGIN: facebook
(function(Popcorn, global ) {
/**
* Facebook Popcorn plug-in
* Places Facebook's "social plugins" inside a div ( http://developers.facebook.com/docs/plugins/ )
* Sets options according to user input or default values
* Options parameter will need a target, type, start and end time
* Type is the name of the plugin in fbxml format. Options: LIKE (default), LIKE-BOX, ACTIVITY, FACEPILE
* Target is the id of the document element that the text needs to be attached to. This target element must exist on the DOM
* 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
*
* Other than the mandatory four parameters, there are several optional parameters (Some options are only applicable to certain plugins)
* Action - like button will either "Like" or "Recommend". Options: recommend / like(default)
* Always_post_to_friends - live-stream posts will be always be posted on your facebook wall if true. Options: true / false(default)
* Border_color - border color of the activity feed. Names (i.e: "white") and html color codes are valid
* Colorscheme - changes the color of almost all plugins. Options: light(default) / dark
* Event_app_id - an app_id is required for the live-stream plugin
* Font - the font of the text contained in the plugin. Options: arial / segoe ui / tahoma / trebuchet ms / verdana / lucida grande
* Header - displays the title of like-box or activity feed. Options: true / false(default)
* Href - url to apply to the plugin. Default is current page
* Layout - changes the format of the 'like' count (written in english or a number in a callout).
* Options: box_count / button_count / standard(default)
* Max_rows - number of rows to disperse pictures in facepile. Default is 1
* Recommendations - shows recommendations, if any, in the bottom half of activity feed. Options: true / false(default)
* Show_faces - show pictures beside like button and like-box. Options: true / false(default)
* Site - href for activity feed. No idea why it must be "site". Default is current page
* Stream - displays a the latest posts from the specified page's wall. Options: true / false(default)
* Type - determines which plugin to create. Case insensitive
* Xid - unique identifier if more than one live-streams are on one page
*
* @param {Object} options
*
* Example:
var p = Popcorn('#video')
.facebook({
type : "LIKE-BOX",
target: "likeboxdiv",
start : 3,
end : 10,
href : "http://www.facebook.com/senecacollege",
show_faces: "true",
header: "false"
} )
* This will show how many people "like" Seneca College's Facebook page, and show their profile pictures
*/
var ranOnce = false;
function toggle( container, display ) {
if ( container ) {
container.style.display = display;
return;
}
setTimeout(function() {
toggle( container, display );
}, 10 );
}
Popcorn.plugin( "facebook" , {
manifest:{
about:{
name : "Popcorn Facebook Plugin",
version: "0.1",
author : "Dan Ventura",
website: "dsventura.blogspot.com"
},
options:{
type : {elem:"select", options:["LIKE", "LIKE-BOX", "ACTIVITY", "FACEPILE", "LIVE-STREAM", "SEND"], label:"Type"},
target : "facebook-container",
start : {elem:'input', type:'number', label:'In'},
end : {elem:'input', type:'number', label:'Out'},
// optional parameters:
font : {elem:"input", type:"text", label:"font"},
xid : {elem:"input", type:"text", label:"Xid"},
href : {elem:"input", type:"text", label:"Href"},
site : {elem:"input", type:"text", label:"Site"},
height : {elem:"input", type:"text", label:"Height"},
width : {elem:"input", type:"text", label:"Width"},
action : {elem:"select", options:["like", "recommend"], label:"Action"},
stream : {elem:"select", options:["false", "true"], label:"Stream"},
header : {elem:"select", options:["false", "true"], label:"Header"},
layout : {elem:"select", options:["standard", "button_count", "box_count"], label:"Layout"},
max_rows : {elem:"input", type:"text", label:"Max_rows"},
border_color : {elem:"input", type:"text", label:"Border_color"},
event_app_id : {elem:"input", type:"text", label:"Event_app_id"},
colorscheme : {elem:"select", options:["light", "dark"], label:"Colorscheme"},
show_faces : {elem:"select", options:["false", "true"], label:"Showfaces"},
recommendations : {elem:"select", options:["false", "true"], label:"Recommendations"},
always_post_to_friends : {elem:"input", options:["false", "true"], label:"Always_post_to_friends"}
}
},
_setup: function( options ) {
var target = document.getElementById( options.target );
// facebook script requires a div named fb-root
if ( !document.getElementById( "fb-root" ) ) {
var fbRoot = document.createElement( "div" );
fbRoot.setAttribute( "id", "fb-root" );
document.body.appendChild( fbRoot );
}
if ( !ranOnce || options.event_app_id ) {
ranOnce = true;
// initialize facebook JS SDK
Popcorn.getScript("http://connect.facebook.net/en_US/all.js");
global.fbAsyncInit = function() {
FB.init({
appId : ( options.event_app_id || "" ),
status : true,
cookie : true,
xfbml : true
});
};
}
var validType = function( type ) {
return ( [ "like", "like-box", "activity", "facepile", "comments", "live-stream", "send" ].indexOf( type ) > -1 );
};
// default plugin is like button
options.type = ( options.type || "like" ).toLowerCase();
// default plugin is like button
if ( !validType( options.type ) ) {
options.type = "like";
}
options._container = document.createElement( "fb:" + options.type );
var setOptions = (function( options ) {
options._container.style.display = "none";
// activity feed uses 'site' rather than 'href'
var attr = options.type === "activity" ? "site" : "href";
options._container.setAttribute( attr, ( options[ attr ] || document.URL ) );
return {
"like": function () {
options._container.setAttribute( "send", ( options.send || false ) );
options._container.setAttribute( "width", options.width );
options._container.setAttribute( "show_faces", options.show_faces );
options._container.setAttribute( "layout", options.layout );
options._container.setAttribute( "font", options.font );
options._container.setAttribute( "colorscheme", options.colorscheme );
},
"like-box": function () {
options._container.setAttribute( "height", ( options.height || 250 ) );
options._container.setAttribute( "width", options.width );
options._container.setAttribute( "show_faces", options.show_faces );
options._container.setAttribute( "stream", options.stream );
options._container.setAttribute( "header", options.header );
options._container.setAttribute( "colorscheme", options.colorscheme );
},
"facepile": function () {
options._container.setAttribute( "height", options.height );
options._container.setAttribute( "width", options.width );
options._container.setAttribute( "max_rows", ( options.max_rows || 1 ) );
},
"activity": function () {
options._container.setAttribute( "width", options.width );
options._container.setAttribute( "height", options.height );
options._container.setAttribute( "header", options.header );
options._container.setAttribute( "border_color", options.border_color );
options._container.setAttribute( "recommendations", options.recommendations );
options._container.setAttribute( "font", options.font );
options._container.setAttribute( "colorscheme", options.colorscheme );
},
"live-stream": function() {
options._container.setAttribute( "width", ( options.width || 400 ) );
options._container.setAttribute( "height", ( options.height || 500 ) );
options._container.setAttribute( "always_post_to_friends", ( options.always_post_to_friends || false ) );
options._container.setAttribute( "event_app_id", options.event_app_id );
options._container.setAttribute( "xid", options.xid );
},
"send": function() {
options._container.setAttribute( "font", options.font );
options._container.setAttribute( "colorscheme", options.colorscheme );
}
};
})( options );
setOptions[ options.type ]();
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "flickr target container doesn't exist" );
}
target && target.appendChild( options._container );
},
/**
* @member facebook
* 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 ){
toggle( options._container, "inline" );
},
/**
* @member facebook
* 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 ){
toggle ( options._container, "none" );
}
});
})( Popcorn, this );
// 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 link - 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'
} )
*
*/
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: "In"
},
end: {
elem: "input",
type: "number",
label: "Out"
},
href: {
elem: "input",
type: "text",
label: "Link URL"
},
target: "image-container",
src: {
elem: "input",
type: "text",
label: "Source URL"
},
text: {
elem: "input",
type: "text",
label: "TEXT"
}
}
},
_setup: function( options ) {
var img = document.createElement( "img" ),
target = document.getElementById( options.target );
options.link = document.createElement( "a" );
options.link.style.position = "relative";
options.link.style.textDecoration = "none";
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
// add the widget's div to the target div
target && target.appendChild( options.link );
img.addEventListener( "load", function() {
// borders look really bad, if someone wants it they can put it on their div target
img.style.borderStyle = "none";
if ( options.href ) {
options.link.href = options.href;
}
options.link.target = "_blank";
var fontHeight = ( img.height / 12 ) + "px",
divText = document.createElement( "div" );
Popcorn.extend( divText.style, {
color: "black",
fontSize: fontHeight,
fontWeight: "bold",
position: "relative",
textAlign: "center",
width: img.width + "px",
zIndex: "10"
});
divText.innerHTML = options.text || "";
options.link.appendChild( divText );
options.link.appendChild( img );
divText.style.top = ( img.height / 2 ) - ( divText.offsetHeight / 2 ) + "px";
options.link.style.display = "none";
}, false );
img.src = options.src;
},
/**
* @member image
* 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.link.style.display = "block";
},
/**
* @member image
* 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.link.style.display = "none";
},
_teardown: function( options ) {
document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( options.link );
}
});
})( Popcorn );
// PLUGIN: lowerthird
(function (Popcorn) {
/**
* Lower Third popcorn plug-in
* Displays information about a speaker over the video, or in the target div
* Options parameter will need a start, and end.
* Optional parameters are target, salutation, name and role.
* 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 document element that the content is
* appended to, this target element must exist on the DOM
* salutation is the speaker's Mr. Ms. Dr. etc.
* name is the speaker's name.
* role is information about the speaker, example Engineer.
*
* @param {Object} options
*
* Example:
var p = Popcorn('#video')
.lowerthird({
start: 5, // seconds, mandatory
end: 15, // seconds, mandatory
salutation: 'Mr', // optional
name: 'Scott Downe', // optional
role: 'Programmer', // optional
target: 'subtitlediv' // optional
} )
*
*/
Popcorn.plugin( "lowerthird" , {
manifest: {
about:{
name: "Popcorn lowerthird Plugin",
version: "0.1",
author: "Scott Downe",
website: "http://scottdowne.wordpress.com/"
},
options:{
start : {elem:'input', type:'text', label:'In'},
end : {elem:'input', type:'text', label:'Out'},
target : 'lowerthird-container',
salutation : {elem:'input', type:'text', label:'Text'},
name : {elem:'input', type:'text', label:'Text'},
role : {elem:'input', type:'text', label:'Text'}
}
},
_setup: function( options ) {
var target = document.getElementById( options.target );
// Creates a div for all Lower Thirds to use
if ( !this.container ) {
this.container = document.createElement('div');
this.container.style.position = "absolute";
this.container.style.color = "white";
this.container.style.textShadow = "black 2px 2px 6px";
this.container.style.fontSize = "24px";
this.container.style.fontWeight = "bold";
this.container.style.paddingLeft = "40px";
// the video element must have height and width defined
this.container.style.width = this.video.offsetWidth + "px";
this.container.style.left = this.position().left + "px";
this.video.parentNode.appendChild( this.container );
}
// if a target is specified, use that
if ( options.target && options.target !== "lowerthird-container") {
options.container = document.createElement( "div" );
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
target && target.appendChild( options.container );
} else { // use shared default container
options.container = this.container;
}
},
/**
* @member lowerthird
* 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.innerHTML = ( options.salutation ? options.salutation + " " : "" ) + options.name + ( options.role ? "<br />" + options.role : "" );
this.container.style.top = this.position().top + this.video.offsetHeight - ( 40 + this.container.offsetHeight ) + "px";
},
/**
* @member lowerthird
* 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.innerHTML = "";
}
} );
})( Popcorn );
// PLUGIN: WEBPAGE
(function (Popcorn) {
/**
* Webpages popcorn plug-in
* Creates an iframe showing a website specified by the user
* Options parameter will need a start, end, id, 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
* Id is the id that you want assigned to the iframe
* 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 website that you want the iframe to display
*
* @param {Object} options
*
* Example:
var p = Popcorn('#video')
.webpage({
id: "webpages-a",
start: 5, // seconds
end: 15, // seconds
src: 'http://www.webmademovies.org',
target: 'webpagediv'
} )
*
*/
Popcorn.plugin( "webpage" , {
manifest: {
about:{
name: "Popcorn Webpage Plugin",
version: "0.1",
author: "@annasob",
website: "annasob.wordpress.com"
},
options:{
id : {elem:'input', type:'text', label:'Id'},
start : {elem:'input', type:'text', label:'In'},
end : {elem:'input', type:'text', label:'Out'},
src : {elem:'input', type:'text', label:'Src'},
target : 'iframe-container'
}
},
_setup : function( options ) {
var target = document.getElementById( options.target );
// make an iframe
options._iframe = document.createElement( 'iframe' );
options._iframe.setAttribute('width', "100%");
options._iframe.setAttribute('height', "100%");
options._iframe.id = options.id;
options._iframe.src = options.src;
options._iframe.style.display = 'none';
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
// add the hidden iframe to the DOM
target && target.appendChild(options._iframe);
},
/**
* @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){
// make the iframe visible
options._iframe.src = options.src;
options._iframe.style.display = 'inline';
},
/**
* @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){
// make the iframe invisible
options._iframe.style.display = 'none';
},
_teardown: function( options ) {
document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( options._iframe );
}
});
})( Popcorn );
// PLUGIN: Timeline
(function ( Popcorn ) {
/**
* timeline popcorn plug-in
* Adds data associated with a certain time in the video, which creates a scrolling view of each item as the video progresses
* Options parameter will need a start, target, title, 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, tho for this plugin an end time may not be needed ( optional )
* -Target is the id of the DOM element that you want the map to appear in. This element must be in the DOM
* -Title is the title of the current timeline box
* -Text is text is simply related text that will be displayed
* -innerHTML gives the user the option to add things such as links, buttons and so on
* -direction specifies whether the timeline will grow from the top or the bottom, receives input as "UP" or "DOWN"
* @param {Object} options
*
* Example:
var p = Popcorn("#video")
.timeline( {
start: 5, // seconds
target: "timeline",
title: "Seneca",
text: "Welcome to seneca",
innerHTML: "Click this link <a href='http://www.google.ca'>Google</a>"
} )
*
*/
var i = 1,
head = document.getElementsByTagName( "head" )[ 0 ],
css = document.createElement( "link" );
css.type = "text/css";
css.rel = "stylesheet";
css.href = "//popcornjs.org/code/plugins/timeline/popcorn.timeline.css";
head.insertBefore( css, head.firstChild );
Popcorn.plugin( "timeline" , function( options ) {
var target = document.getElementById( options.target ),
newdiv = document.createElement( "div" );
newdiv.style.display = "none";
newdiv.id = "timelineDiv" + i;
// Default to up if options.direction is non-existant or not up or down
options.direction = options.direction || "up";
if ( options.direction.toLowerCase() !== "up" || options.direction.toLowerCase() !== "down" ) {
options.direction = "up";
}
if ( target ) {
target.style.width = "400px";
target.style.height = "200px";
target.style.overflow = "auto";
target.appendChild( newdiv );
// if this isnt the first div added to the target div
if( options.direction.length === 2 ){
// insert the current div before the previous div inserted
target.insertBefore( newdiv, document.getElementById( "timelineDiv" + ( i - 1 ) ) );
}
} else if ( Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
i++;
// Default to empty if not used
//options.innerHTML = options.innerHTML || "";
newdiv.innerHTML = "<p><span id='big'>" + options.title + "</span><br />" +
"<span id='mid'>" + options.text + "</span><br />" + options.innerHTML;
return {
start: function( event, options ) {
newdiv.style.display = "block";
if( options.direction === "down" ) {
target.scrollTop = target.scrollHeight;
}
},
end: function( event, options ) {
newdiv.style.display = "none";
},
_teardown: function( options ) {
while ( target.firstChild ) {
target.removeChild( target.firstChild );
}
}
};
},
{
about: {
name: "Popcorn Timeline Plugin",
version: "0.1",
author: "David Seifried @dcseifried",
website: "dseifried.wordpress.com"
},
options: {
start: {
elem: "input",
type: "text",
label: "In"
},
end: {
elem: "input",
type: "text",
label: "Out"
},
target: "feed-container",
title: {
elem: "input",
type: "text",
label: "title"
},
text: {
elem: "input",
type: "text",
label: "text"
},
innerHTML: {
elem: "input",
type: "text",
label: "innerHTML"
},
direction: {
elem: "input",
type: "text",
label: "direction"
}
}
});
})( 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++;
// ensure the target container the user chose exists
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "flickr target container doesn't exist" );
}
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( "p" );
fragment.innerHTML = "<p style='padding:" + _padding + ";'>" + data.title + "<p/>";
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: "In"
},
end: {
elem: "input",
type: "number",
label: "Out"
},
userid: {
elem: "input",
type: "text",
label: "UserID"
},
tags: {
elem: "input",
type: "text",
label: "Tags"
},
username: {
elem: "input",
type: "text",
label: "Username"
},
apikey: {
elem: "input",
type: "text",
label: "Api_key"
},
target: "flickr-container",
height: {
elem: "input",
type: "text",
label: "Height"
},
width: {
elem: "input",
type: "text",
label: "Width"
},
padding: {
elem: "input",
type: "text",
label: "Padding"
},
border: {
elem: "input",
type: "text",
label: "Border"
},
numberofimages: {
elem: "input",
type: "text",
label: "Number of Images"
}
}
});
})( Popcorn );
// PLUGIN: WIKIPEDIA
var wikiCallback;
(function ( Popcorn ) {
/**
* Wikipedia popcorn plug-in
* Displays a wikipedia aricle in the target specified by the user by using
* new DOM element instead overwriting them
* Options parameter will need a start, end, target, lang, src, title and numberofwords.
* -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 document element that the text from the article needs to be
* attached to, this target element must exist on the DOM
* -Lang (optional, defaults to english) is the language in which the article is in.
* -Src is the url of the article
* -Title (optional) is the title of the article
* -numberofwords (optional, defaults to 200) is the number of words you want displaid.
*
* @param {Object} options
*
* Example:
var p = Popcorn("#video")
.wikipedia({
start: 5, // seconds
end: 15, // seconds
src: "http://en.wikipedia.org/wiki/Cape_Town",
target: "wikidiv"
} )
*
*/
Popcorn.plugin( "wikipedia" , {
manifest: {
about:{
name: "Popcorn Wikipedia Plugin",
version: "0.1",
author: "@annasob",
website: "annasob.wordpress.com"
},
options:{
start: {
elem: "input",
type: "text",
label: "In"
},
end: {
elem: "input",
type: "text",
label: "Out"
},
lang: {
elem: "input",
type: "text",
label: "Language"
},
src: {
elem: "input",
type: "text",
label: "Src"
},
title: {
elem: "input",
type: "text",
label: "Title"
},
numberofwords: {
elem: "input",
type: "text",
label: "Num Of Words"
},
target: "wikipedia-container"
}
},
/**
* @member wikipedia
* The setup function will get all of the needed
* items in place before the start function is called.
* This includes getting data from wikipedia, if the data
* is not received and processed before start is called start
* will not do anything
*/
_setup : function( options ) {
// declare needed variables
// get a guid to use for the global wikicallback function
var _text, _guid = Popcorn.guid();
// if the user didn't specify a language default to english
if ( !options.lang ) {
options.lang = "en";
}
// if the user didn't specify number of words to use default to 200
options.numberofwords = options.numberofwords || 200;
// wiki global callback function with a unique id
// function gets the needed information from wikipedia
// and stores it by appending values to the options object
window[ "wikiCallback" + _guid ] = function ( data ) {
options._link = document.createElement( "a" );
options._link.setAttribute( "href", options.src );
options._link.setAttribute( "target", "_blank" );
// add the title of the article to the link
options._link.innerHTML = options.title || data.parse.displaytitle;
// get the content of the wiki article
options._desc = document.createElement( "p" );
// get the article text and remove any special characters
_text = data.parse.text[ "*" ].substr( data.parse.text[ "*" ].indexOf( "<p>" ) );
_text = _text.replace( /((<(.|\n)+?>)|(\((.*?)\) )|(\[(.*?)\]))/g, "" );
options._desc.innerHTML = _text.substr( 0, options.numberofwords ) + " ...";
options._fired = true;
};
if ( options.src ) {
Popcorn.getScript( "http://" + options.lang + ".wikipedia.org/w/api.php?action=parse&props=text&page=" +
options.src.slice( options.src.lastIndexOf("/")+1) + "&format=json&callback=wikiCallback" + _guid);
} else if ( Popcorn.plugin.debug ) {
throw new Error( "Wikipedia plugin needs a 'src'" );
}
},
/**
* @member wikipedia
* 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 ){
// dont do anything if the information didn't come back from wiki
var isReady = function () {
if ( !options._fired ) {
setTimeout( function () {
isReady();
}, 13);
} else {
if ( options._link && options._desc ) {
if ( document.getElementById( options.target ) ) {
document.getElementById( options.target ).appendChild( options._link );
document.getElementById( options.target ).appendChild( options._desc );
options._added = true;
}
}
}
};
isReady();
},
/**
* @member wikipedia
* 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 ){
// ensure that the data was actually added to the
// DOM before removal
if ( options._added ) {
document.getElementById( options.target ).removeChild( options._link );
document.getElementById( options.target ).removeChild( options._desc );
}
},
_teardown: function( options ){
if ( options._added ) {
options._link.parentNode && document.getElementById( options.target ).removeChild( options._link );
options._desc.parentNode && document.getElementById( options.target ).removeChild( options._desc );
delete options.target;
}
}
});
})( Popcorn );
// PLUGIN: GML
(function (Popcorn) {
var gmlPlayer = function( $p ) {
var _stroke = 0,
onPt = 0,
onStroke = 0,
x = null,
y = null,
rotation = false,
strokes = 0,
play = function() {},
reset = function() {
$p.background( 0 );
onPt = onStroke = 0;
x = y = null;
},
drawLine = function( x, y, x2, y2 ) {
var _x, _y, _x2, _y2;
if ( rotation ) {
_x = y * $p.height;
_y = $p.width - ( x * $p.width );
_x2 = y2 * $p.height;
_y2 = $p.width - ( x2 * $p.width );
} else {
_x = x * $p.width;
_y = y * $p.height;
_x2 = x2 * $p.width;
_y2 = y2 * $p.height;
}
$p.stroke( 0 );
$p.strokeWeight( 13 );
$p.strokeCap( $p.SQUARE );
$p.line( _x, _y, _x2, _y2 );
$p.stroke( 255 );
$p.strokeWeight( 12 );
$p.strokeCap( $p.ROUND );
$p.line( _x, _y, _x2, _y2 );
},
seek = function( point ) {
( point < onPt ) && reset();
while ( onPt <= point ) {
if ( !strokes ) {
return;
}
_stroke = strokes[ onStroke ] || strokes;
var pt = _stroke.pt[ onPt ],
p = onPt;
x != null && drawLine( x, y, pt.x, pt.y );
x = pt.x;
y = pt.y;
( onPt === p ) && onPt++;
}
};
$p.draw = function() {
play();
};
$p.setup = function() {};
$p.construct = function( media, data, options ) {
var dataReady = function() {
if ( data ) {
strokes = data.gml.tag.drawing.stroke;
var drawingDur = ( options.end - options.start ) / ( strokes.pt || (function( strokes ) {
var rStrokes = [];
for ( var i = 0, sl = strokes.length; i < sl; i++ ) {
rStrokes = rStrokes.concat( strokes[ i ].pt );
}
return rStrokes;
})( strokes ) ).length;
var tag = data.gml.tag,
app_name = tag.header && tag.header.client && tag.header.client.name;
rotation = app_name === 'Graffiti Analysis 2.0: DustTag' ||
app_name === 'DustTag: Graffiti Analysis 2.0' ||
app_name === 'Fat Tag - Katsu Edition';
play = function() {
if ( media.currentTime < options.endDrawing ) {
seek( ( media.currentTime - options.start ) / drawingDur );
}
};
return;
}
setTimeout( dataReady, 5 );
};
$p.size( 640, 640 );
$p.frameRate( 60 );
$p.smooth();
reset();
$p.noLoop();
dataReady();
};
};
/**
* Grafiti markup Language (GML) popcorn plug-in
* Renders a GML tag inside an HTML element
* Options parameter will need a mandatory start, end, target, gmltag.
* Optional parameters: none.
* 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 document element that you wish to render the grafiti in
* gmltag is the numerical reference to a gml tag via 000000book.com
* @param {Object} options
*
* Example:
var p = Popcorn('#video')
.gml({
start: 0, // seconds
end: 5, // seconds
gmltag: '29582',
target: 'gmldiv'
});
*
*/
Popcorn.plugin( "gml" , {
_setup : function( options ) {
var self = this,
target = document.getElementById( options.target );
options.endDrawing = options.endDrawing || options.end;
// create a canvas to put in the target div
options.container = document.createElement( "canvas" );
options.container.style.display = "none";
options.container.setAttribute( "id", "canvas" + options.gmltag );
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
target && target.appendChild( options.container );
if ( !window.Processing ) {
Popcorn.getScript( "http://processingjs.org/content/download/processing-js-1.2.1/processing-1.2.1.min.js" );
}
// makes sure both processing.js and the gml data are loaded
var readyCheck = function() {
if ( window.Processing ) {
Popcorn.getJSONP( "http://000000book.com/data/" + options.gmltag + ".json?callback=", function( data ) {
options.pjsInstance = new Processing( options.container, gmlPlayer );
options.pjsInstance.construct( self.media, data, options );
options._running && options.pjsInstance.loop();
}, false );
return;
}
setTimeout( readyCheck, 5 );
};
readyCheck();
},
/**
* @member gml
* 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.pjsInstance && options.pjsInstance.loop();
options.container.style.display = "block";
},
/**
* @member gml
* 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.pjsInstance && options.pjsInstance.noLoop();
options.container.style.display = "none";
},
_teardown: function( options ) {
options.pjsInstance && options.pjsInstance.exit();
document.getElementById( options.target ) && document.getElementById( options.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();
_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("http://maps.google.com/maps/api/js?sensor=false&callback=googleCallback");
} else {
setTimeout(function () {
loadMaps();
}, 1);
}
};
/**
* 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
* -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.
* -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 );
// 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 = "100%";
newdiv.style.height = "100%";
i++;
// ensure the target container the user chose exists
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
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 (options.location) {
// calls an anonymous google function called on separate thread
geocoder.geocode({
"address": options.location
}, function (results, status) {
if (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 = new google.maps.Map(newdiv, {
mapTypeId: google.maps.MapTypeId[options.type] || google.maps.MapTypeId.HYBRID
});
map.getDiv().style.display = "none";
}
});
} else {
location = new google.maps.LatLng(options.lat, options.lng);
map = new google.maps.Map(newdiv, {
mapTypeId: google.maps.MapTypeId[options.type] || google.maps.MapTypeId.HYBRID
});
map.getDiv().style.display = "none";
}
} 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 ) {
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;
}
options.zoom = options.zoom || 8; // default to 8
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 ) {
setTimeout(function() {
// Checks whether this is a generated route or not
if( typeof options.tween === "object" ) {
for ( var i = 0; i < rM.length; i++ ) {
// Checks if this position along the tween should be displayed or not
if ( that.media.currentTime >= ( rM[ i ].interval * ( i + 1 ) ) / 1000 &&
( that.media.currentTime <= ( rM[ i ].interval * ( i + 2 ) ) / 1000 ||
that.media.currentTime >= rM[ i ].interval * ( rM.length ) / 1000 ) ) {
sView3.setPosition( new google.maps.LatLng( rM[ i ].position.lat, rM[ i ].position.lng ) );
sView3.setPov({
heading: rM[ i ].pov.heading || 0,
zoom: rM[ i ].pov.zoom || 0,
pitch: rM[ i ].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; k < rM.length; k++ ) {
if( that.media.currentTime >= (options.interval * ( k + 1 ) ) / 1000 &&
( that.media.currentTime <= (options.interval * ( k + 2 ) ) / 1000 ||
that.media.currentTime >= options.interval * ( rM.length ) / 1000 ) ) {
sView2.setPosition( checkpoints[ k ] );
sView2.setPov({
heading: options.heading || 0,
zoom: options.zoom,
pitch: options.pitch || 0
});
}
}
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
for ( var j = 0; j < directionResult.routes[ 0 ].overview_path.length; j++ ) {
checkpoints.push( new google.maps.LatLng( directionResult.routes[ 0 ].overview_path[ j ].lat(), directionResult.routes[ 0 ].overview_path[ 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; i < options.tween.length; i++ ) {
// Make sure interval exists, if not, set to 1000
options.tween[ i ].interval = options.tween[ i ].interval || 1000;
tween( options.tween, 10 );
}
}
}
} 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) {
// the map must be manually removed
document.getElementById(options.target).removeChild(newdiv);
newdiv = map = location = null;
}
};
}, {
about: {
name: "Popcorn Google Map Plugin",
version: "0.1",
author: "@annasob",
website: "annasob.wordpress.com"
},
options: {
start: {
elem: "input",
type: "text",
label: "In"
},
end: {
elem: "input",
type: "text",
label: "Out"
},
target: "map-container",
type: {
elem: "select",
options: ["ROADMAP", "SATELLITE", "STREETVIEW", "HYBRID", "TERRAIN"],
label: "Type"
},
zoom: {
elem: "input",
type: "text",
label: "Zoom"
},
lat: {
elem: "input",
type: "text",
label: "Lat"
},
lng: {
elem: "input",
type: "text",
label: "Lng"
},
location: {
elem: "input",
type: "text",
label: "Location"
},
heading: {
elem: "input",
type: "text",
label: "Heading"
},
pitch: {
elem: "input",
type: "text",
label: "Pitch"
}
}
});
})(Popcorn);
/**
* Pause Popcorn Plug-in
*
* When this plugin is used, links on the webpage, when clicked, will pause
* popcorn videos that especified 'pauseOnLinkClicked' as an option. Links may
* cause a new page to display on a new window, or may cause a new page to
* display in the current window, in which case the videos won't be available
* anymore. It only affects anchor tags. It does not affect objects with click
* events that act as anchors.
*
* Example:
var p = Popcorn('#video', { pauseOnLinkClicked : true } )
.play();
*
*/
document.addEventListener( "click", function( event ) {
var targetElement = event.target;
//Some browsers use an element as the target, some use the text node inside
if ( targetElement.nodeName === "A" || targetElement.parentNode && targetElement.parentNode.nodeName === "A" ) {
Popcorn.instances.forEach( function( video ) {
if ( video.options.pauseOnLinkClicked ) {
video.pause();
}
});
}
}, false );
// PLUGIN: TWITTER
(function (Popcorn) {
var scriptLoading = false;
/**
* Twitter popcorn plug-in
* Appends a Twitter widget to an element on the page.
* Options parameter will need a start, end, target and source.
* Optional parameters are height and width.
* 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
* Src is the hash tag or twitter user to get tweets from
* Target is the id of the document element that the images are
* appended to, this target element must exist on the DOM
* Height is the height of the widget, defaults to 200
* Width is the width of the widget, defaults to 250
*
* @param {Object} options
*
* Example:
var p = Popcorn('#video')
.twitter({
start: 5, // seconds, mandatory
end: 15, // seconds, mandatory
src: '@stevesong', // mandatory, also accepts hash tags
height: 200, // optional
width: 250, // optional
target: 'twitterdiv' // mandatory
} )
*
*/
Popcorn.plugin( "twitter" , {
manifest: {
about:{
name: "Popcorn Twitter Plugin",
version: "0.1",
author: "Scott Downe",
website: "http://scottdowne.wordpress.com/"
},
options:{
start : {elem:'input', type:'number', label:'In'},
end : {elem:'input', type:'number', label:'Out'},
src : {elem:'input', type:'text', label:'Source'},
target : 'twitter-container',
height : {elem:'input', type:'number', label:'Height'},
width : {elem:'input', type:'number', label:'Width'}
}
},
_setup: function( options ) {
if ( !window.TWTR && !scriptLoading ) {
scriptLoading = true;
Popcorn.getScript("http://widgets.twimg.com/j/2/widget.js");
}
var target = document.getElementById( options.target );
// setup widget div that is unique per track
options.container = document.createElement( 'div' ); // create the div to store the widget
options.container.setAttribute('id', Popcorn.guid()); // use this id to connect it to the widget
options.container.style.display = "none"; // display none by default
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
// add the widget's div to the target div
target && target.appendChild( options.container );
// setup info for the widget
var src = options.src || "",
width = options.width || 250,
height = options.height || 200,
profile = /^@/.test( src ),
hash = /^#/.test( src ),
widgetOptions = {
version: 2,
id: options.container.getAttribute( 'id' ), // use this id to connect it to the div
rpp: 30,
width: width,
height: height,
interval: 6000,
theme: {
shell: {
background: '#ffffff',
color: '#000000'
},
tweets: {
background: '#ffffff',
color: '#444444',
links: '#1985b5'
}
},
features: {
loop: true,
timestamp: true,
avatars: true,
hashtags: true,
toptweets: true,
live: true,
scrollbar: false,
behavior: 'default'
}
};
// create widget
var isReady = function( that ) {
if ( window.TWTR ) {
if ( profile ) {
widgetOptions.type = "profile";
new TWTR.Widget( widgetOptions ).render().setUser( src ).start();
} else if ( hash ) {
widgetOptions.type = "search";
widgetOptions.search = src;
widgetOptions.subject = src;
new TWTR.Widget( widgetOptions ).render().start();
}
} else {
setTimeout( function() {
isReady( that );
}, 1);
}
};
isReady( this );
},
/**
* @member Twitter
* 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 Twitter
* 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 ) {
document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( options.container );
}
});
})( Popcorn );
/**
* Processing Popcorn Plug-In
*
* This plugin adds a Processing.js sketch to be added to a target div or canvas.
*
* Options parameter needs to specify start, end, target and sketch attributes
* -Start is the time [in seconds] that you want the sketch to display and start looping.
* -End is the time [in seconds] you want the sketch to become hidden and stop looping.
* -Target is the id of the div or canvas you want the target sketch to be displayed in. ( a target that is a div will have a canvas created and placed inside of it. )
* -Sketch specifies the filename of the Procesing code to be loaded into Processing.js
* -noLoop [optional] specifies whether a sketch should continue to loop when the video is paused or seeking.
*
* @param {Object} options
*
* Example:
var p = Popcorn( "#video" )
.processing({
start: 5,
end: 10,
target: "processing-div",
sketch: "processingSketch.pjs",
noLoop: true
});
*
*/
(function ( Popcorn ) {
Popcorn.plugin( "processing" , function ( options ) {
var init = function( context ) {
var initProcessing,
canvas;
if ( !window.Processing ) {
Popcorn.getScript( "http://processingjs.org/content/download/processing-js-1.2.1/processing-1.2.1.min.js" );
}
options.parentTarget = document.getElementById( options.target );
if ( !options.parentTarget && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
initProcessing = function() {
var addListeners = function() {
context.listen( "pause", function () {
if ( options.canvas.style.display === "inline" ) {
options.pjsInstance.noLoop();
}
});
context.listen( "play", function() {
if ( options.canvas.style.display === "inline" ) {
options.pjsInstance.loop();
}
});
};
if ( window.Processing ) {
options.pjsInstance = new Processing( options.canvas, options.processingCode );
options.pjsInstance.noLoop();
options.seeking = false;
context.listen( "seeking", function() {
options._running && options.canvas.style.display === "inline" && options.noPause && options.pjsInstance.loop();
});
options._running && !context.media.paused && options.pjsInstance.loop();
options.noPause = options.noPause || false;
!options.noPause && addListeners();
options.codeReady = true;
} else {
setTimeout ( function() {
initProcessing.call( this );
}, 10 );
}
};
canvas = document.createElement( "canvas" );
canvas.id = Popcorn.guid( options.target + "-sketch-" );
canvas[ "data-processing-sources" ] = options.sketch;
canvas.style.display = "none";
options.canvas = canvas;
options.parentTarget && options.parentTarget.appendChild( options.canvas );
if ( options.sketch ) {
Popcorn.xhr({
url: options.sketch,
dataType: "text",
success: function( responseCode ) {
options.codeReady = true;
options.processingCode = responseCode;
initProcessing();
}
});
} else {
if ( Popcorn.plugin.debug ) {
throw new Error( "options.sketch is undefined" );
}
}
};
return {
manifest: {
about: {
name: "Popcorn Processing Plugin",
version: "0.1",
author: "Christopher De Cairos, Benjamin Chalovich",
website: "cadecairos.blogspot.com, ben1amin.wordpress.org"
},
options: {
start : { elem: "input", type: "text", label: "In" },
end : { elem: "input", type: "text", label: "Out" },
target : { elem: "input", type: "text", label: "Target" },
sketch : { elem: "input", type: "text", label: "Sketch" },
noPause : { elem: "select", options: [ "TRUE", "FALSE" ], label: "No Loop" }
}
},
_setup: function( options ) {
options.codeReady = false;
init( this );
},
start: function( event, options ) {
options.codeReady && !this.media.paused && options.pjsInstance.loop();
options.canvas.style.display = "inline";
},
end: function( event, options ) {
options.pjsInstance && options.pjsInstance.noLoop();
options.canvas.style.display = "none";
},
_teardown: function( options ) {
options.pjsInstance && options.pjsInstance.exit();
options.parentTarget && options.parentTarget.removeChild( options.canvas );
}
};
});
}( Popcorn ));
// 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;
// Setup a proper frame interval function (60fps), favouring paint events.
var step = ( function() {
var buildFrameRunner = function( runner ) {
return function( f, options ) {
var _f = function() {
f();
if ( 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' ) {
if ( Popcorn.plugin.debug ) {
throw new Error( 'Popcorn Code Plugin Error: onStart must be a function.' );
}
options.onStart = Popcorn.nop;
}
if ( options.onEnd && typeof options.onEnd !== 'function' ) {
if ( Popcorn.plugin.debug ) {
throw new Error( 'Popcorn Code Plugin Error: onEnd must be a function.' );
}
options.onEnd = undefined;
}
if ( options.onFrame && typeof options.onFrame !== 'function' ) {
if ( Popcorn.plugin.debug ) {
throw new Error( 'Popcorn Code Plugin Error: onFrame must be a function.' );
}
options.onFrame = undefined;
}
return {
start: function( event, options ) {
options.onStart( options );
if ( options.onFrame ) {
running = true;
step( options.onFrame, options );
}
},
end: function( event, options ) {
if ( options.onFrame ) {
running = false;
}
if ( options.onEnd ) {
options.onEnd( options );
}
}
};
},
{
about: {
name: 'Popcorn Code Plugin',
version: '0.1',
author: 'David Humphrey (@humphd)',
website: 'http://vocamus.net/dave'
},
options: {
start: {elem:'input', type:'text', label:'In'},
end: {elem:'input', type:'text', label:'Out'},
onStart: {elem:'input', type:'function', label:'onStart'},
onFrame: {elem:'input', type:'function', label:'onFrame'},
onEnd: {elem:'input', type:'function', label:'onEnd'}
}
});
})( Popcorn );
// PLUGIN: LASTFM
(function (Popcorn) {
var _artists = {},
lastFMcallback = function(data){
if (data.artist) {
var htmlString = "";
htmlString = '<h3>'+data.artist.name+'</h3>';
htmlString += '<a href="'+data.artist.url+'" target="_blank" style="float:left;margin:0 10px 0 0;"><img src="'+ data.artist.image[2]['#text'] +'" alt=""></a>';
htmlString += '<p>'+ data.artist.bio.summary +'</p>';
htmlString += '<hr /><p><h4>Tags</h4><ul>';
Popcorn.forEach( data.artist.tags.tag, function( val, i) {
htmlString += '<li><a href="'+ val.url +'">'+ val.name +'</a></li>';
});
htmlString += '</ul></p>';
htmlString += '<hr /><p><h4>Similar</h4><ul>';
Popcorn.forEach( data.artist.similar.artist, function( val, i) {
htmlString += '<li><a href="'+ val.url +'">'+ val.name +'</a></li>';
});
htmlString += '</ul></p>';
_artists[data.artist.name.toLowerCase()].htmlString = htmlString;
}
};
/**
* LastFM popcorn plug-in
* Appends information about a LastFM artist to an element on the page.
* Options parameter will need a start, end, target, artist and apikey.
* 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
* Artist is the name of who's LastFM information you wish to show
* Target is the id of the document element that the images are
* appended to, this target element must exist on the DOM
* ApiKey is the API key registered with LastFM for use with their API
*
* @param {Object} options
*
* Example:
var p = Popcorn('#video')
.lastfm({
start: 5, // seconds, mandatory
end: 15, // seconds, mandatory
artist: 'yacht', // mandatory
target: 'lastfmdiv', // mandatory
apikey: '1234567890abcdef1234567890abcdef' // mandatory
} )
*
*/
Popcorn.plugin( "lastfm" , (function(){
return {
manifest: {
about:{
name: "Popcorn LastFM Plugin",
version: "0.1",
author: "Steven Weerdenburg",
website: "http://sweerdenburg.wordpress.com/"
},
options:{
start : {elem:'input', type:'text', label:'In'},
end : {elem:'input', type:'text', label:'Out'},
target : 'lastfm-container',
artist : {elem:'input', type:'text', label:'Artist'}
}
},
_setup: function( options ) {
options._container = document.createElement( 'div' );
options._container.style.display = "none";
options._container.innerHTML = "";
options.artist = options.artist && options.artist.toLowerCase() || "";
var target = document.getElementById( options.target );
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
target && target.appendChild( options._container );
if(!_artists[options.artist]) {
_artists[options.artist] = {
count: 0,
htmlString: "Unknown Artist"
};
Popcorn.getJSONP("http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist="+ options.artist +"&api_key="+options.apikey+"&format=json&callback=lastFMcallback", lastFMcallback, false );
}
_artists[options.artist].count++;
},
/**
* @member LastFM
* 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.innerHTML = _artists[options.artist].htmlString;
options._container.style.display = "inline";
},
/**
* @member LastFM
* 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";
options._container.innerHTML = "";
},
_teardown: function( options ) {
// cleaning possible reference to _artist array;
--_artists[ options.artist ].count || delete _artists[ options.artist ];
document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( options._container );
}
};
})());
})( Popcorn );
// PLUGIN: Footnote
(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.1",
author: "@annasob",
website: "annasob.wordpress.com"
},
options:{
start : {elem:'input', type:'text', label:'In'},
end : {elem:'input', type:'text', label:'Out'},
target : 'footnote-container',
text : {elem:'input', type:'text', label:'Text'}
}
},
_setup: function(options) {
var target = document.getElementById( options.target );
options._container = document.createElement( 'div' );
options._container.style.display = "none";
options._container.innerHTML = options.text;
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
target && 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 ) {
document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( options._container );
}
});
})( Popcorn );
// PLUGIN: OPENMAP
( function ( Popcorn ) {
/**
* openmap popcorn plug-in
* Adds an OpenLayers map and open map tiles (OpenStreetMap [default], NASA WorldWind, or USGS Topographic)
* Based on the googlemap popcorn plug-in. No StreetView support
* Options parameter will need a start, end, target, type, zoom, lat and lng
* -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: ROADMAP (OpenStreetMap), SATELLITE (NASA WorldWind / LandSat), or TERRAIN (USGS). ROADMAP/OpenStreetMap is the default.
* -Zoom [optional] defaults to 2
* -Lat and Lng are the coordinates of the map if location is not named
* -Location is a name of a place to center the map, geocoded to coordinates using TinyGeocoder.com
* -Markers [optional] is an array of map marker objects, with the following properties:
* --Icon is the URL of a map marker image
* --Size [optional] is the radius in pixels of the scaled marker image (default is 14)
* --Text [optional] is the HTML content of the map marker -- if your popcorn instance is named 'popped', use <script>popped.currentTime(10);</script> to control the video
* --Lat and Lng are coordinates of the map marker if location is not specified
* --Location is a name of a place for the map marker, geocoded to coordinates using TinyGeocoder.com
* Note: using location requires extra loading time, also not specifying both lat/lng and location will
* cause a JavaScript error.
* @param {Object} options
*
* Example:
var p = Popcorn( '#video' )
.openmap({
start: 5,
end: 15,
type: 'ROADMAP',
target: 'map',
lat: 43.665429,
lng: -79.403323
})
*
*/
var newdiv,
i = 1;
// insert openlayers api script once
if ( !window.OpenLayers ) {
Popcorn.getScript( "http://openlayers.org/api/OpenLayers.js", function() {});
}
function toggle( container, display ) {
if ( container.map ) {
container.map.div.style.display = display;
return;
}
setTimeout(function() {
toggle( container, display );
}, 10 );
}
Popcorn.plugin( "openmap", function( options ){
var newdiv,
centerlonlat,
projection,
displayProjection,
pointLayer,
selectControl,
popup,
isGeoReady,
target = document.getElementById( options.target );
// create a new div within the target div
// this is later passed on to the maps api
newdiv = document.createElement( "div" );
newdiv.id = "openmapdiv" + i;
newdiv.style.width = "100%";
newdiv.style.height = "100%";
i++;
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
target && target.appendChild( newdiv );
// callback function fires when the script is run
isGeoReady = function() {
if ( !window.OpenLayers ) {
setTimeout(function() {
isGeoReady();
}, 50);
} else {
if ( options.location ) {
// set a dummy center at start
location = new OpenLayers.LonLat( 0, 0 );
// query TinyGeocoder and re-center in callback
Popcorn.getJSONP(
"http://tinygeocoder.com/create-api.php?q=" + options.location + "&callback=jsonp",
function( latlng ) {
centerlonlat = new OpenLayers.LonLat( latlng[1], latlng[0] );
options.map.setCenter( centerlonlat );
}
);
} else {
centerlonlat = new OpenLayers.LonLat( options.lng, options.lat );
}
options.type = options.type || "ROADMAP";
if ( options.type === "SATELLITE" ) {
// add NASA WorldWind / LANDSAT map
options.map = new OpenLayers.Map( { div: newdiv, "maxResolution": 0.28125, tileSize: new OpenLayers.Size( 512, 512 ) } );
var worldwind = new OpenLayers.Layer.WorldWind( "LANDSAT", "http://worldwind25.arc.nasa.gov/tile/tile.aspx", 2.25, 4, { T: "105" } );
options.map.addLayer( worldwind );
displayProjection = new OpenLayers.Projection( "EPSG:4326" );
projection = new OpenLayers.Projection( "EPSG:4326" );
}
else if ( options.type === "TERRAIN" ) {
// add terrain map ( USGS )
displayProjection = new OpenLayers.Projection( "EPSG:4326" );
projection = new OpenLayers.Projection( "EPSG:4326" );
options.map = new OpenLayers.Map( {div: newdiv, projection: projection } );
var relief = new OpenLayers.Layer.WMS( "USGS Terraserver", "http://terraserver-usa.org/ogcmap.ashx?", { layers: 'DRG' } );
options.map.addLayer( relief );
} else {
// add OpenStreetMap layer
projection = new OpenLayers.Projection( 'EPSG:900913' );
displayProjection = new OpenLayers.Projection( 'EPSG:4326' );
centerlonlat = centerlonlat.transform( displayProjection, projection );
options.map = new OpenLayers.Map( { div: newdiv, projection: projection, "displayProjection": displayProjection } );
var osm = new OpenLayers.Layer.OSM();
options.map.addLayer( osm );
}
if ( options.map ) {
options.map.div.style.display = "none";
}
}
};
isGeoReady();
return {
/**
* @member openmap
* The setup function will be executed when the plug-in is instantiated
*/
_setup: function( options ) {
var isReady = function() {
// wait until OpenLayers has been loaded, and the start function is run, before adding map
if ( !options.map ) {
setTimeout(function() {
isReady();
}, 13 );
} else {
// default zoom is 2
options.zoom = options.zoom || 2;
// make sure options.zoom is a number
if ( options.zoom && typeof options.zoom !== "number" ) {
options.zoom = +options.zoom;
}
// reset the location and zoom just in case the user played with the map
options.map.setCenter( centerlonlat, options.zoom );
if ( options.markers ) {
var layerStyle = OpenLayers.Util.extend( {} , OpenLayers.Feature.Vector.style[ 'default' ] ),
featureSelected = function( clickInfo ) {
clickedFeature = clickInfo.feature;
if ( !clickedFeature.attributes.text ) {
return;
}
popup = new OpenLayers.Popup.FramedCloud(
"featurePopup",
clickedFeature.geometry.getBounds().getCenterLonLat(),
new OpenLayers.Size( 120, 250 ),
clickedFeature.attributes.text,
null,
true,
function( closeInfo ) {
selectControl.unselect( this.feature );
}
);
clickedFeature.popup = popup;
popup.feature = clickedFeature;
options.map.addPopup( popup );
},
featureUnSelected = function( clickInfo ) {
feature = clickInfo.feature;
if ( feature.popup ) {
popup.feature = null;
options.map.removePopup( feature.popup );
feature.popup.destroy();
feature.popup = null;
}
},
gcThenPlotMarker = function( myMarker ) {
Popcorn.getJSONP(
"http://tinygeocoder.com/create-api.php?q=" + myMarker.location + "&callback=jsonp",
function( latlng ) {
var myPoint = new OpenLayers.Geometry.Point( latlng[1], latlng[0] ).transform( displayProjection, projection ),
myPointStyle = OpenLayers.Util.extend( {}, layerStyle );
if ( !myMarker.size || isNaN( myMarker.size ) ) {
myMarker.size = 14;
}
myPointStyle.pointRadius = myMarker.size;
myPointStyle.graphicOpacity = 1;
myPointStyle.externalGraphic = myMarker.icon;
var myPointFeature = new OpenLayers.Feature.Vector( myPoint, null, myPointStyle );
if ( myMarker.text ) {
myPointFeature.attributes = {
text: myMarker.text
};
}
pointLayer.addFeatures( [ myPointFeature ] );
}
);
};
pointLayer = new OpenLayers.Layer.Vector( "Point Layer", { style: layerStyle } );
options.map.addLayer( pointLayer );
for ( var m = 0, l = options.markers.length; m < l ; m++ ) {
var myMarker = options.markers[ m ];
if( myMarker.text ){
if( !selectControl ){
selectControl = new OpenLayers.Control.SelectFeature( pointLayer );
options.map.addControl( selectControl );
selectControl.activate();
pointLayer.events.on({
"featureselected": featureSelected,
"featureunselected": featureUnSelected
});
}
}
if ( myMarker.location ) {
var geocodeThenPlotMarker = gcThenPlotMarker;
geocodeThenPlotMarker(myMarker);
} else {
var myPoint = new OpenLayers.Geometry.Point( myMarker.lng, myMarker.lat ).transform( displayProjection, projection ),
myPointStyle = OpenLayers.Util.extend( {}, layerStyle );
if ( !myMarker.size || isNaN( myMarker.size ) ) {
myMarker.size = 14;
}
myPointStyle.pointRadius = myMarker.size;
myPointStyle.graphicOpacity = 1;
myPointStyle.externalGraphic = myMarker.icon;
var myPointFeature = new OpenLayers.Feature.Vector( myPoint, null, myPointStyle );
if ( myMarker.text ) {
myPointFeature.attributes = {
text: myMarker.text
};
}
pointLayer.addFeatures( [ myPointFeature ] );
}
}
}
}
};
isReady();
},
/**
* @member openmap
* 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 ) {
toggle( options, "block" );
},
/**
* @member openmap
* 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 ) {
toggle( options, "none" );
},
_teardown: function( options ) {
target && target.removeChild( newdiv );
newdiv = map = centerlonlat = projection = displayProjection = pointLayer = selectControl = popup = null;
}
};
},
{
about:{
name: "Popcorn OpenMap Plugin",
version: "0.3",
author: "@mapmeld",
website: "mapadelsur.blogspot.com"
},
options:{
start : { elem: 'input', type: 'text', label: 'In'},
end : { elem: 'input', type: 'text', label: 'Out'},
target : 'map-container',
type : { elem: 'select', options:[ 'ROADMAP', 'SATELLITE', 'TERRAIN' ], label: 'Type' },
zoom : { elem: 'input', type: 'text', label: 'Zoom'},
lat : { elem: 'input', type: 'text', label: 'Lat'},
lng : { elem: 'input', type: 'text', label: 'Lng'},
location : { elem: 'input', type: 'text', label: 'Location'},
markers : { elem: 'input', type: 'text', label: 'List Markers'}
}
});
}) ( Popcorn );
// PLUGIN: Google Feed
(function ( Popcorn ) {
var i = 1,
scriptLoaded = false,
dynamicFeedLoad = function() {
var dontLoad = false,
k = 0,
links = document.getElementsByTagName( "link" ),
len = links.length,
head = document.head || document.getElementsByTagName( "head" )[ 0 ],
css = document.createElement( "link" ),
resource = "http://www.google.com/uds/solutions/dynamicfeed/gfdynamicfeedcontrol.";
if ( !window.GFdynamicFeedControl ) {
Popcorn.getScript( resource + "js", function() {
scriptLoaded = true;
});
} else {
scriptLoaded = true;
}
// Checking if the css file is already included
for ( ; k < len; k++ ){
if ( links[ k ].href === resource + "css" ) {
dontLoad = true;
}
}
if ( !dontLoad ) {
css.type = "text/css";
css.rel = "stylesheet";
css.href = resource + "css";
head.insertBefore( css, head.firstChild );
}
};
if ( !window.google ) {
Popcorn.getScript( "http://www.google.com/jsapi", function() {
google.load( "feeds", "1", {
callback: function () {
dynamicFeedLoad();
}
});
});
} else {
dynamicFeedLoad();
}
/**
* googlefeed popcorn plug-in
* Adds a feed from the specified blog url at the target div
* Options parameter will need a start, end, target, url and title
* -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
* -Url is the url of the blog's feed you are trying to access
* -Title is the title of the blog you want displayed above the feed
* -Orientation is the orientation of the blog, accepts either Horizontal or Vertical, defaults to Vertical
* @param {Object} options
*
* Example:
var p = Popcorn("#video")
.googlefeed({
start: 5, // seconds
end: 15, // seconds
target: "map",
url: "http://zenit.senecac.on.ca/~chris.tyler/planet/rss20.xml",
title: "Planet Feed"
} )
*
*/
Popcorn.plugin( "googlefeed" , function( options ) {
// create a new div and append it to the parent div so nothing
// that already exists in the parent div gets overwritten
var newdiv = document.createElement( "div" ),
target = document.getElementById( options.target ),
initialize = function() {
//ensure that the script has been loaded
if ( !scriptLoaded ) {
setTimeout( function () {
initialize();
}, 5 );
} else {
// Create the feed control using the user entered url and title
options.feed = new GFdynamicFeedControl( options.url, newdiv, {
vertical: options.orientation.toLowerCase() === "vertical" ? true : false,
horizontal: options.orientation.toLowerCase() === "horizontal" ? true : false,
title: options.title = options.title || "Blog"
});
}
};
// Default to vertical orientation if empty or incorrect input
if( !options.orientation || ( options.orientation.toLowerCase() !== "vertical" &&
options.orientation.toLowerCase() !== "horizontal" ) ) {
options.orientation = "vertical";
}
newdiv.style.display = "none";
newdiv.id = "_feed" + i;
newdiv.style.width = "100%";
newdiv.style.height = "100%";
i++;
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
target && target.appendChild( newdiv );
initialize();
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 ){
newdiv.setAttribute( "style", "display:inline" );
},
/**
* @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 ){
newdiv.setAttribute( "style", "display:none" );
},
_teardown: function( options ) {
document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( newdiv );
delete options.feed;
}
};
},
{
about: {
name: "Popcorn Google Feed Plugin",
version: "0.1",
author: "David Seifried",
website: "dseifried.wordpress.com"
},
options: {
start: {
elem: "input",
type: "text",
label: "In"
},
end: {
elem: "input",
type: "text",
label: "Out"
},
target: "feed-container",
url: {
elem: "input",
type: "text",
label: "url"
},
title: {
elem: "input",
type: "text",
label: "title"
},
orientation: {
elem: "select",
options: [ "Vertical","Horizontal" ],
label: "orientation"
}
}
});
})( Popcorn );
// PLUGIN: tagthisperson
(function (Popcorn) {
var peopleArray = [];
// one People object per options.target
var People = function() {
this.name = "";
this.contains = { };
this.toString = function() {
var r = [];
for ( var j in this.contains ) {
if ( this.contains.hasOwnProperty( j ) ) {
r.push( " " + this.contains[ j ] );
}
}
return r.toString();
};
};
/**
* tagthisperson popcorn plug-in
* Adds people's names to an element on the page.
* Options parameter will need a start, end, target, image and person.
* 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
* Person is the name of the person who you want to tag
* Image is the url to the image of the person - optional
* href is the url to the webpage of the person - optional
* 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')
.tagthisperson({
start: 5, // seconds
end: 15, // seconds
person: '@annasob',
image: 'http://newshour.s3.amazonaws.com/photos%2Fspeeches%2Fguests%2FRichardNSmith_thumbnail.jpg',
href: 'http://annasob.wordpress.com',
target: 'tagdiv'
} )
*
*/
Popcorn.plugin( "tagthisperson" , ( function() {
return {
manifest: {
about:{
name: "Popcorn tagthisperson Plugin",
version: "0.1",
author: "@annasob",
website: "annasob.wordpress.com"
},
options:{
start : {elem:'input', type:'text', label:'In'},
end : {elem:'input', type:'text', label:'Out'},
target : 'tagthisperson-container',
person : {elem:'input', type:'text', label:'Name'},
image : {elem:'input', type:'text', label:'Image Src'},
href : {elem:'input', type:'text', label:'URL'}
}
},
_setup: function( options ) {
var exists = false,
target = document.getElementById( options.target );
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
// loop through the existing objects to ensure no duplicates
// the idea here is to have one object per unique options.target
for ( var i = 0; i < peopleArray.length; i++ ) {
if ( peopleArray[ i ].name === options.target ) {
options._p = peopleArray[ i ];
exists = true;
break;
}
}
if ( !exists ) {
options._p = new People();
options._p.name = options.target;
peopleArray.push( options._p );
}
},
/**
* @member tagthisperson
* 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._p.contains[ options.person ] = ( options.image ) ? "<img src='" + options.image + "'/> " : "" ;
options._p.contains[ options.person ] += ( options.href ) ? "<a href='" + options.href + "' target='_blank'> " + options.person + "</a>" : options.person ;
document.getElementById( options.target ).innerHTML = options._p.toString();
},
/**
* @member tagthisperson
* 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 ){
delete options._p.contains[ options.person ];
document.getElementById( options.target ).innerHTML = options._p.toString();
}
};
})());
})( Popcorn );
// PLUGIN: Wordriver
(function (Popcorn) {
var container = {},
spanLocation = 0,
setupContainer = function( target ) {
container[ target ] = document.createElement( "div" );
var t = document.getElementById( target );
t && t.appendChild( container[ target ] );
container[ target ].style.height = "100%";
container[ target ].style.position = "relative";
return container[ target ];
},
// creates an object of supported, cross platform css transitions
span = document.createElement( "span" ),
prefixes = [ "webkit", "Moz", "ms", "O", "" ],
specProp = [ "Transform", "TransitionDuration", "TransitionTimingFunction" ],
supports = {},
prop;
document.getElementsByTagName( "head" )[ 0 ].appendChild( span );
for ( var sIdx = 0, sLen = specProp.length; sIdx < sLen; sIdx++ ) {
for ( var pIdx = 0, pLen = prefixes.length; pIdx < pLen; pIdx++ ) {
prop = prefixes[ pIdx ] + specProp[ sIdx ];
if ( prop in span.style ) {
supports[ specProp[ sIdx ].toLowerCase() ] = prop;
break;
}
}
}
// Garbage collect support test span
document.getElementsByTagName( "head" )[ 0 ].appendChild( span );
/**
* Word River popcorn plug-in
* Displays a string of text, fading it in and out
* while transitioning across the height of the parent container
* for the duration of the instance (duration = end - start)
*
* @param {Object} options
*
* Example:
var p = Popcorn( '#video' )
.wordriver({
start: 5, // When to begin the Word River animation
end: 15, // When to finish the Word River animation
text: 'Hello World', // The text you want to be displayed by Word River
target: 'wordRiverDiv', // The target div to append the text to
color: "blue" // The color of the text. (can be Hex value i.e. #FFFFFF )
} )
*
*/
Popcorn.plugin( "wordriver" , {
manifest: {
about:{
name: "Popcorn WordRiver Plugin"
},
options:{
start : {elem:'input', type:'text', label:'In'},
end : {elem:'input', type:'text', label:'Out'},
target : 'wordriver-container',
text : {elem:'input', type:'text', label:'Text'},
color : {elem:'input', type:'text', label:'Color'}
}
},
_setup: function( options ) {
if ( !document.getElementById( options.target ) && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
options._duration = options.end - options.start;
options._container = container[ options.target ] || setupContainer( options.target );
options.word = document.createElement( "span" );
options.word.style.position = "absolute";
options.word.style.whiteSpace = "nowrap";
options.word.style.opacity = 0;
options.word.style.MozTransitionProperty = "opacity, -moz-transform";
options.word.style.webkitTransitionProperty = "opacity, -webkit-transform";
options.word.style.OTransitionProperty = "opacity, -o-transform";
options.word.style.transitionProperty = "opacity, transform";
options.word.style[ supports.transitionduration ] = 1 + "s, " + options._duration + "s";
options.word.style[ supports.transitiontimingfunction ] = "linear";
options.word.innerHTML = options.text;
options.word.style.color = options.color || "black";
},
start: function( event, options ){
options._container.appendChild( options.word );
// Resets the transform when changing to a new currentTime before the end event occurred.
options.word.style[ supports.transform ] = "";
options.word.style.fontSize = ~~( 30 + 20 * Math.random() ) + "px";
spanLocation = spanLocation % ( options._container.offsetWidth - options.word.offsetWidth );
options.word.style.left = spanLocation + "px";
spanLocation += options.word.offsetWidth + 10;
options.word.style[ supports.transform ] = "translateY(" +
( options._container.offsetHeight - options.word.offsetHeight ) + "px)";
options.word.style.opacity = 1;
// automatically clears the word based on time
setTimeout( function() {
options.word.style.opacity = 0;
// ensures at least one second exists, because the fade animation is 1 second
}, ( ( (options.end - options.start) - 1 ) || 1 ) * 1000 );
},
end: function( event, options ){
// manually clears the word based on user interaction
options.word.style.opacity = 0;
},
_teardown: function( options ) {
// removes word span from generated container
options.word.parentNode && options._container.removeChild( options.word );
// if no more word spans exist in container, remove container
container[ options.target ] &&
!container[ options.target ].childElementCount &&
document.getElementById( options.target ).removeChild( container[ options.target ] ) &&
delete container[ options.target ];
}
});
})( Popcorn );
// PLUGIN: Subtitle
(function ( Popcorn ) {
var scriptLoaded = false,
i = 0,
callBack = function( data ) {
if ( window.google && google.load ) {
google.load( "language", "1", {
callback: function() {
scriptLoaded = true;
}
});
} else {
setTimeout( function() {
callBack( data );
}, 1);
}
},
createDefaultContainer = function( context ) {
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 = Popcorn.guid();
style.position = "absolute";
style.color = "white";
style.textShadow = "black 2px 2px 6px";
style.fontWeight = "bold";
style.textAlign = "center";
updatePosition();
context.media.parentNode.appendChild( ctxContainer );
};
Popcorn.getScript( "http://www.google.com/jsapi", callBack );
/**
* Subtitle popcorn plug-in
* Displays a subtitle over the video, or in the target div
* Options parameter will need a start, and end.
* Optional parameters are 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
* Target is the id of the document element that the content is
* appended to, this target element must exist on the DOM
* Text is the text of the subtitle you want to display.
*
* Language is the expected language the subtitle text is in
* Languagesrc is the target id of the element that contains
* the language value ("en", "fr", etc.) to translate the text into
* example:
* <select id="language">
* <option value="zh" selected="selected">Chinese</option>
* <option value="en">English</option>
* <option value="fr">French</option>
* <option value="de">German</option>
* <option value="it">Italian</option>
* <option value="ja">Japanese</option>
* <option value="ko">Korean</option>
* <option value="fa">Persian</option>
* <option value="pl">Polish</option>
* <option value="pt">Portuguese</option>
* <option value="es">Spanish</option>
* </select>
* Accessibilitysrc is the target id of a checkbox element
* checked means show all subtitles regardless of language and languagesrc
* not checked means only translate if language and languagesrc are different
* if no accessibilitysrc exists, default is to display all subtitles regardless
*
* @param {Object} options
*
* Example:
var p = Popcorn('#video')
.subtitle({
start: 5, // seconds, mandatory
end: 15, // seconds, mandatory
text: 'Hellow world', // optional
target: 'subtitlediv', // optional
language: 'en', // optional
languagesrc: 'language', // optional
accessibilitysrc: 'accessibility' // optional
} )
*
*/
// translates whatever is in options.container into selected language
var translate = function( options, text ) {
options.selectedLanguage = options.languageSrc.options[ options.languageSrc.selectedIndex ].value;
google.language.translate( text, "", options.selectedLanguage, function( result ) {
for( var k = 0, children = options.container.children, len = children.length; k < len; k++ ) {
if ( children[ k ].style.display === "inline" ) {
children[ k ].innerHTML = result.translation;
}
}
});
};
Popcorn.plugin( "subtitle" , {
manifest: {
about: {
name: "Popcorn Subtitle Plugin",
version: "0.1",
author: "Scott Downe",
website: "http://scottdowne.wordpress.com/"
},
options: {
start: {
elem: "input",
type: "text",
label: "In"
},
end: {
elem: "input",
type: "text",
label: "Out"
},
target: "subtitle-container",
text: {
elem: "input",
type: "text",
label: "Text"
}
}
},
_setup: function( options ) {
var newdiv = document.createElement( "div" ),
accessibility = document.getElementById( options.accessibilitysrc );
newdiv.id = "subtitle-" + i;
newdiv.style.display = "none";
i++;
// Creates a div for all subtitles to use
( !this.container && ( !options.target || options.target === "subtitle-container" ) ) &&
createDefaultContainer( this );
// if a target is specified, use that
if ( options.target && options.target !== "subtitle-container" ) {
options.container = document.getElementById( options.target );
} else {
// use shared default container
options.container = this.container;
}
document.getElementById( options.container.id ).appendChild( newdiv );
options.innerContainer = newdiv;
options.showSubtitle = function() {
options.innerContainer.innerHTML = options.text;
};
options.toggleSubtitles = function() {};
var readyCheck = setInterval(function() {
if ( !scriptLoaded ) {
return;
}
clearInterval( readyCheck );
if ( options.languagesrc ) {
options.showSubtitle = translate;
options.languageSrc = document.getElementById( options.languagesrc );
options.selectedLanguage = options.languageSrc.options[ options.languageSrc.selectedIndex ].value;
if ( !this.languageSources ) {
this.languageSources = {};
}
if ( !this.languageSources[ options.languagesrc ] ) {
this.languageSources[ options.languagesrc ] = {};
}
if ( !this.languageSources[ options.languagesrc ][ options.target ] ) {
this.languageSources[ options.languagesrc ][ options.target ] = true;
options.languageSrc.addEventListener( "change", function() {
options.toggleSubtitles();
for( var k = 0, children = options.container.children, len = children.length; k < len; k++ ) {
if ( children[ k ].style.display === "inline" ) {
options.showSubtitle( options, children[ k ].innerHTML );
}
}
}, false );
}
}
if ( accessibility ) {
options.accessibility = accessibility;
options.toggleSubtitles = function() {
options.selectedLanguage = options.languageSrc.options[ options.languageSrc.selectedIndex ].value;
if ( options.accessibility.checked || options.selectedLanguage !== ( options.language || "") ) {
options.display = "inline";
options.container.style.display = options.display;
} else if ( options.selectedLanguage === ( options.language || "") ) {
options.display = "none";
options.container.style.display = options.display;
}
};
options.accessibility.addEventListener( "change", options.toggleSubtitles, false );
// initiate it once to set starting state
options.toggleSubtitles();
}
}, 5);
},
/**
* @member subtitle
* 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.innerContainer.style.display = "inline";
options.showSubtitle( options, options.text );
},
/**
* @member subtitle
* 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.innerContainer.style.display = "none";
options.innerContainer.innerHTML = "";
},
_teardown: function ( options ) {
options.container.removeChild( options.innerContainer );
}
});
})( Popcorn );
// PLUGIN: Google News
(function (Popcorn) {
var scriptLoaded = false,
scriptLoading = false,
callBack = function( data ) {
if ( typeof google !== 'undefined' && google.load ) {
google.load("elements", "1", {packages : ["newsshow"], callback: function() {scriptLoaded = true;}});
} else {
setTimeout( function() {
callBack( data );
}, 1);
}
};
/**
* Google News popcorn plug-in
* Displays Google News information on a topic in a targeted div.
* Options parameter will need a start, end and target.
* Optional parameters are topic. topic defaults to "top stories"
* 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 document element that the content is
* appended to, this target element must exist on the DOM
* Topic is the topic of news articles you want to display.
*
* @param {Object} options
*
* Example:
var p = Popcorn('#video')
.googlenews({
start: 5, // seconds, mandatory
end: 15, // seconds, mandatory
topic: 'oil spill', // optional
target: 'newsdiv' // mandatory
} )
*
*/
Popcorn.plugin( "googlenews" , {
manifest: {
about:{
name: "Popcorn Google News Plugin",
version: "0.1",
author: "Scott Downe",
website: "http://scottdowne.wordpress.com/"
},
options:{
start : {elem:'input', type:'text', label:'In'},
end : {elem:'input', type:'text', label:'Out'},
target : 'news-container',
topic : {elem:'select', type:'text', label:'Type'}
}
},
_setup : function( options ) {
var target = document.getElementById( options.target );
options.container = document.createElement( 'div' );
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
var container = document.createElement( "div" );
target && target.appendChild( options.container );
options.container.appendChild( container );
if ( !scriptLoading ) {
scriptLoading = true;
Popcorn.getScript( "http://www.google.com/jsapi", callBack );
}
var readyCheck = setInterval(function() {
if ( !scriptLoaded ) {
return;
}
clearInterval( readyCheck );
options.newsShow = target && new google.elements.NewsShow( container, {
format : "300x250",
queryList : [
{ q: options.topic || "Top Stories" }
]
} );
}, 5);
options.container.style.display = "none";
},
/**
* @member googlenews
* 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.setAttribute( 'style', 'display:inline' );
},
/**
* @member googlenews
* 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.setAttribute( 'style', 'display:none' );
},
_teardown: function( options ) {
// google news does not like this, throws an error "a is null"
// doesn't hurt popcorn, and only happens once
document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( options.container );
options.newsShow = null;
}
});
})( Popcorn );
// PLUGIN: Attribution
(function( Popcorn ) {
/**
* Attribution popcorn plug-in
* Adds text to an element on the page.
* Options parameter will need a mandatory start, end, target.
* Optional parameters include nameofwork, NameOfWorkUrl, CopyrightHolder, CopyrightHolderUrl, license & licenseUrl.
* 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 document element that the text needs to be attached to, this target element must exist on the DOM
* nameofwork is the title of the attribution
* NameOfWorkUrl is a url that provides more details about the attribution
* CopyrightHolder is the name of the person/institution that holds the rights to the attribution
* CopyrightHolderUrl is the url that provides more details about the copyrightholder
* license is the type of license that the work is copyrighted under
* LicenseUrl is the url that provides more details about the ticense type
* @param {Object} options
*
* Example:
var p = Popcorn('#video')
.attribution({
start: 5, // seconds
end: 15, // seconds
target: 'attributiondiv'
} )
*
*/
Popcorn.plugin( "attribution" , (function(){
var
common = "data:image/png;base64,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",
licenses = {
"cc-by": common + "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",
"cc-by-sa": common + "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",
"cc-by-nd": common + "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",
"cc-by-nc": common + "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",
"cc-by-nc-sa": common + "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",
"cc-by-nc-nd": common + "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"
},
target;
return {
manifest: {
about:{
name: "Popcorn Attribution Plugin",
version: "0.2",
author: "@rwaldron",
website: "github.com/rwldrn"
},
options:{
start: { elem:"input", type:"text", label:"In" },
end: { elem:"input", type:"text", label:"Out" },
nameofwork: { elem:"input", type:"text", label:"Name of Work" },
nameofworkurl: { elem:"input", type:"text", label:"Url of Work" },
copyrightholder: { elem:"input", type:"text", label:"Copyright Holder" },
copyrightholderurl: { elem:"input", type:"text", label:"Copyright Holder Url" },
license: { elem:"input", type:"text", label:"License type" },
licenseurl: { elem:"input", type:"text", label:"License URL" },
target: "attribution-container"
}
},
_setup: function( options ) {
var attrib = "",
license = options.license && licenses[ options.license.toLowerCase() ],
tar = "target=_blank";
// make a div to put the information into
options._container = document.createElement( "div" );
options._container.style.display = "none";
// Cache declared target
target = document.getElementById( options.target );
if ( options.nameofworkurl ) {
attrib += "<a href='" + options.nameofworkurl + "' " + tar + ">";
}
if ( options.nameofwork ) {
attrib += options.nameofwork;
}
if ( options.nameofworkurl ) {
attrib += "</a>";
}
if ( options.copyrightholderurl ) {
attrib += "<a href='" + options.copyrightholderurl + "' " + tar + ">";
}
if ( options.copyrightholder ) {
attrib += ", " + options.copyrightholder;
}
if ( options.copyrightholderurl ) {
attrib += "</a>";
}
//if the user did not specify any parameters just pull the text from the tag
if ( attrib === "" ) {
attrib = options.text;
}
if ( options.license ) {
if ( license ) {
if ( options.licenseurl ) {
attrib = "<a href='" + options.licenseurl + "' " + tar + "><img src='"+ license +"' border='0'/></a> " + attrib;
} else {
attrib = "<img src='"+ license +"' />" + attrib;
}
} else {
attrib += ", license: ";
if ( options.licenseurl ) {
attrib += "<a href='" + options.licenseurl + "' " + tar + ">" + options.license + "</a> ";
} else {
attrib += options.license;
}
}
} else if ( options.licenseurl ) {
attrib += ", <a href='" + options.licenseurl + "' " + tar + ">license</a> ";
}
options._container.innerHTML = attrib;
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
target && target.appendChild( options._container );
},
/**
* @member attribution
* 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 attribution
* 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 ) {
// Cache declared target
target = document.getElementById( options.target );
target && target.removeChild( options._container );
}
};
})());
})( Popcorn );
//PLUGIN: linkedin
(function (Popcorn){
/**
* LinkedIn Popcorn plug-in
* Places a LinkedIn plugin inside a div ( http://developers.facebook.com/docs/plugins/ )
* Options parameter will need a start, end, target, type, and an api key
* Optional parameters are url, counter, format, companyid, and productid
* 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 document element that the plugin needs to be attached to, this target element must exist on the DOM
* Type is the name of the plugin, options are share, memberprofile, companyinsider, companyprofile, or recommendproduct
* Apikey is your own api key from obtained from https://www.linkedin.com/secure/developer
* Url is the desired url to share via LinkedIn. Defaults to the current page if no url is specified
* Counter is the position where the counter will be positioned. This is used if the type is "share" or "recommendproduct"
* The options are right and top (don't include this option if you do not want a counter)
* Format is the data format of the member and company profile plugins. The options are inlined, hover, and click. Defaults to inline
* Companyid must be specified if the type is "companyprofile," "companyinsider," or "recommendproduct"
* Productid must be specified if the type is "recommendproduct"
*
* @param {Object} options
*
* Example:
* <script src="popcorn.linkedin.js"></script>
* ...
* var p = Popcorn("#video")
* .linkedin({
* type: "share",
* url: "http://www.google.ca",
* counter: "right",
* target: "sharediv"
* apikey: "ZOLRI2rzQS_oaXELpPF0aksxwFFEvoxAFZRLfHjaAhcGPfOX0Ds4snkJpWwKs8gk",
* start: 1,
* end: 3
* })
*
* This plugin will be displayed between 1 and 3 seconds, inclusive, in the video. This will show how many people have "shared" Google via LinkedIn,
* with the number of people (counter) displayed to the right of the share plugin.
*/
Popcorn.plugin( "linkedin", {
manifest: {
about: {
name: "Popcorn LinkedIn Plugin",
version: "0.1",
author: "Dan Ventura",
website: "dsventura.blogspot.com"
},
options: {
type: {
elem: "input",
type: "text",
label: "Type"
},
target: "linkedin-container"
}
},
_setup: function( options ) {
var apikey = options.apikey,
target = document.getElementById( options.target ),
script = document.createElement( "script" );
Popcorn.getScript("http://platform.linkedin.com/in.js");
options._container = document.createElement( "div" );
options._container.appendChild( script );
if ( apikey ) {
script.innerHTML = "api_key: " + apikey;
}
options.type = options.type && options.type.toLowerCase() || "";
// Replace the LinkedIn plugin's error message to something more helpful
var errorMsg = function() {
options._container = document.createElement( "p" );
options._container.innerHTML = "Plugin requires a valid <a href='https://www.linkedin.com/secure/developer'>apikey</a>";
if ( !target && Popcorn.plugin.debug ) {
throw ( "target container doesn't exist" );
}
target && target.appendChild( options._container );
};
var setOptions = (function ( options ) {
return {
share: function () {
script.setAttribute( "type", "IN/Share" );
if ( options.counter ) {
script.setAttribute( "data-counter", options.counter );
}
if ( options.url ) {
script.setAttribute( "data-url", options.url );
}
},
memberprofile: function () {
script.setAttribute( "type", "IN/MemberProfile" );
script.setAttribute( "data-id", ( options.memberid ) );
script.setAttribute( "data-format", ( options.format || "inline" ) );
if ( options.text && options.format.toLowerCase() !== "inline" ) {
script.setAttribute( "data-text", options.text );
}
},
companyinsider: function () {
script.setAttribute( "type", "IN/CompanyInsider" );
script.setAttribute( "data-id", options.companyid );
if( options.modules ) {
options._container.setAttribute( "data-modules", options.modules );
}
},
companyprofile: function () {
script.setAttribute( "type", "IN/CompanyProfile" );
script.setAttribute( "data-id", ( options.companyid ) );
script.setAttribute( "data-format", ( options.format || "inline" ) );
if ( options.text && options.format.toLowerCase() !== "inline" ) {
script.setAttribute( "data-text", options.text );
}
if ( options.related !== undefined ) {
script.setAttribute( "data-related", options.related );
}
},
recommendproduct: function () {
script.setAttribute( "type", "IN/RecommendProduct" );
script.setAttribute( "data-company", ( options.companyid || "LinkedIn" ) );
script.setAttribute( "data-product", ( options.productid || "201714" ) );
if ( options.counter ) {
script.setAttribute( "data-counter", options.counter );
}
}
};
})( options );
if ( !apikey ) {
errorMsg();
} else {
setOptions[ options.type ] && setOptions[ options.type ]();
}
if ( !target && Popcorn.plugin.debug ) {
throw new Error( "target container doesn't exist" );
}
target && target.appendChild( options._container );
options._container.style.display = "none";
},
/**
* @member linkedin
* 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 = "block";
},
/**
* @member linkedin
* 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 tar = document.getElementById( options.target );
tar && tar.removeChild( options._container );
}
});
})( Popcorn );
// PARSER: 0.3 SSA/ASS
(function (Popcorn) {
/**
* SSA/ASS popcorn parser plug-in
* Parses subtitle files in the identical SSA and ASS formats.
* Style information is ignored, and may be found in these
* formats: (\N \n {\pos(400,570)} {\kf89})
* Out of the [Script Info], [V4 Styles], [Events], [Pictures],
* and [Fonts] sections, only [Events] is processed.
* Data parameter is given by Popcorn, will need a text.
* Text is the file contents to be parsed
*
* @param {Object} data
*
* Example:
[Script Info]
Title: Testing subtitles for the SSA Format
[V4 Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding
Style: Default,Arial,20,65535,65535,65535,-2147483640,-1,0,1,3,0,2,30,30,30,0,0
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:02.40,0:00:07.20,Default,,0000,0000,0000,,Senator, {\kf89}we're \Nmaking our final \napproach into Coruscant.
Dialogue: 0,0:00:09.71,0:00:13.39,Default,,0000,0000,0000,,{\pos(400,570)}Very good, Lieutenant.
Dialogue: 0,0:00:15.04,0:00:18.04,Default,,0000,0000,0000,,It's \Na \ntrap!
*
*/
// Register for SSA extensions
Popcorn.parser( "parseSSA", function( data ) {
// declare needed variables
var retObj = {
title: "",
remote: "",
data: []
},
subs = [],
startIdx,
endIdx,
textIdx,
lines,
fields,
numFields,
sub,
text,
i = 0,
j = 0,
len = 0,
fieldLen = 0;
// h:mm:ss.cc (centisec) string to SS.mmm
// Returns -1 if invalid
var toSeconds = function( t_in ) {
var t = t_in.split( ":" ),
l = t.length - 1;
// Not all there
if ( t_in.length !== 10 ) {
return -1;
}
return parseInt( t[0], 10 )*3600 + parseInt( t[l-1], 10 )*60 + parseFloat( t[l], 10 );
};
var createTrack = function( name, attributes ) {
var track = {};
track[name] = attributes;
return track;
};
// Here is where the magic happens
// Split on line breaks
lines = data.text.split( /(?:\r\n|\r|\n)/gm );
len = lines.length;
// Ignore non-textual info
while ( i < len && lines[i] !== "[Events]" ) {
i++;
}
fields = lines[++i].substr( 8 ).split( ", " ); // Trim 'Format: ' off front, split on delim
numFields = fields.length;
//Find where in Dialogue string the start, end and text info is
for ( ; j < numFields; j++ ) {
if ( fields[j] === "Start" ) {
startIdx = j;
} else if ( fields[j] === "End" ) {
endIdx = j;
} else if ( fields[j] === "Text" ) {
textIdx = j;
}
}
while ( ++i < len && lines[i] && lines[i][0] !== "[" ) {
sub = {};
// Trim beginning 'Dialogue: ' and split on delim
fields = lines[i].substr( 10 ).split( "," );
sub.start = toSeconds( fields[startIdx] );
sub.end = toSeconds( fields[endIdx] );
// Invalid time, skip
if ( sub.start === -1 || sub.end === -1 ) {
continue;
}
if ( ( fieldLen = fields.length ) === numFields ) {
sub.text = fields[textIdx];
} else {
// There were commas in the text which were split, append back together into one line
text = [];
for( j = textIdx; j < fieldLen; j++ ) {
text.push( fields[j] );
}
sub.text = text.join( "," );
}
// Eliminate advanced styles and convert forced line breaks
sub.text = sub.text.replace( /\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}/gi, "" ).replace( /\\N/gi, "<br />" );
subs.push( createTrack( "subtitle", sub ) );
}
retObj.data = subs;
return retObj;
});
})( Popcorn );
// PARSER: 0.1 TTXT
(function (Popcorn) {
/**
* TTXT popcorn parser plug-in
* Parses subtitle files in the TTXT format.
* Style information is ignored.
* Data parameter is given by Popcorn, will need an xml.
* Xml is the file contents to be parsed as a DOM tree
*
* @param {Object} data
*
* Example:
<TextSample sampleTime="00:00:00.000" text=""></TextSample>
*/
Popcorn.parser( "parseTTXT", function( data ) {
// declare needed variables
var returnData = {
title: "",
remote: "",
data: []
};
// Simple function to convert HH:MM:SS.MMM to SS.MMM
// Assume valid, returns 0 on error
var toSeconds = function(t_in) {
var t = t_in.split(":");
var time = 0;
try {
return parseFloat(t[0], 10)*60*60 + parseFloat(t[1], 10)*60 + parseFloat(t[2], 10);
} catch (e) { time = 0; }
return time;
};
// creates an object of all atrributes keyed by name
var createTrack = function( name, attributes ) {
var track = {};
track[name] = attributes;
return track;
};
// this is where things actually start
var node = data.xml.lastChild.lastChild; // Last Child of TextStreamHeader
var lastStart = Number.MAX_VALUE;
var cmds = [];
// Work backwards through DOM, processing TextSample nodes
while (node) {
if ( node.nodeType === 1 && node.nodeName === "TextSample") {
var sub = {};
sub.start = toSeconds(node.getAttribute('sampleTime'));
sub.text = node.getAttribute('text');
if (sub.text) { // Only process if text to display
// Infer end time from prior element, ms accuracy
sub.end = lastStart - 0.001;
cmds.push( createTrack("subtitle", sub) );
}
lastStart = sub.start;
}
node = node.previousSibling;
}
returnData.data = cmds.reverse();
return returnData;
});
})( Popcorn );
// PARSER: 0.3 JSON
(function (Popcorn) {
Popcorn.parser( "parseJSON", "JSON", function( data ) {
// declare needed variables
var retObj = {
title: "",
remote: "",
data: []
},
manifestData = {},
dataObj = data;
/*
TODO: add support for filling in source children of the video element
remote: [
{
src: "whatever.mp4",
type: 'video/mp4; codecs="avc1, mp4a"'
},
{
src: "whatever.ogv",
type: 'video/ogg; codecs="theora, vorbis"'
}
]
*/
Popcorn.forEach( dataObj.data, function ( obj, key ) {
retObj.data.push( obj );
});
return retObj;
});
})( Popcorn );
// PARSER: 0.1 SBV
(function (Popcorn) {
/**
* SBV popcorn parser plug-in
* Parses subtitle files in the SBV format.
* Times are expected in H:MM:SS.MIL format, with hours optional
* Subtitles which don't match expected format are ignored
* Data parameter is given by Popcorn, will need a text.
* Text is the file contents to be parsed
*
* @param {Object} data
*
* Example:
0:00:02.400,0:00:07.200
Senator, we're making our final approach into Coruscant.
*/
Popcorn.parser( "parseSBV", function( data ) {
// declare needed variables
var retObj = {
title: "",
remote: "",
data: []
},
subs = [],
lines,
i = 0,
len = 0,
idx = 0;
// [H:]MM:SS.MIL string to SS.MIL
// Will thrown exception on bad time format
var toSeconds = function( t_in ) {
var t = t_in.split( ":" ),
l = t.length-1,
time;
try {
time = parseInt( t[l-1], 10 )*60 + parseFloat( t[l], 10 );
// Hours optionally given
if ( l === 2 ) {
time += parseInt( t[0], 10 )*3600;
}
} catch ( e ) {
throw "Bad cue";
}
return time;
};
var createTrack = function( name, attributes ) {
var track = {};
track[name] = attributes;
return track;
};
// Here is where the magic happens
// Split on line breaks
lines = data.text.split( /(?:\r\n|\r|\n)/gm );
len = lines.length;
while ( i < len ) {
var sub = {},
text = [],
time = lines[i++].split( "," );
try {
sub.start = toSeconds( time[0] );
sub.end = toSeconds( time[1] );
// Gather all lines of text
while ( i < len && lines[i] ) {
text.push( lines[i++] );
}
// Join line breaks in text
sub.text = text.join( "<br />" );
subs.push( createTrack( "subtitle", sub ) );
} catch ( e ) {
// Bad cue, advance to end of cue
while ( i < len && lines[i] ) {
i++;
}
}
// Consume empty whitespace
while ( i < len && !lines[i] ) {
i++;
}
}
retObj.data = subs;
return retObj;
});
})( Popcorn );
// PARSER: 0.1 XML
(function (Popcorn) {
/**
*
*
*/
Popcorn.parser( "parseXML", "XML", function( data ) {
// declare needed variables
var returnData = {
title: "",
remote: "",
data: []
},
manifestData = {};
// Simple function to convert 0:05 to 0.5 in seconds
// acceptable formats are HH:MM:SS:MM, MM:SS:MM, SS:MM, SS
var toSeconds = function(time) {
var t = time.split(":");
if (t.length === 1) {
return parseFloat(t[0], 10);
} else if (t.length === 2) {
return parseFloat(t[0], 10) + parseFloat(t[1] / 12, 10);
} else if (t.length === 3) {
return parseInt(t[0] * 60, 10) + parseFloat(t[1], 10) + parseFloat(t[2] / 12, 10);
} else if (t.length === 4) {
return parseInt(t[0] * 3600, 10) + parseInt(t[1] * 60, 10) + parseFloat(t[2], 10) + parseFloat(t[3] / 12, 10);
}
};
// turns a node tree element into a straight up javascript object
// also converts in and out to start and end
// also links manifest data with ids
var objectifyAttributes = function ( nodeAttributes ) {
var returnObject = {};
for ( var i = 0, nal = nodeAttributes.length; i < nal; i++ ) {
var key = nodeAttributes.item(i).nodeName,
data = nodeAttributes.item(i).nodeValue;
// converts in into start
if (key === "in") {
returnObject.start = toSeconds( data );
// converts out into end
} else if ( key === "out" ){
returnObject.end = toSeconds( data );
// this is where ids in the manifest are linked
} else if ( key === "resourceid" ) {
Popcorn.extend( returnObject, manifestData[data] );
// everything else
} else {
returnObject[key] = data;
}
}
return returnObject;
};
// creates an object of all atrributes keyd by name
var createTrack = function( name, attributes ) {
var track = {};
track[name] = attributes;
return track;
};
// recursive function to process a node, or process the next child node
var parseNode = function ( node, allAttributes, manifest ) {
var attributes = {};
Popcorn.extend( attributes, allAttributes, objectifyAttributes( node.attributes ), { text: node.textContent } );
var childNodes = node.childNodes;
// processes the node
if ( childNodes.length < 1 || ( childNodes.length === 1 && childNodes[0].nodeType === 3 ) ) {
if ( !manifest ) {
returnData.data.push( createTrack( node.nodeName, attributes ) );
} else {
manifestData[attributes.id] = attributes;
}
// process the next child node
} else {
for ( var i = 0; i < childNodes.length; i++ ) {
if ( childNodes[i].nodeType === 1 ) {
parseNode( childNodes[i], attributes, manifest );
}
}
}
};
// this is where things actually start
var x = data.documentElement.childNodes;
for ( var i = 0, xl = x.length; i < xl; i++ ) {
if ( x[i].nodeType === 1 ) {
// start the process of each main node type, manifest or timeline
if ( x[i].nodeName === "manifest" ) {
parseNode( x[i], {}, true );
} else { // timeline
parseNode( x[i], {}, false );
}
}
}
return returnData;
});
})( Popcorn );
// PARSER: 0.3 TTML
(function (Popcorn) {
/**
* TTML popcorn parser plug-in
* Parses subtitle files in the TTML format.
* Times may be absolute to the timeline or relative
* Absolute times are ISO 8601 format (hh:mm:ss[.mmm])
* Relative times are a fraction followed by a unit metric (d.ddu)
* Relative times are relative to the time given on the parent node
* Styling information is ignored
* Data parameter is given by Popcorn, will need an xml.
* Xml is the file contents to be processed
*
* @param {Object} data
*
* Example:
<tt xmlns:tts="http://www.w3.org/2006/04/ttaf1#styling" xmlns="http://www.w3.org/2006/04/ttaf1">
<body region="subtitleArea">
<div>
<p xml:id="subtitle1" begin="0.76s" end="3.45s">
It seems a paradox, does it not,
</p>
</div>
</body>
</tt>
*/
Popcorn.parser( "parseTTML", function( data ) {
// declare needed variables
var returnData = {
title: "",
remote: "",
data: []
},
node,
numTracks = 0,
region;
// Convert time expression to SS.mmm
// Expression may be absolute to timeline (hh:mm:ss.ms)
// or relative ( fraction followedd by metric ) ex: 3.4s, 5.7m
// Returns -1 if invalid
var toSeconds = function ( t_in, offset ) {
if ( !t_in ) {
return -1;
}
var t = t_in.split( ":" ),
l = t.length - 1,
metric,
multiplier,
i;
// Try clock time
if ( l >= 2 ) {
return parseInt( t[0], 10 )*3600 + parseInt( t[l-1], 10 )*60 + parseFloat( t[l], 10 );
}
// Was not clock time, assume relative time
// Take metric from end of string (may not be single character)
// First find metric
for( i = t_in.length - 1; i >= 0; i-- ) {
if ( t_in[i] <= "9" && t_in[i] >= "0" ) {
break;
}
}
// Point i at metric and normalize offsete time
i++;
metric = t_in.substr( i );
offset = offset || 0;
// Determine multiplier for metric relative to seconds
if ( metric === "h" ) {
multiplier = 3600;
} else if ( metric === "m" ) {
multiplier = 60;
} else if ( metric === "s" ) {
multiplier = 1;
} else if ( metric === "ms" ) {
multiplier = 0.001;
} else {
return -1;
}
// Valid multiplier
return parseFloat( t_in.substr( 0, i ) ) * multiplier + offset;
};
// creates an object of all atrributes keyd by name
var createTrack = function( name, attributes ) {
var track = {};
track[name] = attributes;
return track;
};
// Parse a node for text content
var parseNode = function( node, timeOffset ) {
var sub = {};
// Trim left and right whitespace from text and change non-explicit line breaks to spaces
sub.text = node.textContent.replace(/^[\s]+|[\s]+$/gm, "").replace(/(?:\r\n|\r|\n)/gm, "<br />");
sub.id = node.getAttribute( "xml:id" );
sub.start = toSeconds ( node.getAttribute( "begin" ), timeOffset );
sub.end = toSeconds( node.getAttribute( "end" ), timeOffset );
sub.target = region;
if ( sub.end < 0 ) {
// No end given, infer duration if possible
// Otherwise, give end as MAX_VALUE
sub.end = toSeconds( node.getAttribute( "duration" ), 0 );
if ( sub.end >= 0 ) {
sub.end += sub.start;
} else {
sub.end = Number.MAX_VALUE;
}
}
return sub;
};
// Parse the children of the given node
var parseChildren = function( node, timeOffset ) {
var currNode = node.firstChild,
sub,
newOffset;
while ( currNode ) {
if ( currNode.nodeType === 1 ) {
if ( currNode.nodeName === "p" ) {
// p is a teextual node, process contents as subtitle
sub = parseNode( currNode, timeOffset );
returnData.data.push( createTrack( "subtitle", sub ) );
numTracks++;
} else if ( currNode.nodeName === "div" ) {
// div is container for subtitles, recurse
newOffset = toSeconds( currNode.getAttribute("begin") );
if (newOffset < 0 ) {
newOffset = timeOffset;
}
parseChildren( currNode, newOffset );
}
}
currNode = currNode.nextSibling;
}
};
// Null checks
if ( !data.xml || !data.xml.documentElement || !( node = data.xml.documentElement.firstChild ) ) {
return returnData;
}
// Find body tag
while ( node.nodeName !== "body" ) {
node = node.nextSibling;
}
region = "";
parseChildren( node, 0 );
return returnData;
});
})( Popcorn );
// PARSER: 0.3 SRT
(function (Popcorn) {
/**
* SRT popcorn parser plug-in
* Parses subtitle files in the SRT format.
* Times are expected in HH:MM:SS,MIL format, though HH:MM:SS.MIL also supported
* Ignore styling, which may occur after the end time or in-text
* While not part of the "official" spec, majority of players support HTML and SSA styling tags
* SSA-style tags are stripped, HTML style tags are left for the browser to handle:
* HTML: <font>, <b>, <i>, <u>, <s>
* SSA: \N or \n, {\cmdArg1}, {\cmd(arg1, arg2, ...)}
* Data parameter is given by Popcorn, will need a text.
* Text is the file contents to be parsed
*
* @param {Object} data
*
* Example:
1
00:00:25,712 --> 00:00:30.399
This text is <font color="red">RED</font> and has not been {\pos(142,120)} positioned.
This takes \Nup three \nentire lines.
This contains nested <b>bold, <i>italic, <u>underline</u> and <s>strike-through</s></u></i></b> HTML tags
Unclosed but <b>supported tags are left in
<ggg>Unsupported</ggg> HTML tags are left in, even if <hhh>not closed.
SSA tags with {\i1} would open and close italicize {\i0}, but are stripped
Multiple {\pos(142,120)\b1}SSA tags are stripped
*/
Popcorn.parser( "parseSRT", function( data ) {
// declare needed variables
var retObj = {
title: "",
remote: "",
data: []
},
subs = [],
i = 0,
len = 0,
idx = 0,
lines,
time,
text,
sub;
// Simple function to convert HH:MM:SS,MMM or HH:MM:SS.MMM to SS.MMM
// Assume valid, returns 0 on error
var toSeconds = function( t_in ) {
var t = t_in.split( ':' );
try {
var s = t[2].split( ',' );
// Just in case a . is decimal seperator
if ( s.length === 1 ) {
s = t[2].split( '.' );
}
return parseFloat( t[0], 10 )*3600 + parseFloat( t[1], 10 )*60 + parseFloat( s[0], 10 ) + parseFloat( s[1], 10 )/1000;
} catch ( e ) {
return 0;
}
};
var createTrack = function( name, attributes ) {
var track = {};
track[name] = attributes;
return track;
};
// Here is where the magic happens
// Split on line breaks
lines = data.text.split( /(?:\r\n|\r|\n)/gm );
len = lines.length;
for( i=0; i < len; i++ ) {
sub = {};
text = [];
sub.id = parseInt( lines[i++], 10 );
// Split on '-->' delimiter, trimming spaces as well
time = lines[i++].split( /[\t ]*-->[\t ]*/ );
sub.start = toSeconds( time[0] );
// So as to trim positioning information from end
idx = time[1].indexOf( " " );
if ( idx !== -1) {
time[1] = time[1].substr( 0, idx );
}
sub.end = toSeconds( time[1] );
// Build single line of text from multi-line subtitle in file
while ( i < len && lines[i] ) {
text.push( lines[i++] );
}
// Join into 1 line, SSA-style linebreaks
// Strip out other SSA-style tags
sub.text = text.join( "\\N" ).replace( /\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}/gi, "" );
// Escape HTML entities
sub.text = sub.text.replace( /</g, "&lt;" ).replace( />/g, "&gt;" );
// Unescape great than and less than when it makes a valid html tag of a supported style (font, b, u, s, i)
// Modified version of regex from Phil Haack's blog: http://haacked.com/archive/2004/10/25/usingregularexpressionstomatchhtml.aspx
// Later modified by kev: http://kevin.deldycke.com/2007/03/ultimate-regular-expression-for-html-tag-parsing-with-php/
sub.text = sub.text.replace( /&lt;(\/?(font|b|u|i|s))((\s+(\w|\w[\w\-]*\w)(\s*=\s*(?:\".*?\"|'.*?'|[^'\">\s]+))?)+\s*|\s*)(\/?)&gt;/gi, "<$1$3$7>" );
sub.text = sub.text.replace( /\\N/gi, "<br />" );
subs.push( createTrack( "subtitle", sub ) );
}
retObj.data = subs;
return retObj;
});
})( Popcorn );
// PARSER: 0.3 WebSRT/VTT
(function ( Popcorn ) {
/**
* WebSRT/VTT popcorn parser plug-in
* Parses subtitle files in the WebSRT/VTT format.
* Styles which appear after timing information are ignored.
* Inline styling tags follow HTML conventions and are left in for the browser to handle
* TrackEvents (cues) which are malformed are ignored.
* Data parameter is given by Popcorn, will need a text.
* Text is the file contents to be parsed
*
* @param {Object} data
*
* Example:
Track-3
00:00:15.542 --> 00:00:18.542 A:start D:vertical L:98%
It's a <i>trap!</i>
*/
Popcorn.parser( "parseVTT", function( data ) {
// declare needed variables
var retObj = {
title: "",
remote: "",
data: []
},
subs = [],
i = 0,
len = 0,
idx = 0,
lines,
time,
text,
sub;
// [HH:]MM:SS.mmm string to SS.mmm float
// Throws exception if invalid
var toSeconds = function( t_in ) {
var t = t_in.split( ":" ),
l = t_in.length,
time;
// Invalid time string provided
if ( l !== 12 && l !== 9 ) {
throw "Bad cue";
}
l = t.length - 1;
try {
time = parseInt( t[l-1], 10 )*60 + parseFloat( t[l], 10 );
// Hours were given
if ( l === 2 ) {
time += parseInt( t[0], 10 )*3600;
}
} catch ( e ) {
throw "Bad cue";
}
return time;
};
var createTrack = function( name, attributes ) {
var track = {};
track[name] = attributes;
return track;
};
// Here is where the magic happens
// Split on line breaks
lines = data.text.split( /(?:\r\n|\r|\n)/gm );
len = lines.length;
while ( i < len ) {
sub = {};
text = [];
try {
sub.id = lines[i++];
// Ignore if id contains "-->"
if ( !sub.id || sub.id.indexOf( "-->" ) !== -1 ) {
throw "Bad cue";
}
time = lines[i++].split( /[\t ]*-->[\t ]*/ );
sub.start = toSeconds(time[0]);
// Filter out any trailing styling info
idx = time[1].indexOf( " " );
if ( idx !== -1 ) {
time[1] = time[1].substr( 0, idx );
}
sub.end = toSeconds( time[1] );
// Build single line of text from multi-line subtitle in file
while ( i < len && lines[i] ) {
text.push( lines[i++] );
}
// Join lines together to one and build subtitle
sub.text = text.join( "<br />" );
subs.push( createTrack( "subtitle", sub ) );
} catch ( e ) {
// Bad cue, advance to end of cue
while ( i < len && lines[i] ) {
i++;
}
}
// Consume empty whitespace after a cue
while ( i < len && !lines[i] ) {
i++;
}
}
retObj.data = subs;
return retObj;
});
})( Popcorn );
(function( global, doc ) {
Popcorn.baseplayer = function() {
return new Popcorn.baseplayer.init();
};
Popcorn.baseplayer.init = function() {
this.readyState = 0;
this.currentTime = 0;
this.baselineTime = new Date();
this.duration = 0;
this.paused = 1;
this.ended = 0;
this.volume = 1;
this.muted = 0;
this.playbackRate = 1;
// These are considered to be "on" by being defined. Initialize to undefined
this.autoplay;
this.loop;
// List of events
this._events = {};
// The underlying player resource. May be <canvas>, <iframe>, <object>, array, etc
this._resource;
// The container div of the resource
this._container;
this.offsetWidth = this.width = 0;
this.offsetHeight = this.height = 0;
this.offsetLeft = 0;
this.offsetTop = 0;
this.offsetParent;
};
Popcorn.baseplayer.init.prototype = {
load: function() {},
play: function() {
this.paused = 0;
this.timeupdate();
},
pause: function() {
this.paused = 1;
},
timeupdate: function() {
// So we can refer to the instance when setTimeout is run
var self = this;
if( !this.paused ) {
this.currentTime += ( new Date() - this.baselineTime ) / 1000;
this.dispatchEvent( "timeupdate" );
}
this.baselineTime = new Date();
setTimeout(function() {
self.timeupdate.call( self );
}, 50 );
},
// By default, assumes this.resource is a DOM Element
// Changing the type of this.resource requires this method to be overridden
getBoundingClientRect: function() {
return Popcorn.position( this._resource || this._container );
},
// Add an event listener to the object
addEventListener: function( evtName, fn ) {
if ( !this._events[ evtName ] ) {
this._events[ evtName ] = [];
}
this._events[ evtName ].push( fn );
return fn;
},
// Can take event object or simple string
dispatchEvent: function( oEvent ) {
var evt,
self = this,
eventInterface,
eventName = oEvent.type;
// A string was passed, create event object
if ( !eventName ) {
eventName = oEvent;
eventInterface = Popcorn.events.getInterface( eventName );
if ( eventInterface ) {
evt = document.createEvent( eventInterface );
evt.initEvent( eventName, true, true, window, 1 );
}
}
Popcorn.forEach( this._events[ eventName ], function( val ) {
val.call( self, evt, self );
});
},
// Extracts values from container onto this object
extractContainerValues: function( id ) {
this._container = document.getElementById( id );
if ( !this._container ) {
return;
}
var bounds = this._container.getBoundingClientRect();
this.offsetWidth = this.width = this.getStyle( "width" ) || 0;
this.offsetHeight = this.height = this.getStyle( "height" ) || 0;
this.offsetLeft = bounds.left;
this.offsetTop = bounds.top;
this.offsetParent = this._container.offsetParent;
return this._container;
},
// By default, assumes this.resource is a DOM Element
// Changing the type of this.resource requires this method to be overridden
// Returns the computed value for CSS style 'prop' as computed by the browser
getStyle: function( prop ) {
var elem = this._resource || this._container;
if ( elem.currentStyle ) {
// IE syntax
return elem.currentStyle[ prop ];
} else if ( global.getComputedStyle ) {
// Firefox, Chrome et. al
return doc.defaultView.getComputedStyle( elem, null ).getPropertyValue( prop );
} else {
// Fallback, just in case
return elem.style[ prop ];
}
}
};
})( window, document );
// Popcorn Vimeo Player Wrapper
( function( Popcorn, global ) {
/**
* Vimeo wrapper for Popcorn.
* This player adds enables Popcorn.js to handle Vimeo videos. It does so by masking an embedded Vimeo video Flash object
* as a video and implementing the HTML5 Media Element interface.
*
* You can specify the video in four ways:
* 1. Use the embed code path supplied by Vimeo as a div's src, and pass the div id into a new Popcorn.vimeo object
*
* <div id="player_1" width="500" height="281" src="http://player.vimeo.com/video/11127501" ></div>
* <script type="text/javascript">
* document.addEventListener("DOMContentLoaded", function() {
* var popcorn = Popcorn( Popcorn.vimeo( "player_1" ) );
* }, false);
* </script>
&
* 2. Pass the div id and the embed code path supplied by Vimeo into a new Popcorn.vimeo object
*
* <div id="player_1" width="500" height="281" ></div>
* <script type="text/javascript">
* document.addEventListener("DOMContentLoaded", function() {
* var popcorn = Popcorn( Popcorn.vimeo( "player_1", "http://player.vimeo.com/video/11127501" ) );
* }, false);
* </script>
*
* 3. Use a web url as a div's src, and pass the div id into a new Popcorn.vimeo object
*
* <div id="player_1" width="500" height="281" src="http://vimeo.com/11127501" ></div>
* <script type="text/javascript">
* document.addEventListener("DOMContentLoaded", function() {
* var popcorn = Popcorn( Popcorn.vimeo( "player_1" ) );
* }, false);
* </script>
*
* 4. Pass the div id and the web url into a new Popcorn.vimeo object
*
* <div id="player_1" width="500" height="281" ></div>
* <script type="text/javascript">
* document.addEventListener("DOMContentLoaded", function() {
* var popcorn = Popcorn( Popcorn.vimeo( "player_1", "http://vimeo.com/11127501" ) );
* }, false);
* </script>
*
* Due to Vimeo's API, certain events must be subscribed to at different times, and some not at all.
* These events are completely custom-implemented and may be subscribed to at any time:
* canplaythrough
* durationchange
* load
* loadedmetadata
* loadstart
* play
* readystatechange
* volumechange
*
* These events are related to player functionality and must be subscribed to during or after the load event:
* abort
* emptied
* ended
* pause
* playing
* progress
* seeked
* timeupdate
*
* These events are not supported:
* canplay
* error
* loadeddata
* ratechange
* seeking
* stalled
* suspend
* waiting
*
* Due to Vimeo's API, some attributes are be supported while others are not.
* Supported media attributes:
* autoplay ( via Popcorn )
* currentTime
* duration ( get only )
* ended ( get only )
* initialTime ( get only, always 0 )
* loop ( get only, set by calling setLoop() )
* muted ( get only )
* paused ( get only )
* readyState ( get only )
* volume
*
* load() function
* mute() function ( toggles on/off )
*
* Unsupported media attributes:
* buffered
* defaultPlaybackRate
* networkState
* playbackRate
* played
* preload
* seekable
* seeking
* src
* startOffsetTime
*/
// Trackers
var timeupdateInterval = 33,
timeCheckInterval = 0.75,
abs = Math.abs,
registry = {};
// base object for DOM-related behaviour like events
var EventManager = function ( owner ) {
var evts = {};
function makeHandler( evtName ) {
if ( !evts[evtName] ) {
evts[evtName] = [];
// Create a wrapper function to all registered listeners
this["on"+evtName] = function( args ) {
Popcorn.forEach( evts[evtName], function( fn ) {
if ( fn ) {
fn.call( owner, args );
}
});
};
}
}
return {
addEventListener: function( evtName, fn, doFire ) {
evtName = evtName.toLowerCase();
makeHandler.call( this, evtName );
evts[evtName].push( fn );
if ( doFire ) {
dispatchEvent( evtName );
}
return fn;
},
// Add many listeners for a single event
// Takes an event name and array of functions
addEventListeners: function( evtName, events ) {
evtName = evtName.toLowerCase();
makeHandler.call( this, evtName );
evts[evtName] = evts[evtName].concat( events );
},
removeEventListener: function( evtName, fn ) {
var evtArray = this.getEventListeners( evtName ),
i,
l;
// Find and remove from events array
for ( i = 0, l = evtArray.length; i < l; i++) {
if ( evtArray[i] === fn ) {
var removed = evtArray[i];
evtArray[i] = 0;
return removed;
}
}
},
getEventListeners: function( evtName ) {
if( evtName ) {
return evts[ evtName.toLowerCase() ] || [];
} else {
return evts;
}
},
dispatchEvent: function( evt, args ) {
// If event object was passed in, toString will yield event type as string (timeupdate)
// If a string, toString() will return the string itself (timeupdate)
evt = "on"+evt.toString().toLowerCase();
this[evt] && this[evt]( args );
}
};
};
Popcorn.vimeo = function( mediaId, list, options ) {
return new Popcorn.vimeo.init( mediaId, list, options );
};
Popcorn.vimeo.onLoad = function( playerId ) {
var player = registry[ playerId ];
player.swfObj = document.getElementById( playerId );
// For calculating position relative to video (like subtitles)
player.offsetWidth = player.swfObj.offsetWidth;
player.offsetHeight = player.swfObj.offsetHeight;
player.offsetParent = player.swfObj.offsetParent;
player.offsetLeft = player.swfObj.offsetLeft;
player.offsetTop = player.swfObj.offsetTop;
player.dispatchEvent( "load" );
};
Popcorn.getScript( "http://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js" );
// A constructor, but we need to wrap it to allow for "static" functions
Popcorn.vimeo.init = (function() {
var rPlayerUri = /^http:\/\/player\.vimeo\.com\/video\/[\d]+/i,
rWebUrl = /vimeo\.com\/[\d]+/,
hasAPILoaded = false;
// Extract the numeric video id from container uri: 'http://player.vimeo.com/video/11127501' or 'http://player.vimeo.com/video/4282282'
// Expect id to be a valid 32/64-bit unsigned integer
// Returns string, empty string if could not match
function extractIdFromUri( uri ) {
if ( !uri ) {
return;
}
var matches = uri.match( rPlayerUri );
return matches ? matches[0].substr(30) : "";
}
// Extract the numeric video id from url: 'http://vimeo.com/11127501' or simply 'vimeo.com/4282282'
// Ignores protocol and subdomain, but one would expecct it to be http://www.vimeo.com/#######
// Expect id to be a valid 32/64-bit unsigned integer
// Returns string, empty string if could not match
function extractIdFromUrl( url ) {
if ( !url ) {
return;
}
var matches = url.match( rWebUrl );
return matches ? matches[0].substr(10) : "";
}
function makeSwf( self, vidId, containerId ) {
if ( !window.swfobject ) {
setTimeout( function() {
makeSwf( self, vidId, containerId );
}, 1);
return;
}
var params,
flashvars,
attributes = {};
flashvars = {
clip_id: vidId,
show_portrait: 1,
show_byline: 1,
show_title: 1,
// required in order to use the Javascript API
js_api: 1,
// moogaloop will call this JS function when it's done loading (optional)
js_onLoad: 'Popcorn.vimeo.onLoad',
// this will be passed into all event methods so you can keep track of multiple moogaloops (optional)
js_swf_id: containerId
};
params = {
allowscriptaccess: 'always',
allowfullscreen: 'true',
// This is so we can overlay html ontop o fFlash
wmode: 'transparent'
};
swfobject.embedSWF( "http://vimeo.com/moogaloop.swf", containerId, self.offsetWidth, self.offsetHeight, "9.0.0", "expressInstall.swf", flashvars, params, attributes );
}
// If container id is not supplied, assumed to be same as player id
var ctor = function ( containerId, videoUrl, options ) {
if ( !containerId ) {
throw "Must supply an id!";
} else if ( /file/.test( location.protocol ) ) {
throw "Must run from a web server!";
}
var vidId,
that = this,
tmp;
this._container = document.createElement( "div" );
this._container.id = containerId + "object";
this._target = document.getElementById( containerId );
this._target.appendChild( this._container );
options = options || {};
options.css && Popcorn.extend( this._target.style, options.css );
this.addEventFn;
this.evtHolder;
this.paused = true;
this.duration = Number.MAX_VALUE;
this.ended = 0;
this.currentTime = 0;
this.volume = 1;
this.loop = 0;
this.initialTime = 0;
this.played = 0;
this.readyState = 0;
this.previousCurrentTime = this.currentTime;
this.previousVolume = this.volume;
this.evtHolder = new EventManager( this );
// For calculating position relative to video (like subtitles)
this.width = this._target.style.width || "504px";
this.height = this._target.style.height || "340px";
if ( !/[\d]%/.test( this.width ) ) {
this.offsetWidth = parseInt( this.width, 10 );
this._target.style.width = this.width + "px";
} else {
// convert from pct to abs pixels
tmp = this._target.style.width;
this._target.style.width = this.width;
this.offsetWidth = this._target.offsetWidth;
this._target.style.width = tmp;
}
if ( !/[\d]%/.test( this.height ) ) {
this.offsetHeight = parseInt( this.height, 10 );
this._target.style.height = this.height + "px";
} else {
// convert from pct to abs pixels
tmp = this._target.style.height;
this._target.style.height = this.height;
this.offsetHeight = this._target.offsetHeight;
this._target.style.height = tmp;
}
this.offsetLeft = 0;
this.offsetTop = 0;
// Try and get a video id from a vimeo site url
// Try either from ctor param or from iframe itself
vidId = extractIdFromUrl( videoUrl ) || extractIdFromUri( videoUrl );
if ( !vidId ) {
throw "No video id";
}
registry[ this._container.id ] = this;
makeSwf( this, vidId, this._container.id );
// Set up listeners to internally track state as needed
this.addEventListener( "load", function() {
var hasLoaded = false;
that.duration = that.swfObj.api_getDuration();
that.evtHolder.dispatchEvent( "durationchange" );
that.evtHolder.dispatchEvent( "loadedmetadata" );
// Chain events and calls together so that this.currentTime reflects the current time of the video
// Done by Getting the Current Time while the video plays
that.addEventListener( "timeupdate", function() {
that.currentTime = that.swfObj.api_getCurrentTime();
});
// Add pause listener to keep track of playing state
that.addEventListener( "pause", function() {
that.paused = true;
});
// Add play listener to keep track of playing state
that.addEventListener( "playing", function() {
that.paused = false;
that.ended = 0;
});
// Add ended listener to keep track of playing state
that.addEventListener( "ended", function() {
if ( that.loop !== "loop" ) {
that.paused = true;
that.ended = 1;
}
});
// Add progress listener to keep track of ready state
that.addEventListener( "progress", function( data ) {
if ( !hasLoaded ) {
hasLoaded = 1;
that.readyState = 3;
that.evtHolder.dispatchEvent( "readystatechange" );
}
// Check if fully loaded
if ( data.percent === 100 ) {
that.readyState = 4;
that.evtHolder.dispatchEvent( "readystatechange" );
that.evtHolder.dispatchEvent( "canplaythrough" );
}
});
});
};
return ctor;
})();
Popcorn.vimeo.init.prototype = Popcorn.vimeo.prototype;
// Sequence object prototype
Popcorn.extend( Popcorn.vimeo.prototype, {
// Do everything as functions instead of get/set
setLoop: function( val ) {
if ( !val ) {
return;
}
this.loop = val;
var isLoop = val === "loop" ? 1 : 0;
// HTML convention says to loop if value is 'loop'
this.swfObj.api_setLoop( isLoop );
},
// Set the volume as a value between 0 and 1
setVolume: function( val ) {
if ( !val && val !== 0 ) {
return;
}
// Normalize in case outside range of expected values
if ( val < 0 ) {
val = -val;
}
if ( val > 1 ) {
val %= 1;
}
// HTML video expects to be 0.0 -> 1.0, Vimeo expects 0-100
this.volume = this.previousVolume = val;
this.swfObj.api_setVolume( val*100 );
this.evtHolder.dispatchEvent( "volumechange" );
},
// Seeks the video
setCurrentTime: function ( time ) {
if ( !time && time !== 0 ) {
return;
}
this.currentTime = this.previousCurrentTime = time;
this.ended = time >= this.duration;
this.swfObj.api_seekTo( time );
// Fire events for seeking and time change
this.evtHolder.dispatchEvent( "seeked" );
this.evtHolder.dispatchEvent( "timeupdate" );
},
// Play the video
play: function() {
// In case someone is cheeky enough to try this before loaded
if ( !this.swfObj ) {
this.addEventListener( "load", this.play );
return;
}
if ( !this.played ) {
this.played = 1;
this.startTimeUpdater();
this.evtHolder.dispatchEvent( "loadstart" );
}
this.evtHolder.dispatchEvent( "play" );
this.swfObj.api_play();
},
// Pause the video
pause: function() {
// In case someone is cheeky enough to try this before loaded
if ( !this.swfObj ) {
this.addEventListener( "load", this.pause );
return;
}
this.swfObj.api_pause();
},
// Toggle video muting
// Unmuting will leave it at the old value
mute: function() {
// In case someone is cheeky enough to try this before loaded
if ( !this.swfObj ) {
this.addEventListener( "load", this.mute );
return;
}
if ( !this.muted() ) {
this.oldVol = this.volume;
if ( this.paused ) {
this.setVolume( 0 );
} else {
this.volume = 0;
}
} else {
if ( this.paused ) {
this.setVolume( this.oldVol );
} else {
this.volume = this.oldVol;
}
}
},
muted: function() {
return this.volume === 0;
},
// Force loading by playing the player. Pause afterwards
load: function() {
// In case someone is cheeky enough to try this before loaded
if ( !this.swfObj ) {
this.addEventListener( "load", this.load );
return;
}
this.play();
this.pause();
},
unload: function() {
// In case someone is cheeky enough to try this before loaded
if ( !this.swfObj ) {
this.addEventListener( "load", this.unload );
return;
}
this.pause();
this.swfObj.api_unload();
this.evtHolder.dispatchEvent( "abort" );
this.evtHolder.dispatchEvent( "emptied" );
},
// Hook an event listener for the player event into internal event system
// Stick to HTML conventions of add event listener and keep lowercase, without prependinng "on"
addEventListener: function( evt, fn ) {
var playerEvt,
that = this;
// In case event object is passed in
evt = evt.type || evt.toLowerCase();
// If it's an HTML media event supported by player, map
if ( evt === "seeked" ) {
playerEvt = "onSeek";
} else if ( evt === "timeupdate" ) {
playerEvt = "onProgress";
} else if ( evt === "progress" ) {
playerEvt = "onLoading";
} else if ( evt === "ended" ) {
playerEvt = "onFinish";
} else if ( evt === "playing" ) {
playerEvt = "onPlay";
} else if ( evt === "pause" ) {
// Direct mapping, CamelCase the event name as vimeo API expects
playerEvt = "on"+evt[0].toUpperCase() + evt.substr(1);
}
// Vimeo only stores 1 callback per event
// Have vimeo call internal collection of callbacks
this.evtHolder.addEventListener( evt, fn, false );
// Link manual event structure with Vimeo's if not already
if( playerEvt && this.evtHolder.getEventListeners( evt ).length === 1 ) {
// Setup global functions on Popcorn.vimeo to sync player events to an internal collection
// Some events expect 2 args, some only one (the player id)
if ( playerEvt === "onSeek" || playerEvt === "onProgress" || playerEvt === "onLoading" ) {
Popcorn.vimeo[playerEvt] = function( arg1, arg2 ) {
var player = registry[arg2];
player.evtHolder.dispatchEvent( evt, arg1 );
};
} else {
Popcorn.vimeo[playerEvt] = function( arg1 ) {
var player = registry[arg1];
player.evtHolder.dispatchEvent( evt );
};
}
this.swfObj.api_addEventListener( playerEvt, "Popcorn.vimeo."+playerEvt );
}
},
removeEventListener: function( evtName, fn ) {
return this.evtHolder.removeEventListener( evtName, fn );
},
dispatchEvent: function( evtName ) {
return this.evtHolder.dispatchEvent( evtName );
},
getBoundingClientRect: function() {
return this._target.getBoundingClientRect();
},
startTimeUpdater: function() {
var self = this,
seeked = 0;
if ( abs( this.currentTime - this.previousCurrentTime ) > timeCheckInterval ) {
// Has programatically set the currentTime
this.setCurrentTime( this.currentTime );
seeked = 1;
} else {
this.previousCurrentTime = this.currentTime;
}
if ( this.volume !== this.previousVolume ) {
this.setVolume( this.volume );
}
if ( !self.paused || seeked ) {
this.dispatchEvent( 'timeupdate' );
}
if( !self.ended ) {
setTimeout( function() {
self.startTimeUpdater.call(self);
}, timeupdateInterval);
}
}
});
})( Popcorn, window );
// Popcorn Youtube Player Wrapper
var onYouTubePlayerReady;
( function( Popcorn ) {
/**
* Youtube wrapper for popcorn.
* This plug-in adds capability for Popcorn.js to deal with Youtube
* videos. This plug-in also doesn't use Popcorn's plugin() API and
* instead hacks directly into Popcorn's core.
*
* To use this plug-in, onYouTubePlayerReady() event handler needs to be
* called by the Youtube video player, before videos can be registered.
* Once videos are registered, calls to them can be made the same way as
* regular Popcorn objects. Also note that enablejsapi=1 needs to be added
* to the embed code, in order for Youtube's JavaScript API to work.
*
* Note that there are a few methods, properties and events that are not
* supported. See the bottom of this plug-in for a complete list.
*/
// Intended
var undef;
// Config parameters
// 33 ms per update is suitable for 30 fps
// 0.05 sec tolerance between old and new times to determine if currentTime has been set programatically
// 250 ms progress interval as specified by WHATWG
var timeupdateInterval = 33,
timeCheckInterval = 0.5,
progressInterval = 250;
// Ready State Constants
var READY_STATE_HAVE_NOTHING = 0,
READY_STATE_HAVE_METADATA = 1,
READY_STATE_HAVE_CURRENT_DATA = 2,
READY_STATE_HAVE_FUTURE_DATA = 3,
READY_STATE_HAVE_ENOUGH_DATA = 4;
// Youtube State Constants
var YOUTUBE_STATE_UNSTARTED = -1,
YOUTUBE_STATE_ENDED = 0,
YOUTUBE_STATE_PLAYING = 1,
YOUTUBE_STATE_PAUSED = 2,
YOUTUBE_STATE_BUFFERING = 3,
YOUTUBE_STATE_CUED = 5;
var urlRegex = /^.*[\/=](.{11})/;
// Collection of all Youtube players
var registry = {},
loadedPlayers = {};
var abs = Math.abs;
Popcorn.getScript( "http://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js" );
// Extract the id from a web url
function extractIdFromUrl( url ) {
if ( !url ) {
return;
}
var matches = urlRegex.exec( url );
// Return id, which comes after first equals sign
return matches ? matches[1] : "";
}
// Extract the id from a player url
function extractIdFromUri( url ) {
if ( !url ) {
return;
}
var matches = urlRegex.exec( url );
// Return id, which comes after first equals sign
return matches ? matches[1] : "";
}
function getPlayerAddress( vidId, playerId ) {
if( !vidId ) {
return;
}
return "http://www.youtube.com/e/" + id;
}
function makeSWF( url, container ) {
var bounds,
params,
flashvars,
attributes,
self = this;
if ( !window.swfobject ) {
setTimeout( function() {
makeSWF.call( self, url, container );
}, 1 );
return;
}
bounds = container.getBoundingClientRect();
// Determine width/height/etc based on container
this.width = container.style.width || 460;
this.height = container.style.height || 350;
// Just in case we got the attributes as strings. We'll need to do math with these later
this.width = parseFloat(this.width);
this.height = parseFloat(this.height);
// For calculating position relative to video (like subtitles)
this.offsetWidth = this.width;
this.offsetHeight = this.height;
this.offsetLeft = bounds.left;
this.offsetTop = bounds.top;
this.offsetParent = container.offsetParent;
flashvars = {
playerapiid: this.playerId,
controls: this.controls,
iv_load_policy: this.iv_load_policy
};
params = {
allowscriptaccess: 'always',
allowfullscreen: 'true',
// This is so we can overlay html on top of Flash
wmode: 'transparent'
};
attributes = {
id: this.playerId
};
swfobject.embedSWF( "http://www.youtube.com/e/" + this.vidId +"?enablejsapi=1&playerapiid=" + this.playerId + "&version=3",
this.playerId, this.width, this.height, "8", null, flashvars, params, attributes );
}
// Called when a player is loaded
// Playerid must match the element id
onYouTubePlayerReady = function ( playerId ) {
var vid = registry[playerId];
loadedPlayers[playerId] = 1;
// Video hadn't loaded yet when ctor was called
vid.video = document.getElementById( playerId );
vid.duration = vid.video.getDuration();
// Issue load event
vid.dispatchEvent( 'load' );
vid.dispatchEvent( "durationchange" );
};
Popcorn.youtube = function( elementId, url, options ) {
return new Popcorn.youtube.init( elementId, url, options );
};
Popcorn.youtube.init = function( elementId, url, options ) {
if ( !elementId ) {
throw "Element id is invalid.";
} else if ( /file/.test( location.protocol ) ) {
throw "This must be run from a web server.";
}
options = options || {};
var self = this;
this.playerId = elementId + Popcorn.guid();
this.readyState = READY_STATE_HAVE_NOTHING;
this._eventListeners = {};
this.loadStarted = false;
this.loadedData = false;
this.fullyLoaded = false;
this.paused = false;
// If supplied as number, append 'px' on end
// If suppliied as '###' or '###px', convert to number and append 'px' back on end
options.width = options.width && (+options.width)+"px";
options.height = options.height && (+options.height)+"px";
// show controls on video. Integer value - 1 is for show, 0 is for hide
this.controls = +options.controls === 0 || +options.controls === 1 ? options.controls : 1;
// show video annotations, 1 is show, 3 is hide
this.iv_load_policy = +options.annotations === 1 || +options.annotations === 3 ? options.annotations : 1;
this._target = document.getElementById( elementId );
this._container = document.createElement( "div" );
this._container.id = this.playerId;
this._container.style.height = this._target.style.height = options.height || this._target.style.height || "350px";
this._container.style.width = this._target.style.width = options.width || this._target.style.width || "460px";
this._target.appendChild( this._container );
this.offsetHeight = +this._target.offsetHeight;
this.offsetWidth = +this._target.offsetWidth;
this.currentTime = this.previousCurrentTime = 0;
this.volume = this.previousVolume = this.preMuteVol = 1;
this.duration = 0;
this.vidId = extractIdFromUrl( url ) || extractIdFromUri( url );
if ( !this.vidId ) {
throw "Could not find video id";
}
this.addEventListener( "load", function() {
// For calculating position relative to video (like subtitles)
this.offsetWidth = this.video.offsetWidth;
this.offsetHeight = this.video.offsetHeight;
this.offsetParent = this.video.offsetParent;
// Set up stuff that requires the API to be loaded
this.registerYoutubeEventHandlers();
this.registerInternalEventHandlers();
});
(function() {
var hasBeenCalled = 0;
self.addEventListener( "playing", function() {
if (hasBeenCalled) {
return;
}
hasBeenCalled = 1;
self.duration = self.video.getDuration();
self.dispatchEvent( "durationchange" );
});
})();
if ( loadedPlayers[this.playerId] ) {
this.video = registry[this.playerId].video;
this.vidId = this.vidId || extractIdFromUrl( this._container.getAttribute( "src" ) ) || extractIdFromUri( this._container.getAttribute( "src" ) );
if (this.vidId !== registry[this.playerId].vidId ) {
this.video.cueVideoById( this.vidId );
} else {
// Same video, new ctor. Force a seek to the beginning
this.previousCurrentTime = 1;
}
this.dispatchEvent( 'load' );
} else if ( this._container ) {
makeSWF.call( this, url, this._container );
} else {
// Container not yet loaded, get it on DOMDontentLoad
document.addEventListener( "DOMContentLoaded", function() {
self._container = document.getElementById( elementId );
if ( !self._container ) {
throw "Could not find container!";
}
makeSWF.call( self, url, self._container );
}, false);
}
registry[this.playerId] = this;
};
// end Popcorn.youtube.init
Popcorn.extend( Popcorn.youtube.init.prototype, {
// For internal use only.
// Register handlers to YouTube events.
registerYoutubeEventHandlers: function() {
var youcorn = this,
stateChangeHandler = 'Popcorn.youtube.stateChangeEventHandler',
errorHandler = 'Popcorn.youtube.errorEventHandler';
this.video.addEventListener( 'onStateChange', stateChangeHandler );
this.video.addEventListener( 'onError', errorHandler );
/**
* Since Flash can only call named functions, they are declared
* separately here.
*/
Popcorn.youtube.stateChangeEventHandler = function( state ) {
// In case ctor has been called many times for many ctors
// Only use latest ctor call for each player id
var self = registry[youcorn.playerId];
if ( state === YOUTUBE_STATE_UNSTARTED ) {
self.readyState = READY_STATE_HAVE_METADATA;
self.dispatchEvent( 'loadedmetadata' );
} else if ( state === YOUTUBE_STATE_ENDED ) {
self.dispatchEvent( 'ended' );
} else if ( state === YOUTUBE_STATE_PLAYING ) {
// Being able to play means current data is loaded.
if ( !this.loadedData ) {
this.loadedData = true;
self.dispatchEvent( 'loadeddata' );
}
self.readyState = READY_STATE_HAVE_CURRENT_DATA;
self.dispatchEvent( 'playing' );
} else if ( state === YOUTUBE_STATE_PAUSED ) {
self.dispatchEvent( 'pause' );
} else if ( state === YOUTUBE_STATE_BUFFERING ) {
self.dispatchEvent( 'waiting' );
} else if ( state === YOUTUBE_STATE_CUED ) {
// not handled
}
};
Popcorn.youtube.errorEventHandler = function( state ) {
youcorn.dispatchEvent( 'error' );
};
},
// For internal use only.
// Start current time and loading progress syncing intervals.
registerInternalEventHandlers: function() {
this.addEventListener( 'playing', function() {
this.startTimeUpdater();
});
this.addEventListener( 'loadedmetadata', function() {
this.startProgressUpdater();
});
},
play: function() {
// In case called before video is loaded, defer acting
if ( !loadedPlayers[this.playerId] ) {
this.addEventListener( "load", function() {
this.play();
});
return;
}
this.dispatchEvent( 'play' );
this.video.playVideo();
},
pause: function() {
// In case called before video is loaded, defer acting
if ( !loadedPlayers[this.playerId] ) {
this.addEventListener( "load", this.pause );
return;
}
this.video.pauseVideo();
// pause event is raised by Youtube.
},
load: function() {
// In case called before video is loaded, defer acting
if ( !loadedPlayers[this.playerId] ) {
this.addEventListener( "load", function() {
this.load();
});
return;
}
this.video.playVideo();
this.video.pauseVideo();
},
seekTo: function( time ) {
var playing = this.video.getPlayerState() == YOUTUBE_STATE_PLAYING;
this.video.seekTo( time, true );
// Prevent Youtube's behaviour to start playing video after seeking.
if ( !playing ) {
this.video.paused = true;
this.video.pauseVideo();
} else {
this.video.paused = false;
}
// Data need to be loaded again.
if ( !this.fullyLoaded ) {
this.loadedData = false;
}
// Raise event.
this.dispatchEvent( 'seeked' );
},
// Mute is toggleable
mute: function() {
// In case called before video is loaded, defer acting
if ( !loadedPlayers[this.playerId] ) {
this.addEventListener( "load", this.mute );
return;
}
if ( this.volume !== 0 ) {
this.preMuteVol = this.volume;
this.setVolume( 0 );
} else {
this.setVolume( this.preMuteVol );
}
},
// Expects beteween 0 and 1
setVolume: function( vol ) {
this.volume = this.previousVolume = vol;
this.video.setVolume( vol * 100 );
this.dispatchEvent( 'volumechange' );
},
addEventListener: function( evt, func ) {
var evtName = evt.type || evt;
if ( !this._eventListeners[evtName] ) {
this._eventListeners[evtName] = [];
}
this._eventListeners[evtName].push( func );
},
/**
* Notify event listeners about an event.
*/
dispatchEvent: function( name ) {
var evtName = name.type || name;
if ( !this._eventListeners[evtName] ) {
return;
}
var self = this;
Popcorn.forEach( this._eventListeners[evtName], function( evt ) {
evt.call( self, null );
});
},
/* Unsupported methods. */
defaultPlaybackRate: function( arg ) {
},
playbackRate: function( arg ) {
},
getBoundingClientRect: function() {
var b,
self = this;
if ( this.video ) {
b = this.video.getBoundingClientRect();
return {
bottom: b.bottom,
left: b.left,
right: b.right,
top: b.top,
// These not guaranteed to be in there
width: b.width || ( b.right - b.left ),
height: b.height || ( b.bottom - b.top )
};
} else {
b = self._container.getBoundingClientRect();
// Update bottom, right for expected values once the container loads
return {
left: b.left,
top: b.top,
width: self._target.offsetWidth,
height: self._target.offsetHeight,
bottom: b.top + self._target.offsetWidth,
right: b.left + self._target.offsetHeight
};
}
},
startTimeUpdater: function() {
var state = typeof this.video.getPlayerState != "function" ? this.readyState : this.video.getPlayerState(),
self = this,
seeked = 0;
if ( abs( this.currentTime - this.previousCurrentTime ) > timeCheckInterval ) {
// Has programatically set the currentTime
this.previousCurrentTime = this.currentTime - timeCheckInterval;
this.seekTo( this.currentTime );
seeked = 1;
} else {
this.previousCurrentTime = this.currentTime;
this.currentTime = typeof this.video.getCurrentTime != "function" ? this.currentTime : this.video.getCurrentTime();
}
if ( this.volume !== this.previousVolume ) {
this.setVolume( this.volume );
}
if ( state !== YOUTUBE_STATE_ENDED && state !== YOUTUBE_STATE_PAUSED || seeked ) {
this.dispatchEvent( 'timeupdate' );
}
if( state !== YOUTUBE_STATE_ENDED ) {
setTimeout( function() {
self.startTimeUpdater.call(self);
}, timeupdateInterval);
}
},
startProgressUpdater: function() {
var bytesLoaded = this.video.getVideoBytesLoaded(),
bytesToLoad = this.video.getVideoBytesTotal(),
self = this;
// do nothing if size is not yet determined
if ( bytesToLoad === 0 ) {
return;
}
// raise an event if load has just started
if ( !this.loadStarted ) {
this.loadStarted = true;
this.dispatchEvent( 'loadstart' );
}
// fully loaded
if ( bytesLoaded >= bytesToLoad ) {
this.fullyLoaded = true;
this.readyState = READY_STATE_HAVE_ENOUGH_DATA;
this.dispatchEvent( 'canplaythrough' );
return;
}
this.dispatchEvent( 'progress' );
setTimeout( function() {
self.startProgressUpdater.call( self );
}, progressInterval);
}
}); // end Popcorn.extend
/* Unsupported properties and events. */
/**
* Unsupported events are:
* * suspend
* * abort
* * emptied
* * stalled
* * canplay
* * seeking
* * ratechange
*/
})( Popcorn );
// Popcorn Soundcloud Player Wrapper
( function( Popcorn, global ) {
/**
* Soundcloud wrapper for Popcorn.
* This player adds enables Popcorn.js to handle Soundcloud audio. It does so by masking an embedded Soundcloud Flash object
* as a video and implementing the HTML5 Media Element interface.
*
* You can configure the video source and dimensions in two ways:
* 1. Use the embed code path supplied by Soundcloud the id of the desired location into a new Popcorn.soundcloud object.
* Width and height can be configured throughh CSS.
*
* <div id="player_1" style="width: 500px; height: 81px"></div>
* <script type="text/javascript">
* document.addEventListener("DOMContentLoaded", function() {
* var popcorn = Popcorn( Popcorn.soundcloud( "player_1", "http://soundcloud.com/forss/flickermood" ));
* }, false);
* </script>
*
* 2. Width and height may also be configured directly with the player; this will override any CSS. This is useful for
* when different sizes are desired. for multiple players within the same parent container.
*
* <div id="player_1"></div>
* <script type="text/javascript">
* document.addEventListener("DOMContentLoaded", function() {
* var popcorn = Popcorn( Popcorn.soundcloud( "player_1", "http://soundcloud.com/forss/flickermood", {
* width: "500", // Optional, will default to CSS values
* height: "81" // Optional, will default to CSS values
* }));
* }, false);
* </script>
*
* The player can be further configured to integrate with the SoundCloud API:
*
* var popcorn = Popcorn( Popcorn.soundcloud( "player_1", "http://soundcloud.com/forss/flickermood", {
* width: "100%", // Optional, the width for the player. May also be as '##px'
* // Defaults to the maximum possible width
* height: "81px", // Optional, the height for the player. May also be as '###%'
* // Defaults to 81px
* api: { // Optional, information for Soundcloud API interaction
* key: "abcdefsdfsdf", // Required for API interaction. The Soundcloud API key
* commentdiv: "divId_for_output", // Required for comment retrieval, the Div Id for outputting comments.
* commentformat: function( comment ) {} // Optional, a function to format a comment. Returns HTML string
* }
* }));
*
* Comments are retrieved from Soundcloud when the player is registered with Popcorn by calling the registerWithPopcorn()
* function. For this to work, the api_key and commentdiv attributes must be set. Comments are output by default similar to
* how Soundcloud formats them in-player, but a custom formatting function may be supplied. It receives a comment object and
* the current date. A comment object has:
*
* var comment = {
* start: 0, // Required. Start time in ms.
* date: new Date(), // Required. Date comment wasa posted.
* text: "", // Required. Comment text
* user: { // Required. Describes the user who posted the comment
* name: "", // Required. User name
* profile: "", // Required. User profile link
* avatar: "" // Required. User avatar link
* }
* }
*
* These events are completely custom-implemented and may be subscribed to at any time:
* canplaythrough
* durationchange
* load
* loadedmetadata
* loadstart
* play
* readystatechange
* volumechange
*
* These events are related to player functionality and must be subscribed to during or after the load event:
* canplay
* ended
* error
* pause
* playing
* progress
* seeked
* timeupdate
*
* These events are not supported:
* abort
* emptied
* loadeddata
* ratechange
* seeking
* stalled
* suspend
* waiting
*
* Supported media attributes:
* autoplay ( via Popcorn )
* currentTime
* defaultPlaybackRate ( get only )
* duration ( get only )
* ended ( get only )
* initialTime ( get only, always 0 )
* loop ( get only, set by calling setLoop() )
* muted ( get only )
* paused ( get only )
* playbackRate ( get only )
* played ( get only, 0/1 only )
* readyState ( get only )
* src ( get only )
* volume
*
* load() function
* mute() function ( toggles on/off )
* play() function
* pause() function
*
* Unsupported media attributes:
* buffered
* networkState
* preload
* seekable
* seeking
* startOffsetTime
*
* canPlayType() function
*/
// Trackers
var timeupdateInterval = 33,
timeCheckInterval = 0.25,
abs = Math.abs,
floor = Math.floor,
round = Math.round,
registry = {};
function hasAllDependencies() {
return global.swfobject && global.soundcloud;
}
// Borrowed from: http://www.quirksmode.org/dom/getstyles.html
// Gets the style for the given element
function getStyle( elem, styleProp ) {
if ( elem.currentStyle ) {
// IE way
return elem.currentStyle[styleProp];
} else if ( global.getComputedStyle ) {
// Firefox, Chrome, et. al
return document.defaultView.getComputedStyle( elem, null ).getPropertyValue( styleProp );
}
}
function formatComment( comment ) {
// Calclate the difference between d and now, express as "n units ago"
function ago( d ) {
var diff = ( ( new Date() ).getTime() - d.getTime() )/1000;
function pluralize( value, unit ) {
return value + " " + unit + ( value > 1 ? "s" : "") + " ago";
}
if ( diff < 60 ) {
return pluralize( round( diff ), "second" );
}
diff /= 60;
if ( diff < 60 ) {
return pluralize( round( diff ), "minute" );
}
diff /= 60;
if ( diff < 24 ) {
return pluralize( round( diff ), "hour" );
}
diff /= 24;
// Rough approximation of months
if ( diff < 30 ) {
return pluralize( round( diff ), "day" );
}
if ( diff < 365 ) {
return pluralize( round( diff/30 ), "month" );
}
return pluralize( round( diff/365 ), "year" );
}
// Converts sec to min.sec
function timeToFraction ( totalSec ) {
var min = floor( totalSec / 60 ),
sec = round( totalSec % 60 );
return min + "." + ( sec < 10 ? "0" : "" ) + sec;
}
return '<div><a href="' + comment.user.profile + '">' +
'<img width="16px height="16px" src="' + comment.user.avatar + '"></img>' +
comment.user.name + '</a> at ' + timeToFraction( comment.start ) + ' ' +
ago( comment.date ) +
'<br />' + comment.text + '</span>';
}
function isReady( self ) {
if ( !hasAllDependencies() ) {
setTimeout( function() {
isReady( self );
}, 15 );
return;
}
var flashvars = {
enable_api: true,
object_id: self._playerId,
url: self.src,
// Hide comments in player if showing them elsewhere
show_comments: !self._options.api.key && !self._options.api.commentdiv
},
params = {
allowscriptaccess: "always",
// This is so we can overlay html ontop of Flash
wmode: 'transparent'
},
attributes = {
id: self._playerId,
name: self._playerId
},
actualTarget = document.createElement( 'div' );
actualTarget.setAttribute( "id", self._playerId );
self._container.appendChild( actualTarget );
swfobject.embedSWF( "http://player.soundcloud.com/player.swf", self._playerId, self.offsetWidth, self.height, "9.0.0", "expressInstall.swf", flashvars, params, attributes );
}
Popcorn.getScript( "http://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js" );
// Source file originally from 'https://github.com/soundcloud/Widget-JS-API/raw/master/soundcloud.player.api.js'
Popcorn.getScript( "http://popcornjs.org/code/players/soundcloud/lib/soundcloud.player.api.js", function() {
// Play event is fired twice when player is first started. Ignore second one
var ignorePlayEvt = 1;
// Register the wrapper's load event with the player
soundcloud.addEventListener( 'onPlayerReady', function( object, data ) {
var wrapper = registry[object.api_getFlashId()];
wrapper.swfObj = object;
wrapper.duration = object.api_getTrackDuration();
wrapper.currentTime = object.api_getTrackPosition();
// This eliminates volumechangee event from firing on load
wrapper.volume = wrapper.previousVolume = object.api_getVolume()/100;
// The numeric id of the track for use with Soundcloud API
wrapper._mediaId = data.mediaId;
wrapper.dispatchEvent( 'load' );
wrapper.dispatchEvent( 'canplay' );
wrapper.dispatchEvent( 'durationchange' );
wrapper.timeupdate();
});
// Register events for when the flash player plays a track for the first time
soundcloud.addEventListener( 'onMediaStart', function( object, data ) {
var wrapper = registry[object.api_getFlashId()];
wrapper.played = 1;
wrapper.dispatchEvent( 'playing' );
});
// Register events for when the flash player plays a track
soundcloud.addEventListener( 'onMediaPlay', function( object, data ) {
if ( ignorePlayEvt ) {
ignorePlayEvt = 0;
return;
}
var wrapper = registry[object.api_getFlashId()];
wrapper.dispatchEvent( 'play' );
});
// Register events for when the flash player pauses a track
soundcloud.addEventListener( 'onMediaPause', function( object, data ) {
var wrapper = registry[object.api_getFlashId()];
wrapper.dispatchEvent( 'pause' );
});
// Register events for when the flash player is buffering
soundcloud.addEventListener( 'onMediaBuffering', function( object, data ) {
var wrapper = registry[object.api_getFlashId()];
wrapper.dispatchEvent( 'progress' );
if ( wrapper.readyState === 0 ) {
wrapper.readyState = 3;
wrapper.dispatchEvent( "readystatechange" );
}
});
// Register events for when the flash player is done buffering
soundcloud.addEventListener( 'onMediaDoneBuffering', function( object, data ) {
var wrapper = registry[object.api_getFlashId()];
wrapper.dispatchEvent( 'canplaythrough' );
});
// Register events for when the flash player has finished playing
soundcloud.addEventListener( 'onMediaEnd', function( object, data ) {
var wrapper = registry[object.api_getFlashId()];
wrapper.paused = 1;
//wrapper.pause();
wrapper.dispatchEvent( 'ended' );
});
// Register events for when the flash player has seeked
soundcloud.addEventListener( 'onMediaSeek', function( object, data ) {
var wrapper = registry[object.api_getFlashId()];
wrapper.setCurrentTime( object.api_getTrackPosition() );
if ( wrapper.paused ) {
wrapper.dispatchEvent( "timeupdate" );
}
});
// Register events for when the flash player has errored
soundcloud.addEventListener( 'onPlayerError', function( object, data ) {
var wrapper = registry[object.api_getFlashId()];
wrapper.dispatchEvent( 'error' );
});
});
Popcorn.soundcloud = function( containerId, src, options ) {
return new Popcorn.soundcloud.init( containerId, src, options );
};
// A constructor, but we need to wrap it to allow for "static" functions
Popcorn.soundcloud.init = (function() {
function pullFromContainer( that ) {
var options = that._options,
container = that._container,
bounds = container.getBoundingClientRect(),
tmp,
undef;
that.width = options.width || getStyle( container, "width" ) || "100%";
that.height = options.height || getStyle( container, "height" ) || "81px";
that.src = options.src;
that.autoplay = options.autoplay;
if ( parseFloat( that.height, 10 ) !== 81 ) {
that.height = "81px";
}
that.offsetLeft = bounds.left;
that.offsetTop = bounds.top;
that.offsetHeight = parseFloat( that.height, 10 );
that.offsetWidth = parseFloat( that.width, 10 );
// Width and height may've been specified as a %, find the value now in case a plugin needs it (like subtitle)
if ( /[\d]+%/.test( that.width ) ) {
tmp = getStyle( container, "width" );
that._container.style.width = that.width;
that.offsetWidth = that._container.offsetWidth;
that._container.style.width = tmp;
}
if ( /[\d]+%/.test( that.height ) ) {
tmp = getStyle( container, "height" );
that._container.style.height = that.height;
that.offsetHeight = that._container.offsetHeight;
that._container.style.height = tmp;
}
}
// If container id is not supplied, assumed to be same as player id
var ctor = function ( containerId, src, options ) {
if ( !containerId ) {
throw "Must supply an id!";
} else if ( !src ) {
throw "Must supply a source!";
} else if ( /file/.test( location.protocol ) ) {
throw "Must run from a web server!";
}
var container = this._container = document.getElementById( containerId );
if ( !container ) {
throw "Could not find that container in the DOM!";
}
options = options || {};
options.api = options.api || {};
options.target = containerId;
options.src = src;
options.api.commentformat = options.api.commentformat || formatComment;
this._mediaId = 0;
this._listeners = {};
this._playerId = Popcorn.guid( options.target );
this._containerId = options.target;
this._options = options;
this._comments = [];
this._popcorn;
pullFromContainer( this );
this.duration = 0;
this.volume = 1;
this.currentTime = 0;
this.ended = 0;
this.paused = 1;
this.readyState = 0;
this.playbackRate = 1;
this.top = 0;
this.left = 0;
this.autoplay;
this.played = 0;
this.addEventListener( "load", function() {
var boundRect = this.getBoundingClientRect();
this.top = boundRect.top;
this.left = boundRect.left;
this.offsetWidth = this.swfObj.offsetWidth;
this.offsetHeight = this.swfObj.offsetHeight;
this.offsetLeft = this.swfObj.offsetLeft;
this.offsetTop = this.swfObj.offsetTop;
});
registry[ this._playerId ] = this;
isReady( this );
};
return ctor;
})();
Popcorn.soundcloud.init.prototype = Popcorn.soundcloud.prototype;
// Sequence object prototype
Popcorn.extend( Popcorn.soundcloud.prototype, {
// Set the volume as a value between 0 and 1
setVolume: function( val ) {
if ( !val && val !== 0 ) {
return;
}
// Normalize in case outside range of expected values of 0 .. 1
if ( val < 0 ) {
val = -val;
}
if ( val > 1 ) {
val %= 1;
}
// HTML video expects to be 0.0 -> 1.0, Flash object expects 0-100
this.volume = this.previousVolume = val;
this.swfObj.api_setVolume( val*100 );
this.dispatchEvent( "volumechange" );
},
// Seeks the video
setCurrentTime: function ( time ) {
if ( !time && time !== 0 ) {
return;
}
this.currentTime = this.previousCurrentTime = time;
this.ended = time >= this.duration;
// Fire events for seeking and time change
this.dispatchEvent( "seeked" );
},
// Play the video
play: function() {
// In case someone is cheeky enough to try this before loaded
if ( !this.swfObj ) {
this.addEventListener( "load", this.play );
return;
} else if ( !this.paused ) {
// No need to process if already playing
return;
}
this.paused = 0;
this.swfObj.api_play();
},
// Pause the video
pause: function() {
// In case someone is cheeky enough to try this before loaded
if ( !this.swfObj ) {
this.addEventListener( "load", this.pause );
return;
} else if ( this.paused ) {
// No need to process if already playing
return;
}
this.paused = 1;
this.swfObj.api_pause();
},
// Toggle video muting
// Unmuting will leave it at the old value
mute: function() {
// In case someone is cheeky enough to try this before loaded
if ( !this.swfObj ) {
this.addEventListener( "load", this.mute );
return;
}
if ( !this.muted() ) {
this.oldVol = this.volume;
if ( this.paused ) {
this.setVolume( 0 );
} else {
this.volume = 0;
}
} else {
if ( this.paused ) {
this.setVolume( this.oldVol );
} else {
this.volume = this.oldVol;
}
}
},
muted: function() {
return this.volume === 0;
},
// Force loading by playing the player. Pause afterwards
load: function() {
// In case someone is cheeky enough to try this before loaded
if ( !this.swfObj ) {
this.addEventListener( "load", this.load );
return;
}
this.play();
this.pause();
},
// Hook an event listener for the player event into internal event system
// Stick to HTML conventions of add event listener and keep lowercase, without prepending "on"
addEventListener: function( evt, fn ) {
if ( !this._listeners[evt] ) {
this._listeners[evt] = [];
}
this._listeners[evt].push( fn );
return fn;
},
dispatchEvent: function( evt ) {
var self = this,
evtName = evt.type || evt;
// Manually triggered a UI event, have it invoke rather than just the event handlers
if ( evtName === "play" && this.paused || evtName === "pause" && !this.paused ) {
this[evtName]();
return;
}
Popcorn.forEach( this._listeners[evtName], function( fn ) {
fn.call( self );
});
},
timeupdate: function() {
var self = this,
checkedVolume = this.swfObj.api_getVolume()/100,
seeked = 0;
// If has been changed through setting currentTime attribute
if ( abs( this.currentTime - this.previousCurrentTime ) > timeCheckInterval ) {
// Has programatically set the currentTime
this.swfObj.api_seekTo( this.currentTime );
seeked = 1;
} else {
this.previousCurrentTime = this.currentTime = this.swfObj.api_getTrackPosition();
}
// If has been changed throughh volume attribute
if ( checkedVolume !== this.previousVolume ) {
this.setVolume( checkedVolume );
} else if ( this.volume !== this.previousVolume ) {
this.setVolume( this.volume );
}
if ( !this.paused ) {
this.dispatchEvent( 'timeupdate' );
}
if( !self.ended ) {
setTimeout( function() {
self.timeupdate.call( self );
}, timeupdateInterval);
}
},
getBoundingClientRect: function() {
var b,
self = this;
if ( this.swfObj ) {
b = this.swfObj.getBoundingClientRect();
return {
bottom: b.bottom,
left: b.left,
right: b.right,
top: b.top,
// These not guaranteed to be in there
width: b.width || ( b.right - b.left ),
height: b.height || ( b.bottom - b.top )
};
} else {
//container = document.getElementById( this.playerId );
tmp = this._container.getBoundingClientRect();
// Update bottom, right for expected values once the container loads
return {
left: tmp.left,
top: tmp.top,
width: self.offsetWidth,
height: self.offsetHeight,
bottom: tmp.top + this.width,
right: tmp.top + this.height
};
}
},
registerPopcornWithPlayer: function( popcorn ) {
if ( !this.swfObj ) {
this.addEventListener( "load", function() {
this.registerPopcornWithPlayer( popcorn );
});
return;
}
this._popcorn = popcorn;
var api = this._options.api;
if ( api.key && api.commentdiv ) {
var self = this;
Popcorn.xhr({
url: "http://api.soundcloud.com/tracks/" + self._mediaId + "/comments.js?consumer_key=" + api.key,
success: function( data ) {
Popcorn.forEach( data.json, function ( obj ) {
self.addComment({
start: obj.timestamp/1000,
date: new Date( obj.created_at ),
text: obj.body,
user: {
name: obj.user.username,
profile: obj.user.permalink_url,
avatar: obj.user.avatar_url
}
});
});
}
});
}
}
});
Popcorn.extend( Popcorn.soundcloud.prototype, {
addComment: function( obj, displayFn ) {
var self = this,
comment = {
start: obj.start || 0,
date: obj.date || new Date(),
text: obj.text || "",
user: {
name: obj.user.name || "",
profile: obj.user.profile || "",
avatar: obj.user.avatar || ""
},
display: function() {
return ( displayFn || self._options.api.commentformat )( comment );
}
};
this._comments.push( comment );
if ( !this._popcorn ) {
return;
}
this._popcorn.subtitle({
start: comment.start,
target: this._options.api.commentdiv,
display: 'inline',
language: 'en',
text: comment.display()
});
}
});
})( Popcorn, window );// EFFECT: applyclass
(function (Popcorn) {
/**
* apply css class to jquery selector
* selector is relative to plugin target's id
* so .overlay is actually jQuery( "#target .overlay")
*
* @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',
effect: 'applyclass',
applyclass: 'selector: class'
})
*
*/
var toggleClass = function( event, options ) {
var idx = 0, len = 0, elements;
Popcorn.forEach( options.classes, function( key, val ) {
elements = [];
if ( key === "parent" ) {
elements[ 0 ] = document.querySelectorAll("#" + options.target )[ 0 ].parentNode;
} else {
elements = document.querySelectorAll("#" + options.target + " " + key );
}
for ( idx = 0, len = elements.length; idx < len; idx++ ) {
elements[ idx ].classList.toggle( val );
}
});
};
Popcorn.compose( "applyclass", {
manifest: {
about: {
name: "Popcorn applyclass Effect",
version: "0.1",
author: "@scottdowne",
website: "scottdowne.wordpress.com"
},
options: {}
},
_setup: function( options ) {
options.classes = {};
options.applyclass = options.applyclass || "";
var classes = options.applyclass.replace( /\s/g, "" ).split( "," ),
item = [],
idx = 0, len = classes.length;
for ( ; idx < len; idx++ ) {
item = classes[ idx ].split( ":" );
if ( item[ 0 ] ) {
options.classes[ item[ 0 ] ] = item[ 1 ] || "";
}
}
},
start: toggleClass,
end: toggleClass
});
})( Popcorn );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment