/*
* popcorn.js version 1.3
* http://popcornjs.org
*
* Copyright 2011, Mozilla Foundation
* Licensed under the MIT license
*/
(function(global, document) {
// Popcorn.js does not support archaic browsers
if ( !document.addEventListener ) {
global.Popcorn = {
isSupported: false
};
var methods = ( "byId forEach extend effects error guid sizeOf isArray nop position disable enable destroy" +
"addTrackEvent removeTrackEvent getTrackEvents getTrackEvent getLastTrackEventId " +
"timeUpdate plugin removePlugin compose effect xhr getJSONP getScript" ).split(/\s+/);
while ( methods.length ) {
global.Popcorn[ methods.shift() ] = function() {};
}
return;
}
var
AP = Array.prototype,
OP = Object.prototype,
forEach = AP.forEach,
slice = AP.slice,
hasOwn = OP.hasOwnProperty,
toString = OP.toString,
// Copy global Popcorn (may not exist)
_Popcorn = global.Popcorn,
// Ready fn cache
readyStack = [],
readyBound = false,
readyFired = false,
// Non-public internal data object
internal = {
events: {
hash: {},
apis: {}
}
},
// Non-public `requestAnimFrame`
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
requestAnimFrame = (function(){
return global.requestAnimationFrame ||
global.webkitRequestAnimationFrame ||
global.mozRequestAnimationFrame ||
global.oRequestAnimationFrame ||
global.msRequestAnimationFrame ||
function( callback, element ) {
global.setTimeout( callback, 16 );
};
}()),
// Non-public `getKeys`, return an object's keys as an array
getKeys = function( obj ) {
return Object.keys ? Object.keys( obj ) : (function( obj ) {
var item,
list = [];
for ( item in obj ) {
if ( hasOwn.call( obj, item ) ) {
list.push( item );
}
}
return list;
})( obj );
},
// Declare constructor
// Returns an instance object.
Popcorn = function( entity, options ) {
// Return new Popcorn object
return new Popcorn.p.init( entity, options || null );
};
// Popcorn API version, automatically inserted via build system.
Popcorn.version = "1.3";
// Boolean flag allowing a client to determine if Popcorn can be supported
Popcorn.isSupported = true;
// Instance caching
Popcorn.instances = [];
// Declare a shortcut (Popcorn.p) to and a definition of
// the new prototype for our Popcorn constructor
Popcorn.p = Popcorn.prototype = {
init: function( entity, options ) {
var matches, nodeName,
self = this;
// Supports Popcorn(function () { /../ })
// Originally proposed by Daniel Brooks
if ( typeof entity === "function" ) {
// If document ready has already fired
if ( document.readyState === "complete" ) {
entity( document, Popcorn );
return;
}
// Add `entity` fn to ready stack
readyStack.push( entity );
// This process should happen once per page load
if ( !readyBound ) {
// set readyBound flag
readyBound = true;
var DOMContentLoaded = function() {
readyFired = true;
// Remove global DOM ready listener
document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
// Execute all ready function in the stack
for ( var i = 0, readyStackLength = readyStack.length; i < readyStackLength; i++ ) {
readyStack[ i ].call( document, Popcorn );
}
// GC readyStack
readyStack = null;
};
// Register global DOM ready listener
document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
}
return;
}
if ( typeof entity === "string" ) {
try {
matches = document.querySelector( entity );
} catch( e ) {
throw new Error( "Popcorn.js Error: Invalid media element selector: " + entity );
}
}
// Get media element by id or object reference
this.media = matches || entity;
// inner reference to this media element's nodeName string value
nodeName = ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video";
// Create an audio or video element property reference
this[ nodeName ] = this.media;
this.options = options || {};
// Resolve custom ID or default prefixed ID
this.id = this.options.id || Popcorn.guid( nodeName );
// Throw if an attempt is made to use an ID that already exists
if ( Popcorn.byId( this.id ) ) {
throw new Error( "Popcorn.js Error: Cannot use duplicate ID (" + this.id + ")" );
}
this.isDestroyed = false;
this.data = {
// data structure of all
running: {
cue: []
},
// Executed by either timeupdate event or in rAF loop
timeUpdate: Popcorn.nop,
// Allows disabling a plugin per instance
disabled: {},
// Stores DOM event queues by type
events: {},
// Stores Special event hooks data
hooks: {},
// Store track event history data
history: [],
// Stores ad-hoc state related data]
state: {
volume: this.media.volume
},
// Store track event object references by trackId
trackRefs: {},
// Playback track event queues
trackEvents: {
byStart: [{
start: -1,
end: -1
}],
byEnd: [{
start: -1,
end: -1
}],
animating: [],
startIndex: 0,
endIndex: 0,
previousUpdateTime: -1
}
};
// Register new instance
Popcorn.instances.push( this );
// function to fire when video is ready
var isReady = function() {
// chrome bug: http://code.google.com/p/chromium/issues/detail?id=119598
// it is possible the video's time is less than 0
// this has the potential to call track events more than once, when they should not
// start: 0, end: 1 will start, end, start again, when it should just start
// just setting it to 0 if it is below 0 fixes this issue
if ( self.media.currentTime < 0 ) {
self.media.currentTime = 0;
}
self.media.removeEventListener( "loadeddata", isReady, false );
var duration, videoDurationPlus,
runningPlugins, runningPlugin, rpLength, rpNatives;
// Adding padding to the front and end of the arrays
// this is so we do not fall off either end
duration = self.media.duration;
// Check for no duration info (NaN)
videoDurationPlus = duration != duration ? Number.MAX_VALUE : duration + 1;
Popcorn.addTrackEvent( self, {
start: videoDurationPlus,
end: videoDurationPlus
});
if ( self.options.frameAnimation ) {
// if Popcorn is created with frameAnimation option set to true,
// requestAnimFrame is used instead of "timeupdate" media event.
// This is for greater frame time accuracy, theoretically up to
// 60 frames per second as opposed to ~4 ( ~every 15-250ms)
self.data.timeUpdate = function () {
Popcorn.timeUpdate( self, {} );
// fire frame for each enabled active plugin of every type
Popcorn.forEach( Popcorn.manifest, function( key, val ) {
runningPlugins = self.data.running[ val ];
// ensure there are running plugins on this type on this instance
if ( runningPlugins ) {
rpLength = runningPlugins.length;
for ( var i = 0; i < rpLength; i++ ) {
runningPlugin = runningPlugins[ i ];
rpNatives = runningPlugin._natives;
rpNatives && rpNatives.frame &&
rpNatives.frame.call( self, {}, runningPlugin, self.currentTime() );
}
}
});
self.emit( "timeupdate" );
!self.isDestroyed && requestAnimFrame( self.data.timeUpdate );
};
!self.isDestroyed && requestAnimFrame( self.data.timeUpdate );
} else {
self.data.timeUpdate = function( event ) {
Popcorn.timeUpdate( self, event );
};
if ( !self.isDestroyed ) {
self.media.addEventListener( "timeupdate", self.data.timeUpdate, false );
}
}
};
Object.defineProperty( this, "error", {
get: function() {
return self.media.error;
}
});
if ( self.media.readyState >= 2 ) {
isReady();
} else {
self.media.addEventListener( "loadeddata", isReady, false );
}
return this;
}
};
// Extend constructor prototype to instance prototype
// Allows chaining methods to instances
Popcorn.p.init.prototype = Popcorn.p;
Popcorn.byId = function( str ) {
var instances = Popcorn.instances,
length = instances.length,
i = 0;
for ( ; i < length; i++ ) {
if ( instances[ i ].id === str ) {
return instances[ i ];
}
}
return null;
};
Popcorn.forEach = function( obj, fn, context ) {
if ( !obj || !fn ) {
return {};
}
context = context || this;
var key, len;
// Use native whenever possible
if ( forEach && obj.forEach === forEach ) {
return obj.forEach( fn, context );
}
if ( toString.call( obj ) === "[object NodeList]" ) {
for ( key = 0, len = obj.length; key < len; key++ ) {
fn.call( context, obj[ key ], key, obj );
}
return obj;
}
for ( key in obj ) {
if ( hasOwn.call( obj, key ) ) {
fn.call( context, obj[ key ], key, obj );
}
}
return obj;
};
Popcorn.extend = function( obj ) {
var dest = obj, src = slice.call( arguments, 1 );
Popcorn.forEach( src, function( copy ) {
for ( var prop in copy ) {
dest[ prop ] = copy[ prop ];
}
});
return dest;
};
// A Few reusable utils, memoized onto Popcorn
Popcorn.extend( Popcorn, {
noConflict: function( deep ) {
if ( deep ) {
global.Popcorn = _Popcorn;
}
return Popcorn;
},
error: function( msg ) {
throw new Error( msg );
},
guid: function( prefix ) {
Popcorn.guid.counter++;
return ( prefix ? prefix : "" ) + ( +new Date() + Popcorn.guid.counter );
},
sizeOf: function( obj ) {
var size = 0;
for ( var prop in obj ) {
size++;
}
return size;
},
isArray: Array.isArray || function( array ) {
return toString.call( array ) === "[object Array]";
},
nop: function() {},
position: function( elem ) {
var clientRect = elem.getBoundingClientRect(),
bounds = {},
doc = elem.ownerDocument,
docElem = document.documentElement,
body = document.body,
clientTop, clientLeft, scrollTop, scrollLeft, top, left;
// Determine correct clientTop/Left
clientTop = docElem.clientTop || body.clientTop || 0;
clientLeft = docElem.clientLeft || body.clientLeft || 0;
// Determine correct scrollTop/Left
scrollTop = ( global.pageYOffset && docElem.scrollTop || body.scrollTop );
scrollLeft = ( global.pageXOffset && docElem.scrollLeft || body.scrollLeft );
// Temp top/left
top = Math.ceil( clientRect.top + scrollTop - clientTop );
left = Math.ceil( clientRect.left + scrollLeft - clientLeft );
for ( var p in clientRect ) {
bounds[ p ] = Math.round( clientRect[ p ] );
}
return Popcorn.extend({}, bounds, { top: top, left: left });
},
disable: function( instance, plugin ) {
if ( !instance.data.disabled[ plugin ] ) {
instance.data.disabled[ plugin ] = true;
for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) {
event = instance.data.running[ plugin ][ i ];
event._natives.end.call( instance, null, event );
}
}
return instance;
},
enable: function( instance, plugin ) {
if ( instance.data.disabled[ plugin ] ) {
instance.data.disabled[ plugin ] = false;
for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) {
event = instance.data.running[ plugin ][ i ];
event._natives.start.call( instance, null, event );
}
}
return instance;
},
destroy: function( instance ) {
var events = instance.data.events,
trackEvents = instance.data.trackEvents,
singleEvent, item, fn, plugin;
// Iterate through all events and remove them
for ( item in events ) {
singleEvent = events[ item ];
for ( fn in singleEvent ) {
delete singleEvent[ fn ];
}
events[ item ] = null;
}
// remove all plugins off the given instance
for ( plugin in Popcorn.registryByName ) {
Popcorn.removePlugin( instance, plugin );
}
// Remove all data.trackEvents #1178
trackEvents.byStart.length = 0;
trackEvents.byEnd.length = 0;
if ( !instance.isDestroyed ) {
instance.data.timeUpdate && instance.media.removeEventListener( "timeupdate", instance.data.timeUpdate, false );
instance.isDestroyed = true;
}
}
});
// Memoized GUID Counter
Popcorn.guid.counter = 1;
// Factory to implement getters, setters and controllers
// as Popcorn instance methods. The IIFE will create and return
// an object with defined methods
Popcorn.extend(Popcorn.p, (function() {
var methods = "load play pause currentTime playbackRate volume duration preload playbackRate " +
"autoplay loop controls muted buffered readyState seeking paused played seekable ended",
ret = {};
// Build methods, store in object that is returned and passed to extend
Popcorn.forEach( methods.split( /\s+/g ), function( name ) {
ret[ name ] = function( arg ) {
var previous;
if ( typeof this.media[ name ] === "function" ) {
// Support for shorthanded play(n)/pause(n) jump to currentTime
// If arg is not null or undefined and called by one of the
// allowed shorthandable methods, then set the currentTime
// Supports time as seconds or SMPTE
if ( arg != null && /play|pause/.test( name ) ) {
this.media.currentTime = Popcorn.util.toSeconds( arg );
}
this.media[ name ]();
return this;
}
if ( arg != null ) {
// Capture the current value of the attribute property
previous = this.media[ name ];
// Set the attribute property with the new value
this.media[ name ] = arg;
// If the new value is not the same as the old value
// emit an "attrchanged event"
if ( previous !== arg ) {
this.emit( "attrchange", {
attribute: name,
previousValue: previous,
currentValue: arg
});
}
return this;
}
return this.media[ name ];
};
});
return ret;
})()
);
Popcorn.forEach( "enable disable".split(" "), function( method ) {
Popcorn.p[ method ] = function( plugin ) {
return Popcorn[ method ]( this, plugin );
};
});
Popcorn.extend(Popcorn.p, {
// Rounded currentTime
roundTime: function() {
return Math.round( this.media.currentTime );
},
// Attach an event to a single point in time
exec: function( id, time, fn ) {
var length = arguments.length,
trackEvent, sec;
// Check if first could possibly be a SMPTE string
// p.cue( "smpte string", fn );
// try/catch avoid awful throw in Popcorn.util.toSeconds
// TODO: Get rid of that, replace with NaN return?
try {
sec = Popcorn.util.toSeconds( id );
} catch ( e ) {}
// If it can be converted into a number then
// it's safe to assume that the string was SMPTE
if ( typeof sec === "number" ) {
id = sec;
}
// Shift arguments based on use case
//
// Back compat for:
// p.cue( time, fn );
if ( typeof id === "number" && length === 2 ) {
fn = time;
time = id;
id = Popcorn.guid( "cue" );
} else {
// Support for new forms
// p.cue( "empty-cue" );
if ( length === 1 ) {
// Set a time for an empty cue. It's not important what
// the time actually is, because the cue is a no-op
time = -1;
} else {
// Get the trackEvent that matches the given id.
trackEvent = this.getTrackEvent( id );
if ( trackEvent ) {
// p.cue( "my-id", 12 );
// p.cue( "my-id", function() { ... });
if ( typeof id === "string" && length === 2 ) {
// p.cue( "my-id", 12 );
// The path will update the cue time.
if ( typeof time === "number" ) {
// Re-use existing trackEvent start callback
fn = trackEvent._natives.start;
}
// p.cue( "my-id", function() { ... });
// The path will update the cue function
if ( typeof time === "function" ) {
fn = time;
// Re-use existing trackEvent start time
time = trackEvent.start;
}
}
} else {
if ( length >= 2 ) {
// p.cue( "a", "00:00:00");
if ( typeof time === "string" ) {
try {
sec = Popcorn.util.toSeconds( time );
} catch ( e ) {}
time = sec;
}
// p.cue( "b", 11 );
if ( typeof time === "number" ) {
fn = Popcorn.nop();
}
// p.cue( "c", function() {});
if ( typeof time === "function" ) {
fn = time;
time = -1;
}
}
}
}
}
// Creating a one second track event with an empty end
// Or update an existing track event with new values
Popcorn.addTrackEvent( this, {
id: id,
start: time,
end: time + 1,
_running: false,
_natives: {
start: fn || Popcorn.nop,
end: Popcorn.nop,
type: "cue"
}
});
return this;
},
// Mute the calling media, optionally toggle
mute: function( toggle ) {
var event = toggle == null || toggle === true ? "muted" : "unmuted";
// If `toggle` is explicitly `false`,
// unmute the media and restore the volume level
if ( event === "unmuted" ) {
this.media.muted = false;
this.media.volume = this.data.state.volume;
}
// If `toggle` is either null or undefined,
// save the current volume and mute the media element
if ( event === "muted" ) {
this.data.state.volume = this.media.volume;
this.media.muted = true;
}
// Trigger either muted|unmuted event
this.emit( event );
return this;
},
// Convenience method, unmute the calling media
unmute: function( toggle ) {
return this.mute( toggle == null ? false : !toggle );
},
// Get the client bounding box of an instance element
position: function() {
return Popcorn.position( this.media );
},
// Toggle a plugin's playback behaviour (on or off) per instance
toggle: function( plugin ) {
return Popcorn[ this.data.disabled[ plugin ] ? "enable" : "disable" ]( this, plugin );
},
// Set default values for plugin options objects per instance
defaults: function( plugin, defaults ) {
// If an array of default configurations is provided,
// iterate and apply each to this instance
if ( Popcorn.isArray( plugin ) ) {
Popcorn.forEach( plugin, function( obj ) {
for ( var name in obj ) {
this.defaults( name, obj[ name ] );
}
}, this );
return this;
}
if ( !this.options.defaults ) {
this.options.defaults = {};
}
if ( !this.options.defaults[ plugin ] ) {
this.options.defaults[ plugin ] = {};
}
Popcorn.extend( this.options.defaults[ plugin ], defaults );
return this;
}
});
Popcorn.Events = {
UIEvents: "blur focus focusin focusout load resize scroll unload",
MouseEvents: "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave click dblclick",
Events: "loadstart progress suspend emptied stalled play pause error " +
"loadedmetadata loadeddata waiting playing canplay canplaythrough " +
"seeking seeked timeupdate ended ratechange durationchange volumechange"
};
Popcorn.Events.Natives = Popcorn.Events.UIEvents + " " +
Popcorn.Events.MouseEvents + " " +
Popcorn.Events.Events;
internal.events.apiTypes = [ "UIEvents", "MouseEvents", "Events" ];
// Privately compile events table at load time
(function( events, data ) {
var apis = internal.events.apiTypes,
eventsList = events.Natives.split( /\s+/g ),
idx = 0, len = eventsList.length, prop;
for( ; idx < len; idx++ ) {
data.hash[ eventsList[idx] ] = true;
}
apis.forEach(function( val, idx ) {
data.apis[ val ] = {};
var apiEvents = events[ val ].split( /\s+/g ),
len = apiEvents.length,
k = 0;
for ( ; k < len; k++ ) {
data.apis[ val ][ apiEvents[ k ] ] = true;
}
});
})( Popcorn.Events, internal.events );
Popcorn.events = {
isNative: function( type ) {
return !!internal.events.hash[ type ];
},
getInterface: function( type ) {
if ( !Popcorn.events.isNative( type ) ) {
return false;
}
var eventApi = internal.events,
apis = eventApi.apiTypes,
apihash = eventApi.apis,
idx = 0, len = apis.length, api, tmp;
for ( ; idx < len; idx++ ) {
tmp = apis[ idx ];
if ( apihash[ tmp ][ type ] ) {
api = tmp;
break;
}
}
return api;
},
// Compile all native events to single array
all: Popcorn.Events.Natives.split( /\s+/g ),
// Defines all Event handling static functions
fn: {
trigger: function( type, data ) {
var eventInterface, evt;
// setup checks for custom event system
if ( this.data.events[ type ] && Popcorn.sizeOf( this.data.events[ type ] ) ) {
eventInterface = Popcorn.events.getInterface( type );
if ( eventInterface ) {
evt = document.createEvent( eventInterface );
evt.initEvent( type, true, true, global, 1 );
this.media.dispatchEvent( evt );
return this;
}
// Custom events
Popcorn.forEach( this.data.events[ type ], function( obj, key ) {
obj.call( this, data );
}, this );
}
return this;
},
listen: function( type, fn ) {
var self = this,
hasEvents = true,
eventHook = Popcorn.events.hooks[ type ],
origType = type,
tmp;
if ( !this.data.events[ type ] ) {
this.data.events[ type ] = {};
hasEvents = false;
}
// Check and setup event hooks
if ( eventHook ) {
// Execute hook add method if defined
if ( eventHook.add ) {
eventHook.add.call( this, {}, fn );
}
// Reassign event type to our piggyback event type if defined
if ( eventHook.bind ) {
type = eventHook.bind;
}
// Reassign handler if defined
if ( eventHook.handler ) {
tmp = fn;
fn = function wrapper( event ) {
eventHook.handler.call( self, event, tmp );
};
}
// assume the piggy back event is registered
hasEvents = true;
// Setup event registry entry
if ( !this.data.events[ type ] ) {
this.data.events[ type ] = {};
// Toggle if the previous assumption was untrue
hasEvents = false;
}
}
// Register event and handler
this.data.events[ type ][ fn.name || ( fn.toString() + Popcorn.guid() ) ] = fn;
// only attach one event of any type
if ( !hasEvents && Popcorn.events.all.indexOf( type ) > -1 ) {
this.media.addEventListener( type, function( event ) {
Popcorn.forEach( self.data.events[ type ], function( obj, key ) {
if ( typeof obj === "function" ) {
obj.call( self, event );
}
});
}, false);
}
return this;
},
unlisten: function( type, fn ) {
if ( this.data.events[ type ] && this.data.events[ type ][ fn ] ) {
delete this.data.events[ type ][ fn ];
return this;
}
this.data.events[ type ] = null;
return this;
}
},
hooks: {
canplayall: {
bind: "canplaythrough",
add: function( event, callback ) {
var state = false;
if ( this.media.readyState ) {
callback.call( this, event );
state = true;
}
this.data.hooks.canplayall = {
fired: state
};
},
// declare special handling instructions
handler: function canplayall( event, callback ) {
if ( !this.data.hooks.canplayall.fired ) {
// trigger original user callback once
callback.call( this, event );
this.data.hooks.canplayall.fired = true;
}
}
}
}
};
// Extend Popcorn.events.fns (listen, unlisten, trigger) to all Popcorn instances
// Extend aliases (on, off, emit)
Popcorn.forEach( [ [ "trigger", "emit" ], [ "listen", "on" ], [ "unlisten", "off" ] ], function( key ) {
Popcorn.p[ key[ 0 ] ] = Popcorn.p[ key[ 1 ] ] = Popcorn.events.fn[ key[ 0 ] ];
});
// Internal Only - Adds track events to the instance object
Popcorn.addTrackEvent = function( obj, track ) {
var trackEvent, isUpdate, eventType;
// Do a lookup for existing trackevents with this id
if ( track.id ) {
trackEvent = obj.getTrackEvent( track.id );
}
// If a track event by this id currently exists, modify it
if ( trackEvent ) {
isUpdate = true;
// Create a new object with the existing trackEvent
// Extend with new track properties
track = Popcorn.extend( {}, trackEvent, track );
// Remove the existing track from the instance
obj.removeTrackEvent( track.id );
}
// Determine if this track has default options set for it
// If so, apply them to the track object
if ( track && track._natives && track._natives.type &&
( obj.options.defaults && obj.options.defaults[ track._natives.type ] ) ) {
track = Popcorn.extend( {}, obj.options.defaults[ track._natives.type ], track );
}
if ( track._natives ) {
// Supports user defined track event id
track._id = track.id || track._id || Popcorn.guid( track._natives.type );
// Push track event ids into the history
obj.data.history.push( track._id );
}
track.start = Popcorn.util.toSeconds( track.start, obj.options.framerate );
track.end = Popcorn.util.toSeconds( track.end, obj.options.framerate );
// Store this definition in an array sorted by times
var byStart = obj.data.trackEvents.byStart,
byEnd = obj.data.trackEvents.byEnd,
startIndex, endIndex;
for ( startIndex = byStart.length - 1; startIndex >= 0; startIndex-- ) {
if ( track.start >= byStart[ startIndex ].start ) {
byStart.splice( startIndex + 1, 0, track );
break;
}
}
for ( endIndex = byEnd.length - 1; endIndex >= 0; endIndex-- ) {
if ( track.end > byEnd[ endIndex ].end ) {
byEnd.splice( endIndex + 1, 0, track );
break;
}
}
// Display track event immediately if it's enabled and current
if ( track.end > obj.media.currentTime &&
track.start <= obj.media.currentTime ) {
track._running = true;
obj.data.running[ track._natives.type ].push( track );
if ( !obj.data.disabled[ track._natives.type ] ) {
track._natives.start.call( obj, null, track );
}
}
// update startIndex and endIndex
if ( startIndex <= obj.data.trackEvents.startIndex &&
track.start <= obj.data.trackEvents.previousUpdateTime ) {
obj.data.trackEvents.startIndex++;
}
if ( endIndex <= obj.data.trackEvents.endIndex &&
track.end < obj.data.trackEvents.previousUpdateTime ) {
obj.data.trackEvents.endIndex++;
}
this.timeUpdate( obj, null, true );
// Store references to user added trackevents in ref table
if ( track._id ) {
Popcorn.addTrackEvent.ref( obj, track );
}
// If the call to addTrackEvent was an update/modify call, fire an event
if ( isUpdate ) {
// Determine appropriate event type to trigger
// they are identical in function, but the naming
// adds some level of intuition for the end developer
// to rely on
if ( track._natives.type === "cue" ) {
eventType = "cuechange";
} else {
eventType = "trackchange";
}
// Fire an event with change information
obj.emit( eventType, {
id: track.id,
previousValue: {
time: trackEvent.start,
fn: trackEvent._natives.start
},
currentValue: {
time: track.start,
fn: track._natives.start
}
});
}
};
// Internal Only - Adds track event references to the instance object's trackRefs hash table
Popcorn.addTrackEvent.ref = function( obj, track ) {
obj.data.trackRefs[ track._id ] = track;
return obj;
};
Popcorn.removeTrackEvent = function( obj, removeId ) {
var start, end, animate,
historyLen = obj.data.history.length,
length = obj.data.trackEvents.byStart.length,
index = 0,
indexWasAt = 0,
byStart = [],
byEnd = [],
animating = [],
history = [];
while ( --length > -1 ) {
start = obj.data.trackEvents.byStart[ index ];
end = obj.data.trackEvents.byEnd[ index ];
// Padding events will not have _id properties.
// These should be safely pushed onto the front and back of the
// track event array
if ( !start._id ) {
byStart.push( start );
byEnd.push( end );
}
// Filter for user track events (vs system track events)
if ( start._id ) {
// If not a matching start event for removal
if ( start._id !== removeId ) {
byStart.push( start );
}
// If not a matching end event for removal
if ( end._id !== removeId ) {
byEnd.push( end );
}
// If the _id is matched, capture the current index
if ( start._id === removeId ) {
indexWasAt = index;
// If a _teardown function was defined,
// enforce for track event removals
if ( start._natives._teardown ) {
start._natives._teardown.call( obj, start );
}
}
}
// Increment the track index
index++;
}
// Reset length to be used by the condition below to determine
// if animating track events should also be filtered for removal.
// Reset index below to be used by the reverse while as an
// incrementing counter
length = obj.data.trackEvents.animating.length;
index = 0;
if ( length ) {
while ( --length > -1 ) {
animate = obj.data.trackEvents.animating[ index ];
// Padding events will not have _id properties.
// These should be safely pushed onto the front and back of the
// track event array
if ( !animate._id ) {
animating.push( animate );
}
// If not a matching animate event for removal
if ( animate._id && animate._id !== removeId ) {
animating.push( animate );
}
// Increment the track index
index++;
}
}
// Update
if ( indexWasAt <= obj.data.trackEvents.startIndex ) {
obj.data.trackEvents.startIndex--;
}
if ( indexWasAt <= obj.data.trackEvents.endIndex ) {
obj.data.trackEvents.endIndex--;
}
obj.data.trackEvents.byStart = byStart;
obj.data.trackEvents.byEnd = byEnd;
obj.data.trackEvents.animating = animating;
for ( var i = 0; i < historyLen; i++ ) {
if ( obj.data.history[ i ] !== removeId ) {
history.push( obj.data.history[ i ] );
}
}
// Update ordered history array
obj.data.history = history;
// Update track event references
Popcorn.removeTrackEvent.ref( obj, removeId );
};
// Internal Only - Removes track event references from instance object's trackRefs hash table
Popcorn.removeTrackEvent.ref = function( obj, removeId ) {
delete obj.data.trackRefs[ removeId ];
return obj;
};
// Return an array of track events bound to this instance object
Popcorn.getTrackEvents = function( obj ) {
var trackevents = [],
refs = obj.data.trackEvents.byStart,
length = refs.length,
idx = 0,
ref;
for ( ; idx < length; idx++ ) {
ref = refs[ idx ];
// Return only user attributed track event references
if ( ref._id ) {
trackevents.push( ref );
}
}
return trackevents;
};
// Internal Only - Returns an instance object's trackRefs hash table
Popcorn.getTrackEvents.ref = function( obj ) {
return obj.data.trackRefs;
};
// Return a single track event bound to this instance object
Popcorn.getTrackEvent = function( obj, trackId ) {
return obj.data.trackRefs[ trackId ];
};
// Internal Only - Returns an instance object's track reference by track id
Popcorn.getTrackEvent.ref = function( obj, trackId ) {
return obj.data.trackRefs[ trackId ];
};
Popcorn.getLastTrackEventId = function( obj ) {
return obj.data.history[ obj.data.history.length - 1 ];
};
Popcorn.timeUpdate = function( obj, event ) {
var currentTime = obj.media.currentTime,
previousTime = obj.data.trackEvents.previousUpdateTime,
tracks = obj.data.trackEvents,
end = tracks.endIndex,
start = tracks.startIndex,
byStartLen = tracks.byStart.length,
byEndLen = tracks.byEnd.length,
registryByName = Popcorn.registryByName,
trackstart = "trackstart",
trackend = "trackend",
byEnd, byStart, byAnimate, natives, type, runningPlugins;
// Playbar advancing
if ( previousTime <= currentTime ) {
while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end <= currentTime ) {
byEnd = tracks.byEnd[ end ];
natives = byEnd._natives;
type = natives && natives.type;
// If plugin does not exist on this instance, remove it
if ( !natives ||
( !!registryByName[ type ] ||
!!obj[ type ] ) ) {
if ( byEnd._running === true ) {
byEnd._running = false;
runningPlugins = obj.data.running[ type ];
runningPlugins.splice( runningPlugins.indexOf( byEnd ), 1 );
if ( !obj.data.disabled[ type ] ) {
natives.end.call( obj, event, byEnd );
obj.emit( trackend,
Popcorn.extend({}, byEnd, {
plugin: type,
type: trackend
})
);
}
}
end++;
} else {
// remove track event
Popcorn.removeTrackEvent( obj, byEnd._id );
return;
}
}
while ( tracks.byStart[ start ] && tracks.byStart[ start ].start <= currentTime ) {
byStart = tracks.byStart[ start ];
natives = byStart._natives;
type = natives && natives.type;
// If plugin does not exist on this instance, remove it
if ( !natives ||
( !!registryByName[ type ] ||
!!obj[ type ] ) ) {
if ( byStart.end > currentTime &&
byStart._running === false ) {
byStart._running = true;
obj.data.running[ type ].push( byStart );
if ( !obj.data.disabled[ type ] ) {
natives.start.call( obj, event, byStart );
obj.emit( trackstart,
Popcorn.extend({}, byStart, {
plugin: type,
type: trackstart
})
);
}
}
start++;
} else {
// remove track event
Popcorn.removeTrackEvent( obj, byStart._id );
return;
}
}
// Playbar receding
} else if ( previousTime > currentTime ) {
while ( tracks.byStart[ start ] && tracks.byStart[ start ].start > currentTime ) {
byStart = tracks.byStart[ start ];
natives = byStart._natives;
type = natives && natives.type;
// if plugin does not exist on this instance, remove it
if ( !natives ||
( !!registryByName[ type ] ||
!!obj[ type ] ) ) {
if ( byStart._running === true ) {
byStart._running = false;
runningPlugins = obj.data.running[ type ];
runningPlugins.splice( runningPlugins.indexOf( byStart ), 1 );
if ( !obj.data.disabled[ type ] ) {
natives.end.call( obj, event, byStart );
obj.emit( trackend,
Popcorn.extend({}, byStart, {
plugin: type,
type: trackend
})
);
}
}
start--;
} else {
// remove track event
Popcorn.removeTrackEvent( obj, byStart._id );
return;
}
}
while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end > currentTime ) {
byEnd = tracks.byEnd[ end ];
natives = byEnd._natives;
type = natives && natives.type;
// if plugin does not exist on this instance, remove it
if ( !natives ||
( !!registryByName[ type ] ||
!!obj[ type ] ) ) {
if ( byEnd.start <= currentTime &&
byEnd._running === false ) {
byEnd._running = true;
obj.data.running[ type ].push( byEnd );
if ( !obj.data.disabled[ type ] ) {
natives.start.call( obj, event, byEnd );
obj.emit( trackstart,
Popcorn.extend({}, byEnd, {
plugin: type,
type: trackstart
})
);
}
}
end--;
} else {
// remove track event
Popcorn.removeTrackEvent( obj, byEnd._id );
return;
}
}
}
tracks.endIndex = end;
tracks.startIndex = start;
tracks.previousUpdateTime = currentTime;
//enforce index integrity if trackRemoved
tracks.byStart.length < byStartLen && tracks.startIndex--;
tracks.byEnd.length < byEndLen && tracks.endIndex--;
};
// Map and Extend TrackEvent functions to all Popcorn instances
Popcorn.extend( Popcorn.p, {
getTrackEvents: function() {
return Popcorn.getTrackEvents.call( null, this );
},
getTrackEvent: function( id ) {
return Popcorn.getTrackEvent.call( null, this, id );
},
getLastTrackEventId: function() {
return Popcorn.getLastTrackEventId.call( null, this );
},
removeTrackEvent: function( id ) {
Popcorn.removeTrackEvent.call( null, this, id );
return this;
},
removePlugin: function( name ) {
Popcorn.removePlugin.call( null, this, name );
return this;
},
timeUpdate: function( event ) {
Popcorn.timeUpdate.call( null, this, event );
return this;
},
destroy: function() {
Popcorn.destroy.call( null, this );
return this;
}
});
// Plugin manifests
Popcorn.manifest = {};
// Plugins are registered
Popcorn.registry = [];
Popcorn.registryByName = {};
// An interface for extending Popcorn
// with plugin functionality
Popcorn.plugin = function( name, definition, manifest ) {
if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
Popcorn.error( "'" + name + "' is a protected function name" );
return;
}
// Provides some sugar, but ultimately extends
// the definition into Popcorn.p
var reserved = [ "start", "end" ],
plugin = {},
setup,
isfn = typeof definition === "function",
methods = [ "_setup", "_teardown", "start", "end", "frame" ];
// combines calls of two function calls into one
var combineFn = function( first, second ) {
first = first || Popcorn.nop;
second = second || Popcorn.nop;
return function() {
first.apply( this, arguments );
second.apply( this, arguments );
};
};
// If `manifest` arg is undefined, check for manifest within the `definition` object
// If no `definition.manifest`, an empty object is a sufficient fallback
Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {};
// apply safe, and empty default functions
methods.forEach(function( method ) {
definition[ method ] = safeTry( definition[ method ] || Popcorn.nop, name );
});
var pluginFn = function( setup, options ) {
if ( !options ) {
return this;
}
// When the "ranges" property is set and its value is an array, short-circuit
// the pluginFn definition to recall itself with an options object generated from
// each range object in the ranges array. (eg. { start: 15, end: 16 } )
if ( options.ranges && Popcorn.isArray(options.ranges) ) {
Popcorn.forEach( options.ranges, function( range ) {
// Create a fresh object, extend with current options
// and start/end range object's properties
// Works with in/out as well.
var opts = Popcorn.extend( {}, options, range );
// Remove the ranges property to prevent infinitely
// entering this condition
delete opts.ranges;
// Call the plugin with the newly created opts object
this[ name ]( opts );
}, this);
// Return the Popcorn instance to avoid creating an empty track event
return this;
}
// Storing the plugin natives
var natives = options._natives = {},
compose = "",
originalOpts, manifestOpts;
Popcorn.extend( natives, setup );
options._natives.type = name;
options._running = false;
natives.start = natives.start || natives[ "in" ];
natives.end = natives.end || natives[ "out" ];
if ( options.once ) {
natives.end = combineFn( natives.end, function() {
this.removeTrackEvent( options._id );
});
}
// extend teardown to always call end if running
natives._teardown = combineFn(function() {
var args = slice.call( arguments ),
runningPlugins = this.data.running[ natives.type ];
// end function signature is not the same as teardown,
// put null on the front of arguments for the event parameter
args.unshift( null );
// only call end if event is running
args[ 1 ]._running &&
runningPlugins.splice( runningPlugins.indexOf( options ), 1 ) &&
natives.end.apply( this, args );
}, natives._teardown );
// default to an empty string if no effect exists
// split string into an array of effects
options.compose = options.compose && options.compose.split( " " ) || [];
options.effect = options.effect && options.effect.split( " " ) || [];
// join the two arrays together
options.compose = options.compose.concat( options.effect );
options.compose.forEach(function( composeOption ) {
// if the requested compose is garbage, throw it away
compose = Popcorn.compositions[ composeOption ] || {};
// extends previous functions with compose function
methods.forEach(function( method ) {
natives[ method ] = combineFn( natives[ method ], compose[ method ] );
});
});
// Ensure a manifest object, an empty object is a sufficient fallback
options._natives.manifest = manifest;
// Checks for expected properties
if ( !( "start" in options ) ) {
options.start = options[ "in" ] || 0;
}
if ( !options.end && options.end !== 0 ) {
options.end = options[ "out" ] || Number.MAX_VALUE;
}
// Use hasOwn to detect non-inherited toString, since all
// objects will receive a toString - its otherwise undetectable
if ( !hasOwn.call( options, "toString" ) ) {
options.toString = function() {
var props = [
"start: " + options.start,
"end: " + options.end,
"id: " + (options.id || options._id)
];
// Matches null and undefined, allows: false, 0, "" and truthy
if ( options.target != null ) {
props.push( "target: " + options.target );
}
return name + " ( " + props.join(", ") + " )";
};
}
// Resolves 239, 241, 242
if ( !options.target ) {
// Sometimes the manifest may be missing entirely
// or it has an options object that doesn't have a `target` property
manifestOpts = "options" in manifest && manifest.options;
options.target = manifestOpts && "target" in manifestOpts && manifestOpts.target;
}
if ( options._natives ) {
// ensure an initial id is there before setup is called
options._id = Popcorn.guid( options._natives.type );
}
// Trigger _setup method if exists
options._natives._setup && options._natives._setup.call( this, options );
// Create new track event for this instance
Popcorn.addTrackEvent( this, options );
// Future support for plugin event definitions
// for all of the native events
Popcorn.forEach( setup, function( callback, type ) {
if ( type !== "type" ) {
if ( reserved.indexOf( type ) === -1 ) {
this.on( type, callback );
}
}
}, this );
return this;
};
// Extend Popcorn.p with new named definition
// Assign new named definition
Popcorn.p[ name ] = plugin[ name ] = function( id, options ) {
var length = arguments.length,
trackEvent, defaults, mergedSetupOpts;
// Shift arguments based on use case
//
// Back compat for:
// p.plugin( options );
if ( id && !options ) {
options = id;
id = null;
} else {
// Get the trackEvent that matches the given id.
trackEvent = this.getTrackEvent( id );
// If the track event does not exist, ensure that the options
// object has a proper id
if ( !trackEvent ) {
options.id = id;
// If the track event does exist, merge the updated properties
} else {
options = Popcorn.extend( {}, trackEvent, options );
Popcorn.addTrackEvent( this, options );
return this;
}
}
this.data.running[ name ] = this.data.running[ name ] || [];
// Merge with defaults if they exist, make sure per call is prioritized
defaults = ( this.options.defaults && this.options.defaults[ name ] ) || {};
mergedSetupOpts = Popcorn.extend( {}, defaults, options );
return pluginFn.call( this, isfn ? definition.call( this, mergedSetupOpts ) : definition,
mergedSetupOpts );
};
// if the manifest parameter exists we should extend it onto the definition object
// so that it shows up when calling Popcorn.registry and Popcorn.registryByName
if ( manifest ) {
Popcorn.extend( definition, {
manifest: manifest
});
}
// Push into the registry
var entry = {
fn: plugin[ name ],
definition: definition,
base: definition,
parents: [],
name: name
};
Popcorn.registry.push(
Popcorn.extend( plugin, entry, {
type: name
})
);
Popcorn.registryByName[ name ] = entry;
return plugin;
};
// Storage for plugin function errors
Popcorn.plugin.errors = [];
// Returns wrapped plugin function
function safeTry( fn, pluginName ) {
return function() {
// When Popcorn.plugin.debug is true, do not suppress errors
if ( Popcorn.plugin.debug ) {
return fn.apply( this, arguments );
}
try {
return fn.apply( this, arguments );
} catch ( ex ) {
// Push plugin function errors into logging queue
Popcorn.plugin.errors.push({
plugin: pluginName,
thrown: ex,
source: fn.toString()
});
// Trigger an error that the instance can listen for
// and react to
this.emit( "pluginerror", Popcorn.plugin.errors );
}
};
}
// Debug-mode flag for plugin development
// True for Popcorn development versions, false for stable/tagged versions
Popcorn.plugin.debug = ( Popcorn.version === "@" + "VERSION" );
// removePlugin( type ) removes all tracks of that from all instances of popcorn
// removePlugin( obj, type ) removes all tracks of type from obj, where obj is a single instance of popcorn
Popcorn.removePlugin = function( obj, name ) {
// Check if we are removing plugin from an instance or from all of Popcorn
if ( !name ) {
// Fix the order
name = obj;
obj = Popcorn.p;
if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
Popcorn.error( "'" + name + "' is a protected function name" );
return;
}
var registryLen = Popcorn.registry.length,
registryIdx;
// remove plugin reference from registry
for ( registryIdx = 0; registryIdx < registryLen; registryIdx++ ) {
if ( Popcorn.registry[ registryIdx ].name === name ) {
Popcorn.registry.splice( registryIdx, 1 );
delete Popcorn.registryByName[ name ];
delete Popcorn.manifest[ name ];
// delete the plugin
delete obj[ name ];
// plugin found and removed, stop checking, we are done
return;
}
}
}
var byStart = obj.data.trackEvents.byStart,
byEnd = obj.data.trackEvents.byEnd,
animating = obj.data.trackEvents.animating,
idx, sl;
// remove all trackEvents
for ( idx = 0, sl = byStart.length; idx < sl; idx++ ) {
if ( byStart[ idx ] && byStart[ idx ]._natives && byStart[ idx ]._natives.type === name ) {
byStart[ idx ]._natives._teardown && byStart[ idx ]._natives._teardown.call( obj, byStart[ idx ] );
byStart.splice( idx, 1 );
// update for loop if something removed, but keep checking
idx--; sl--;
if ( obj.data.trackEvents.startIndex <= idx ) {
obj.data.trackEvents.startIndex--;
obj.data.trackEvents.endIndex--;
}
}
// clean any remaining references in the end index
// we do this seperate from the above check because they might not be in the same order
if ( byEnd[ idx ] && byEnd[ idx ]._natives && byEnd[ idx ]._natives.type === name ) {
byEnd.splice( idx, 1 );
}
}
//remove all animating events
for ( idx = 0, sl = animating.length; idx < sl; idx++ ) {
if ( animating[ idx ] && animating[ idx ]._natives && animating[ idx ]._natives.type === name ) {
animating.splice( idx, 1 );
// update for loop if something removed, but keep checking
idx--; sl--;
}
}
};
Popcorn.compositions = {};
// Plugin inheritance
Popcorn.compose = function( name, definition, manifest ) {
// If `manifest` arg is undefined, check for manifest within the `definition` object
// If no `definition.manifest`, an empty object is a sufficient fallback
Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {};
// register the effect by name
Popcorn.compositions[ name ] = definition;
};
Popcorn.plugin.effect = Popcorn.effect = Popcorn.compose;
var rnaiveExpr = /^(?:\.|#|\[)/;
// Basic DOM utilities and helpers API. See #1037
Popcorn.dom = {
debug: false,
// Popcorn.dom.find( selector, context )
//
// Returns the first element that matches the specified selector
// Optionally provide a context element, defaults to `document`
//
// eg.
// Popcorn.dom.find("video") returns the first video element
// Popcorn.dom.find("#foo") returns the first element with `id="foo"`
// Popcorn.dom.find("foo") returns the first element with `id="foo"`
// Note: Popcorn.dom.find("foo") is the only allowed deviation
// from valid querySelector selector syntax
//
// Popcorn.dom.find(".baz") returns the first element with `class="baz"`
// Popcorn.dom.find("[preload]") returns the first element with `preload="..."`
// ...
// See https://developer.mozilla.org/En/DOM/Document.querySelector
//
//
find: function( selector, context ) {
var node = null;
// Trim leading/trailing whitespace to avoid false negatives
selector = selector.trim();
// Default context is the `document`
context = context || document;
if ( selector ) {
// If the selector does not begin with "#", "." or "[",
// it could be either a nodeName or ID w/o "#"
if ( !rnaiveExpr.test( selector ) ) {
// Try finding an element that matches by ID first
node = document.getElementById( selector );
// If a match was found by ID, return the element
if ( node !== null ) {
return node;
}
}
// Assume no elements have been found yet
// Catch any invalid selector syntax errors and bury them.
try {
node = context.querySelector( selector );
} catch ( e ) {
if ( Popcorn.dom.debug ) {
throw new Error(e);
}
}
}
return node;
}
};
// Cache references to reused RegExps
var rparams = /\?/,
// XHR Setup object
setup = {
url: "",
data: "",
dataType: "",
success: Popcorn.nop,
type: "GET",
async: true,
xhr: function() {
return new global.XMLHttpRequest();
}
};
Popcorn.xhr = function( options ) {
options.dataType = options.dataType && options.dataType.toLowerCase() || null;
if ( options.dataType &&
( options.dataType === "jsonp" || options.dataType === "script" ) ) {
Popcorn.xhr.getJSONP(
options.url,
options.success,
options.dataType === "script"
);
return;
}
var settings = Popcorn.extend( {}, setup, options );
// Create new XMLHttpRequest object
settings.ajax = settings.xhr();
if ( settings.ajax ) {
if ( settings.type === "GET" && settings.data ) {
// append query string
settings.url += ( rparams.test( settings.url ) ? "&" : "?" ) + settings.data;
// Garbage collect and reset settings.data
settings.data = null;
}
settings.ajax.open( settings.type, settings.url, settings.async );
settings.ajax.send( settings.data || null );
return Popcorn.xhr.httpData( settings );
}
};
Popcorn.xhr.httpData = function( settings ) {
var data, json = null,
parser, xml = null;
settings.ajax.onreadystatechange = function() {
if ( settings.ajax.readyState === 4 ) {
try {
json = JSON.parse( settings.ajax.responseText );
} catch( e ) {
//suppress
}
data = {
xml: settings.ajax.responseXML,
text: settings.ajax.responseText,
json: json
};
// Normalize: data.xml is non-null in IE9 regardless of if response is valid xml
if ( !data.xml || !data.xml.documentElement ) {
data.xml = null;
try {
parser = new DOMParser();
xml = parser.parseFromString( settings.ajax.responseText, "text/xml" );
if ( !xml.getElementsByTagName( "parsererror" ).length ) {
data.xml = xml;
}
} catch ( e ) {
// data.xml remains null
}
}
// If a dataType was specified, return that type of data
if ( settings.dataType ) {
data = data[ settings.dataType ];
}
settings.success.call( settings.ajax, data );
}
};
return data;
};
Popcorn.xhr.getJSONP = function( url, success, isScript ) {
var head = document.head || document.getElementsByTagName( "head" )[ 0 ] || document.documentElement,
script = document.createElement( "script" ),
isFired = false,
params = [],
rjsonp = /(=)\?(?=&|$)|\?\?/,
replaceInUrl, prefix, paramStr, callback, callparam;
if ( !isScript ) {
// is there a calback already in the url
callparam = url.match( /(callback=[^&]*)/ );
if ( callparam !== null && callparam.length ) {
prefix = callparam[ 1 ].split( "=" )[ 1 ];
// Since we need to support developer specified callbacks
// and placeholders in harmony, make sure matches to "callback="
// aren't just placeholders.
// We coded ourselves into a corner here.
// JSONP callbacks should never have been
// allowed to have developer specified callbacks
if ( prefix === "?" ) {
prefix = "jsonp";
}
// get the callback name
callback = Popcorn.guid( prefix );
// replace existing callback name with unique callback name
url = url.replace( /(callback=[^&]*)/, "callback=" + callback );
} else {
callback = Popcorn.guid( "jsonp" );
if ( rjsonp.test( url ) ) {
url = url.replace( rjsonp, "$1" + callback );
}
// split on first question mark,
// this is to capture the query string
params = url.split( /\?(.+)?/ );
// rebuild url with callback
url = params[ 0 ] + "?";
if ( params[ 1 ] ) {
url += params[ 1 ] + "&";
}
url += "callback=" + callback;
}
// Define the JSONP success callback globally
window[ callback ] = function( data ) {
// Fire success callbacks
success && success( data );
isFired = true;
};
}
script.addEventListener( "load", function() {
// Handling remote script loading callbacks
if ( isScript ) {
// getScript
success && success();
}
// Executing for JSONP requests
if ( isFired ) {
// Garbage collect the callback
delete window[ callback ];
}
// Garbage collect the script resource
head.removeChild( script );
}, false );
script.src = url;
head.insertBefore( script, head.firstChild );
return;
};
Popcorn.getJSONP = Popcorn.xhr.getJSONP;
Popcorn.getScript = Popcorn.xhr.getScript = function( url, success ) {
return Popcorn.xhr.getJSONP( url, success, true );
};
Popcorn.util = {
// Simple function to parse a timestamp into seconds
// Acceptable formats are:
// HH:MM:SS.MMM
// HH:MM:SS;FF
// Hours and minutes are optional. They default to 0
toSeconds: function( timeStr, framerate ) {
// Hours and minutes are optional
// Seconds must be specified
// Seconds can be followed by milliseconds OR by the frame information
var validTimeFormat = /^([0-9]+:){0,2}[0-9]+([.;][0-9]+)?$/,
errorMessage = "Invalid time format",
digitPairs, lastIndex, lastPair, firstPair,
frameInfo, frameTime;
if ( typeof timeStr === "number" ) {
return timeStr;
}
if ( typeof timeStr === "string" &&
!validTimeFormat.test( timeStr ) ) {
Popcorn.error( errorMessage );
}
digitPairs = timeStr.split( ":" );
lastIndex = digitPairs.length - 1;
lastPair = digitPairs[ lastIndex ];
// Fix last element:
if ( lastPair.indexOf( ";" ) > -1 ) {
frameInfo = lastPair.split( ";" );
frameTime = 0;
if ( framerate && ( typeof framerate === "number" ) ) {
frameTime = parseFloat( frameInfo[ 1 ], 10 ) / framerate;
}
digitPairs[ lastIndex ] = parseInt( frameInfo[ 0 ], 10 ) + frameTime;
}
firstPair = digitPairs[ 0 ];
return {
1: parseFloat( firstPair, 10 ),
2: ( parseInt( firstPair, 10 ) * 60 ) +
parseFloat( digitPairs[ 1 ], 10 ),
3: ( parseInt( firstPair, 10 ) * 3600 ) +
( parseInt( digitPairs[ 1 ], 10 ) * 60 ) +
parseFloat( digitPairs[ 2 ], 10 )
}[ digitPairs.length || 1 ];
}
};
// alias for exec function
Popcorn.p.cue = Popcorn.p.exec;
// Protected API methods
Popcorn.protect = {
natives: getKeys( Popcorn.p ).map(function( val ) {
return val.toLowerCase();
})
};
// Setup logging for deprecated methods
Popcorn.forEach({
// Deprecated: Recommended
"listen": "on",
"unlisten": "off",
"trigger": "emit",
"exec": "cue"
}, function( recommend, api ) {
var original = Popcorn.p[ api ];
// Override the deprecated api method with a method of the same name
// that logs a warning and defers to the new recommended method
Popcorn.p[ api ] = function() {
if ( typeof console !== "undefined" && console.warn ) {
console.warn(
"Deprecated method '" + api + "', " +
(recommend == null ? "do not use." : "use '" + recommend + "' instead." )
);
// Restore api after first warning
Popcorn.p[ api ] = original;
}
return Popcorn.p[ recommend ].apply( this, [].slice.call( arguments ) );
};
});
// Exposes Popcorn to global context
global.Popcorn = Popcorn;
})(window, window.document);
/*!
* Popcorn.sequence
*
* Copyright 2011, Rick Waldron
* Licensed under MIT license.
*
*/
/* jslint forin: true, maxerr: 50, indent: 4, es5: true */
/* global Popcorn: true */
// Requires Popcorn.js
(function( global, Popcorn ) {
// TODO: as support increases, migrate to element.dataset
var doc = global.document,
location = global.location,
rprotocol = /:\/\//,
// TODO: better solution to this sucky stop-gap
lochref = location.href.replace( location.href.split("/").slice(-1)[0], "" ),
// privately held
range = function(start, stop, step) {
start = start || 0;
stop = ( stop || start || 0 ) + 1;
step = step || 1;
var len = Math.ceil((stop - start) / step) || 0,
idx = 0,
range = [];
range.length = len;
while (idx < len) {
range[idx++] = start;
start += step;
}
return range;
};
Popcorn.sequence = function( parent, list ) {
return new Popcorn.sequence.init( parent, list );
};
Popcorn.sequence.init = function( parent, list ) {
// Video element
this.parent = doc.getElementById( parent );
// Store ref to a special ID
this.seqId = Popcorn.guid( "__sequenced" );
// List of HTMLVideoElements
this.queue = [];
// List of Popcorn objects
this.playlist = [];
// Lists of in/out points
this.inOuts = {
// Stores the video in/out times for each video in sequence
ofVideos: [],
// Stores the clip in/out times for each clip in sequences
ofClips: []
};
// Store first video dimensions
this.dims = {
width: 0, //this.video.videoWidth,
height: 0 //this.video.videoHeight
};
this.active = 0;
this.cycling = false;
this.playing = false;
this.times = {
last: 0
};
// Store event pointers and queues
this.events = {
};
var self = this,
clipOffset = 0;
// Create `video` elements
Popcorn.forEach( list, function( media, idx ) {
var video = doc.createElement( "video" );
video.preload = "auto";
// Setup newly created video element
video.controls = true;
// If the first, show it, if the after, hide it
video.style.display = ( idx && "none" ) || "" ;
// Seta registered sequence id
video.id = self.seqId + "-" + idx ;
// Push this video into the sequence queue
self.queue.push( video );
var //satisfy lint
mIn = media["in"],
mOut = media["out"];
// Push the in/out points into sequence ioVideos
self.inOuts.ofVideos.push({
"in": ( mIn !== undefined && mIn ) || 1,
"out": ( mOut !== undefined && mOut ) || 0
});
self.inOuts.ofVideos[ idx ]["out"] = self.inOuts.ofVideos[ idx ]["out"] || self.inOuts.ofVideos[ idx ]["in"] + 2;
// Set the sources
video.src = !rprotocol.test( media.src ) ? lochref + media.src : media.src;
// Set some squence specific data vars
video.setAttribute("data-sequence-owner", parent );
video.setAttribute("data-sequence-guid", self.seqId );
video.setAttribute("data-sequence-id", idx );
video.setAttribute("data-sequence-clip", [ self.inOuts.ofVideos[ idx ]["in"], self.inOuts.ofVideos[ idx ]["out"] ].join(":") );
// Append the video to the parent element
self.parent.appendChild( video );
self.playlist.push( Popcorn("#" + video.id ) );
});
self.inOuts.ofVideos.forEach(function( obj ) {
var clipDuration = obj["out"] - obj["in"],
offs = {
"in": clipOffset,
"out": clipOffset + clipDuration
};
self.inOuts.ofClips.push( offs );
clipOffset = offs["out"] + 1;
});
Popcorn.forEach( this.queue, function( media, idx ) {
function canPlayThrough( event ) {
// If this is idx zero, use it as dimension for all
if ( !idx ) {
self.dims.width = media.videoWidth;
self.dims.height = media.videoHeight;
}
media.currentTime = self.inOuts.ofVideos[ idx ]["in"] - 0.5;
media.removeEventListener( "canplaythrough", canPlayThrough, false );
return true;
}
// Hook up event listeners for managing special playback
media.addEventListener( "canplaythrough", canPlayThrough, false );
// TODO: consolidate & DRY
media.addEventListener( "play", function( event ) {
self.playing = true;
}, false );
media.addEventListener( "pause", function( event ) {
self.playing = false;
}, false );
media.addEventListener( "timeupdate", function( event ) {
var target = event.srcElement || event.target,
seqIdx = +( (target.dataset && target.dataset.sequenceId) || target.getAttribute("data-sequence-id") ),
floor = Math.floor( media.currentTime );
if ( self.times.last !== floor &&
seqIdx === self.active ) {
self.times.last = floor;
if ( floor === self.inOuts.ofVideos[ seqIdx ]["out"] ) {
Popcorn.sequence.cycle.call( self, seqIdx );
}
}
}, false );
});
return this;
};
Popcorn.sequence.init.prototype = Popcorn.sequence.prototype;
//
Popcorn.sequence.cycle = function( idx ) {
if ( !this.queue ) {
Popcorn.error("Popcorn.sequence.cycle is not a public method");
}
var // Localize references
queue = this.queue,
ioVideos = this.inOuts.ofVideos,
current = queue[ idx ],
nextIdx = 0,
next, clip;
var // Popcorn instances
$popnext,
$popprev;
if ( queue[ idx + 1 ] ) {
nextIdx = idx + 1;
}
// Reset queue
if ( !queue[ idx + 1 ] ) {
nextIdx = 0;
this.playlist[ idx ].pause();
} else {
next = queue[ nextIdx ];
clip = ioVideos[ nextIdx ];
// Constrain dimentions
Popcorn.extend( next, {
width: this.dims.width,
height: this.dims.height
});
$popnext = this.playlist[ nextIdx ];
$popprev = this.playlist[ idx ];
// When not resetting to 0
current.pause();
this.active = nextIdx;
this.times.last = clip["in"] - 1;
// Play the next video in the sequence
$popnext.currentTime( clip["in"] );
$popnext[ nextIdx ? "play" : "pause" ]();
// Trigger custom cycling event hook
this.trigger( "cycle", {
position: {
previous: idx,
current: nextIdx
}
});
// Set the previous back to it's beginning time
// $popprev.currentTime( ioVideos[ idx ].in );
if ( nextIdx ) {
// Hide the currently ending video
current.style.display = "none";
// Show the next video in the sequence
next.style.display = "";
}
this.cycling = false;
}
return this;
};
var excludes = [ "timeupdate", "play", "pause" ];
// Sequence object prototype
Popcorn.extend( Popcorn.sequence.prototype, {
// Returns Popcorn object from sequence at index
eq: function( idx ) {
return this.playlist[ idx ];
},
// Remove a sequence from it's playback display container
remove: function() {
this.parent.innerHTML = null;
},
// Returns Clip object from sequence at index
clip: function( idx ) {
return this.inOuts.ofVideos[ idx ];
},
// Returns sum duration for all videos in sequence
duration: function() {
var ret = 0,
seq = this.inOuts.ofClips,
idx = 0;
for ( ; idx < seq.length; idx++ ) {
ret += seq[ idx ]["out"] - seq[ idx ]["in"] + 1;
}
return ret - 1;
},
play: function() {
this.playlist[ this.active ].play();
return this;
},
// Attach an event to a single point in time
exec: function ( time, fn ) {
var index = this.active;
this.inOuts.ofClips.forEach(function( off, idx ) {
if ( time >= off["in"] && time <= off["out"] ) {
index = idx;
}
});
//offsetBy = time - self.inOuts.ofVideos[ index ].in;
time += this.inOuts.ofVideos[ index ]["in"] - this.inOuts.ofClips[ index ]["in"];
// Creating a one second track event with an empty end
Popcorn.addTrackEvent( this.playlist[ index ], {
start: time - 1,
end: time,
_running: false,
_natives: {
start: fn || Popcorn.nop,
end: Popcorn.nop,
type: "exec"
}
});
return this;
},
// Binds event handlers that fire only when all
// videos in sequence have heard the event
listen: function( type, callback ) {
var self = this,
seq = this.playlist,
total = seq.length,
count = 0,
fnName;
if ( !callback ) {
callback = Popcorn.nop;
}
// Handling for DOM and Media events
if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) {
Popcorn.forEach( seq, function( video ) {
video.listen( type, function( event ) {
event.active = self;
if ( excludes.indexOf( type ) > -1 ) {
callback.call( video, event );
} else {
if ( ++count === total ) {
callback.call( video, event );
}
}
});
});
} else {
// If no events registered with this name, create a cache
if ( !this.events[ type ] ) {
this.events[ type ] = {};
}
// Normalize a callback name key
fnName = callback.name || Popcorn.guid( "__" + type );
// Store in event cache
this.events[ type ][ fnName ] = callback;
}
// Return the sequence object
return this;
},
unlisten: function( type, name ) {
// TODO: finish implementation
},
trigger: function( type, data ) {
var self = this;
// Handling for DOM and Media events
if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) {
// find the active video and trigger api events on that video.
return;
} else {
// Only proceed if there are events of this type
// currently registered on the sequence
if ( this.events[ type ] ) {
Popcorn.forEach( this.events[ type ], function( callback, name ) {
callback.call( self, { type: type }, data );
});
}
}
return this;
}
});
Popcorn.forEach( Popcorn.manifest, function( obj, plugin ) {
// Implement passthrough methods to plugins
Popcorn.sequence.prototype[ plugin ] = function( options ) {
// console.log( this, options );
var videos = {}, assignTo = [],
idx, off, inOuts, inIdx, outIdx, keys, clip, clipInOut, clipRange;
for ( idx = 0; idx < this.inOuts.ofClips.length; idx++ ) {
// store reference
off = this.inOuts.ofClips[ idx ];
// array to test against
inOuts = range( off["in"], off["out"] );
inIdx = inOuts.indexOf( options.start );
outIdx = inOuts.indexOf( options.end );
if ( inIdx > -1 ) {
videos[ idx ] = Popcorn.extend( {}, off, {
start: inOuts[ inIdx ],
clipIdx: inIdx
});
}
if ( outIdx > -1 ) {
videos[ idx ] = Popcorn.extend( {}, off, {
end: inOuts[ outIdx ],
clipIdx: outIdx
});
}
}
keys = Object.keys( videos ).map(function( val ) {
return +val;
});
assignTo = range( keys[ 0 ], keys[ 1 ] );
//console.log( "PLUGIN CALL MAPS: ", videos, keys, assignTo );
for ( idx = 0; idx < assignTo.length; idx++ ) {
var compile = {},
play = assignTo[ idx ],
vClip = videos[ play ];
if ( vClip ) {
// has instructions
clip = this.inOuts.ofVideos[ play ];
clipInOut = vClip.clipIdx;
clipRange = range( clip["in"], clip["out"] );
if ( vClip.start ) {
compile.start = clipRange[ clipInOut ];
compile.end = clipRange[ clipRange.length - 1 ];
}
if ( vClip.end ) {
compile.start = clipRange[ 0 ];
compile.end = clipRange[ clipInOut ];
}
//compile.start += 0.1;
//compile.end += 0.9;
} else {
compile.start = this.inOuts.ofVideos[ play ]["in"];
compile.end = this.inOuts.ofVideos[ play ]["out"];
//compile.start += 0.1;
//compile.end += 0.9;
}
// Handling full clip persistance
//if ( compile.start === compile.end ) {
//compile.start -= 0.1;
//compile.end += 0.9;
//}
// Call the plugin on the appropriate Popcorn object in the playlist
// Merge original options object & compiled (start/end) object into
// a new fresh object
this.playlist[ play ][ plugin ](
Popcorn.extend( {}, options, compile )
);
}
// Return the sequence object
return this;
};
});
})( this, Popcorn );
(function( Popcorn ) {
document.addEventListener( "DOMContentLoaded", function() {
// Supports non-specific elements
var dataAttr = "data-timeline-sources",
medias = document.querySelectorAll( "[" + dataAttr + "]" );
Popcorn.forEach( medias, function( idx, key ) {
var media = medias[ key ],
hasDataSources = false,
dataSources, data, popcornMedia;
// Ensure that the DOM has an id
if ( !media.id ) {
media.id = Popcorn.guid( "__popcorn" );
}
// Ensure we're looking at a dom node
if ( media.nodeType && media.nodeType === 1 ) {
popcornMedia = Popcorn( "#" + media.id );
dataSources = ( media.getAttribute( dataAttr ) || "" ).split( "," );
if ( dataSources[ 0 ] ) {
Popcorn.forEach( dataSources, function( source ) {
// split the parser and data as parser!file
data = source.split( "!" );
// if no parser is defined for the file, assume "parse" + file extension
if ( data.length === 1 ) {
// parse a relative URL for the filename, split to get extension
data = source.match( /(.*)[\/\\]([^\/\\]+\.\w+)$/ )[ 2 ].split( "." );
data[ 0 ] = "parse" + data[ 1 ].toUpperCase();
data[ 1 ] = source;
}
// If the media has data sources and the correct parser is registered, continue to load
if ( dataSources[ 0 ] && popcornMedia[ data[ 0 ] ] ) {
// Set up the media and load in the datasources
popcornMedia[ data[ 0 ] ]( data[ 1 ] );
}
});
}
// Only play the media if it was specified to do so
if ( !!popcornMedia.autoplay() ) {
popcornMedia.play();
}
}
});
}, false );
})( Popcorn );(function( global, Popcorn ) {
var navigator = global.navigator;
// Initialize locale data
// Based on http://en.wikipedia.org/wiki/Language_localisation#Language_tags_and_codes
function initLocale( arg ) {
var locale = typeof arg === "string" ? arg : [ arg.language, arg.region ].join( "-" ),
parts = locale.split( "-" );
// Setup locale data table
return {
iso6391: locale,
language: parts[ 0 ] || "",
region: parts[ 1 ] || ""
};
}
// Declare locale data table
var localeData = initLocale( navigator.userLanguage || navigator.language );
Popcorn.locale = {
// Popcorn.locale.get()
// returns reference to privately
// defined localeData
get: function() {
return localeData;
},
// Popcorn.locale.set( string|object );
set: function( arg ) {
localeData = initLocale( arg );
Popcorn.locale.broadcast();
return localeData;
},
// Popcorn.locale.broadcast( type )
// Sends events to all popcorn media instances that are
// listening for locale events
broadcast: function( type ) {
var instances = Popcorn.instances,
length = instances.length,
idx = 0,
instance;
type = type || "locale:changed";
// Iterate all current instances
for ( ; idx < length; idx++ ) {
instance = instances[ idx ];
// For those instances with locale event listeners,
// trigger a locale change event
if ( type in instance.data.events ) {
instance.trigger( type );
}
}
}
};
})( this, this.Popcorn );(function( Popcorn ) {
var
AP = Array.prototype,
OP = Object.prototype,
forEach = AP.forEach,
slice = AP.slice,
hasOwn = OP.hasOwnProperty,
toString = OP.toString;
// stores parsers keyed on filetype
Popcorn.parsers = {};
// An interface for extending Popcorn
// with parser functionality
Popcorn.parser = function( name, type, definition ) {
if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
Popcorn.error( "'" + name + "' is a protected function name" );
return;
}
// fixes parameters for overloaded function call
if ( typeof type === "function" && !definition ) {
definition = type;
type = "";
}
if ( typeof definition !== "function" || typeof type !== "string" ) {
return;
}
// Provides some sugar, but ultimately extends
// the definition into Popcorn.p
var natives = Popcorn.events.all,
parseFn,
parser = {};
parseFn = function( filename, callback ) {
if ( !filename ) {
return this;
}
var that = this;
Popcorn.xhr({
url: filename,
dataType: type,
success: function( data ) {
var tracksObject = definition( data ),
tracksData,
tracksDataLen,
tracksDef,
idx = 0;
tracksData = tracksObject.data || [];
tracksDataLen = tracksData.length;
tracksDef = null;
// If no tracks to process, return immediately
if ( !tracksDataLen ) {
return;
}
// Create tracks out of parsed object
for ( ; idx < tracksDataLen; idx++ ) {
tracksDef = tracksData[ idx ];
for ( var key in tracksDef ) {
if ( hasOwn.call( tracksDef, key ) && !!that[ key ] ) {
that[ key ]( tracksDef[ key ] );
}
}
}
if ( callback ) {
callback();
}
}
});
return this;
};
// Assign new named definition
parser[ name ] = parseFn;
// Extend Popcorn.p with new named definition
Popcorn.extend( Popcorn.p, parser );
// keys the function name by filetype extension
//Popcorn.parsers[ name ] = true;
return parser;
};
})( Popcorn );(function( Popcorn ) {
// combines calls of two function calls into one
var combineFn = function( first, second ) {
first = first || Popcorn.nop;
second = second || Popcorn.nop;
return function() {
first.apply( this, arguments );
second.apply( this, arguments );
};
};
// ID string matching
var rIdExp = /^(#([\w\-\_\.]+))$/;
var audioExtensions = "ogg|oga|aac|mp3|wav",
videoExtensions = "ogg|ogv|mp4|webm",
mediaExtensions = audioExtensions + "|" + videoExtensions;
var audioExtensionRegexp = new RegExp( "^.*\\.(" + audioExtensions + ")($|\\?)" ),
mediaExtensionRegexp = new RegExp( "^.*\\.(" + mediaExtensions + ")($|\\?)" );
Popcorn.player = function( name, player ) {
// return early if a player already exists under this name
if ( Popcorn[ name ] ) {
return;
}
player = player || {};
var playerFn = function( target, src, options ) {
options = options || {};
// List of events
var date = new Date() / 1000,
baselineTime = date,
currentTime = 0,
readyState = 0,
volume = 1,
muted = false,
events = {},
// The container div of the resource
container = typeof target === "string" ? Popcorn.dom.find( target ) : target,
basePlayer = {},
timeout,
popcorn;
if ( !Object.prototype.__defineGetter__ ) {
basePlayer = container || document.createElement( "div" );
}
// copies a div into the media object
for( var val in container ) {
// don't copy properties if using container as baseplayer
if ( val in basePlayer ) {
continue;
}
if ( typeof container[ val ] === "object" ) {
basePlayer[ val ] = container[ val ];
} else if ( typeof container[ val ] === "function" ) {
basePlayer[ val ] = (function( value ) {
// this is a stupid ugly kludgy hack in honour of Safari
// in Safari a NodeList is a function, not an object
if ( "length" in container[ value ] && !container[ value ].call ) {
return container[ value ];
} else {
return function() {
return container[ value ].apply( container, arguments );
};
}
}( val ));
} else {
Popcorn.player.defineProperty( basePlayer, val, {
get: (function( value ) {
return function() {
return container[ value ];
};
}( val )),
set: Popcorn.nop,
configurable: true
});
}
}
var timeupdate = function() {
date = new Date() / 1000;
if ( !basePlayer.paused ) {
basePlayer.currentTime = basePlayer.currentTime + ( date - baselineTime );
basePlayer.dispatchEvent( "timeupdate" );
timeout = setTimeout( timeupdate, 10 );
}
baselineTime = date;
};
basePlayer.play = function() {
this.paused = false;
if ( basePlayer.readyState >= 4 ) {
baselineTime = new Date() / 1000;
basePlayer.dispatchEvent( "play" );
timeupdate();
}
};
basePlayer.pause = function() {
this.paused = true;
basePlayer.dispatchEvent( "pause" );
};
Popcorn.player.defineProperty( basePlayer, "currentTime", {
get: function() {
return currentTime;
},
set: function( val ) {
// make sure val is a number
currentTime = +val;
basePlayer.dispatchEvent( "timeupdate" );
return currentTime;
},
configurable: true
});
Popcorn.player.defineProperty( basePlayer, "volume", {
get: function() {
return volume;
},
set: function( val ) {
// make sure val is a number
volume = +val;
basePlayer.dispatchEvent( "volumechange" );
return volume;
},
configurable: true
});
Popcorn.player.defineProperty( basePlayer, "muted", {
get: function() {
return muted;
},
set: function( val ) {
// make sure val is a number
muted = +val;
basePlayer.dispatchEvent( "volumechange" );
return muted;
},
configurable: true
});
Popcorn.player.defineProperty( basePlayer, "readyState", {
get: function() {
return readyState;
},
set: function( val ) {
readyState = val;
return readyState;
},
configurable: true
});
// Adds an event listener to the object
basePlayer.addEventListener = function( evtName, fn ) {
if ( !events[ evtName ] ) {
events[ evtName ] = [];
}
events[ evtName ].push( fn );
return fn;
};
// Removes an event listener from the object
basePlayer.removeEventListener = function( evtName, fn ) {
var i,
listeners = events[ evtName ];
if ( !listeners ){
return;
}
// walk backwards so we can safely splice
for ( i = events[ evtName ].length - 1; i >= 0; i-- ) {
if( fn === listeners[ i ] ) {
listeners.splice(i, 1);
}
}
return fn;
};
// Can take event object or simple string
basePlayer.dispatchEvent = function( oEvent ) {
var evt,
self = this,
eventInterface,
eventName = oEvent.type;
// A string was passed, create event object
if ( !eventName ) {
eventName = oEvent;
eventInterface = Popcorn.events.getInterface( eventName );
if ( eventInterface ) {
evt = document.createEvent( eventInterface );
evt.initEvent( eventName, true, true, window, 1 );
}
}
if ( events[ eventName ] ) {
for ( var i = events[ eventName ].length - 1; i >= 0; i-- ) {
events[ eventName ][ i ].call( self, evt, self );
}
}
};
// Attempt to get src from playerFn parameter
basePlayer.src = src || "";
basePlayer.duration = 0;
basePlayer.paused = true;
basePlayer.ended = 0;
options && options.events && Popcorn.forEach( options.events, function( val, key ) {
basePlayer.addEventListener( key, val, false );
});
// true and undefined returns on canPlayType means we should attempt to use it,
// false means we cannot play this type
if ( player._canPlayType( container.nodeName, src ) !== false ) {
if ( player._setup ) {
player._setup.call( basePlayer, options );
} else {
// there is no setup, which means there is nothing to load
basePlayer.readyState = 4;
basePlayer.dispatchEvent( "loadedmetadata" );
basePlayer.dispatchEvent( "loadeddata" );
basePlayer.dispatchEvent( "canplaythrough" );
}
} else {
// Asynchronous so that users can catch this event
setTimeout( function() {
basePlayer.dispatchEvent( "error" );
}, 0 );
}
popcorn = new Popcorn.p.init( basePlayer, options );
if ( player._teardown ) {
popcorn.destroy = combineFn( popcorn.destroy, function() {
player._teardown.call( basePlayer, options );
});
}
return popcorn;
};
playerFn.canPlayType = player._canPlayType = player._canPlayType || Popcorn.nop;
Popcorn[ name ] = Popcorn.player.registry[ name ] = playerFn;
};
Popcorn.player.registry = {};
Popcorn.player.defineProperty = Object.defineProperty || function( object, description, options ) {
object.__defineGetter__( description, options.get || Popcorn.nop );
object.__defineSetter__( description, options.set || Popcorn.nop );
};
// player queue is to help players queue things like play and pause
// HTML5 video's play and pause are asynch, but do fire in sequence
// play() should really mean "requestPlay()" or "queuePlay()" and
// stash a callback that will play the media resource when it's ready to be played
Popcorn.player.playerQueue = function() {
var _queue = [],
_running = false;
return {
next: function() {
_running = false;
_queue.shift();
_queue[ 0 ] && _queue[ 0 ]();
},
add: function( callback ) {
_queue.push(function() {
_running = true;
callback && callback();
});
// if there is only one item on the queue, start it
!_running && _queue[ 0 ]();
}
};
};
// smart will attempt to find you a match, if it does not find a match,
// it will attempt to create a video element with the source,
// if that failed, it will throw.
Popcorn.smart = function( target, src, options ) {
var playerType,
elementTypes = [ "AUDIO", "VIDEO" ],
sourceNode,
firstSrc,
node = Popcorn.dom.find( target ),
i, srcResult,
canPlayTypeTester = document.createElement( "video" ),
canPlayTypes = {
"ogg": "video/ogg",
"ogv": "video/ogg",
"oga": "audio/ogg",
"webm": "video/webm",
"mp4": "video/mp4",
"mp3": "audio/mp3"
};
var canPlayType = function( type ) {
return canPlayTypeTester.canPlayType( canPlayTypes[ type ] );
};
var canPlaySrc = function( src ) {
srcResult = mediaExtensionRegexp.exec( src );
if ( !srcResult || !srcResult[ 1 ] ) {
return false;
}
return canPlayType( srcResult[ 1 ] );
};
if ( !node ) {
Popcorn.error( "Specified target " + target + " was not found." );
return;
}
// For when no src is defined.
// Usually this is a video element with a src already on it.
if ( elementTypes.indexOf( node.nodeName ) > -1 && !src ) {
if ( typeof src === "object" ) {
options = src;
src = undefined;
}
return Popcorn( node, options );
}
// if our src is not an array, create an array of one.
if ( typeof( src ) === "string" ) {
src = [ src ];
}
// go through each src, and find the first playable.
// this only covers player sources popcorn knows of,
// and not things like a youtube src that is private.
// it will still consider a private youtube video to be playable.
for ( i = 0, srcLength = src.length; i < srcLength; i++ ) {
// src is a playable HTML5 video, we don't need to check custom players.
if ( canPlaySrc( src[ i ] ) ) {
src = src[ i ];
break;
}
// for now we loop through and use the first valid player we find.
for ( var key in Popcorn.player.registry ) {
if ( Popcorn.player.registry.hasOwnProperty( key ) ) {
if ( Popcorn.player.registry[ key ].canPlayType( node.nodeName, src[ i ] ) ) {
// Popcorn.smart( player, src, /* options */ )
return Popcorn[ key ]( node, src[ i ], options );
}
}
}
}
// Popcorn.smart( div, src, /* options */ )
// attempting to create a video in a container
if ( elementTypes.indexOf( node.nodeName ) === -1 ) {
firstSrc = typeof( src ) === "string" ? src : src.length ? src[ 0 ] : src;
target = document.createElement( !!audioExtensionRegexp.exec( firstSrc ) ? elementTypes[ 0 ] : elementTypes[ 1 ] );
// Controls are defaulted to being present
target.controls = true;
node.appendChild( target );
node = target;
}
options && options.events && options.events.error && node.addEventListener( "error", options.events.error, false );
node.src = src;
return Popcorn( node, options );
};
})( Popcorn );
// PLUGIN: mediaspawner
/**
* mediaspawner Popcorn Plugin.
* Adds Video/Audio to the page using Popcorns players
* Start is the time that you want this plug-in to execute
* End is the time that you want this plug-in to stop executing
*
* @param {HTML} options
*
* Example:
var p = Popcorn('#video')
.mediaspawner( {
source: "http://www.youtube.com/watch?v=bUB1L3zGVvc",
target: "mediaspawnerdiv",
start: 1,
end: 10,
caption: "This is a test. We are assuming conrol. We are assuming control."
})
*
*/
(function ( Popcorn, global ) {
var PLAYER_URL = "http://popcornjs.org/code/modules/player/popcorn.player.js",
urlRegex = /(?:http:\/\/www\.|http:\/\/|www\.|\.|^)(youtu|vimeo|soundcloud|baseplayer)/,
forEachPlayer,
playerTypeLoading = {},
playerTypesLoaded = {
"vimeo": false,
"youtube": false,
"soundcloud": false,
"module": false
};
Object.defineProperty( playerTypeLoading, forEachPlayer, {
get: function() {
return playerTypesLoaded[ forEachPlayer ];
},
set: function( val ) {
playerTypesLoaded[ forEachPlayer ] = val;
}
});
Popcorn.plugin( "mediaspawner", {
manifest: {
about: {
name: "Popcorn Media Spawner Plugin",
version: "0.1",
author: "Matthew Schranz, @mjschranz",
website: "mschranz.wordpress.com"
},
options: {
source: {
elem: "input",
type: "text",
label: "Media Source",
"default": "http://www.youtube.com/watch?v=CXDstfD9eJ0"
},
caption: {
elem: "input",
type: "text",
label: "Media Caption",
"default": "Popcorn Popping",
optional: true
},
target: "mediaspawner-container",
start: {
elem: "input",
type: "number",
label: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
autoplay: {
elem: "input",
type: "checkbox",
label: "Autoplay Video",
optional: true
},
width: {
elem: "input",
type: "number",
label: "Media Width",
"default": 400,
units: "px",
optional: true
},
height: {
elem: "input",
type: "number",
label: "Media Height",
"default": 200,
units: "px",
optional: true
}
}
},
_setup: function( options ) {
var target = document.getElementById( options.target ) || {},
mediaType,
container,
capContainer,
regexResult;
regexResult = urlRegex.exec( options.source );
if ( regexResult ) {
mediaType = regexResult[ 1 ];
// our regex only handles youtu ( incase the url looks something like youtu.be )
if ( mediaType === "youtu" ) {
mediaType = "youtube";
}
}
else {
// if the regex didn't return anything we know it's an HTML5 source
mediaType = "HTML5";
}
// Store Reference to Type for use in end
options._type = mediaType;
// Create separate container for plugin
options._container = document.createElement( "div" );
container = options._container;
container.id = "mediaSpawnerdiv-" + Popcorn.guid();
// Default width and height of media
options.width = options.width || 400;
options.height = options.height || 200;
// Captions now need to be in their own container, due to the problem with flash players
// described in start/end
if ( options.caption ) {
capContainer = document.createElement( "div" );
capContainer.innerHTML = options.caption;
capContainer.style.display = "none";
options._capCont = capContainer;
container.appendChild( capContainer );
}
target && target.appendChild( container );
function constructMedia(){
function checkPlayerTypeLoaded() {
if ( mediaType !== "HTML5" && !window.Popcorn[ mediaType ] ) {
setTimeout( function() {
checkPlayerTypeLoaded();
}, 300 );
} else {
options.id = options._container.id;
// Set the width/height of the container before calling Popcorn.smart
// Allows youtube to pickup on the specified height an create the player
// with specified dimensions
options._container.style.width = options.width + "px";
options._container.style.height = options.height + "px";
options.popcorn = Popcorn.smart( "#" + options.id, options.source );
if ( mediaType === "HTML5" ) {
options.popcorn.controls( true );
}
// Set them to 0 now so it is hidden
options._container.style.width = "0px";
options._container.style.height = "0px";
options._container.style.visibility = "hidden";
options._container.style.overflow = "hidden";
}
}
if ( mediaType !== "HTML5" && !window.Popcorn[ mediaType ] && !playerTypeLoading[ mediaType ] ) {
playerTypeLoading[ mediaType ] = true;
Popcorn.getScript( "http://popcornjs.org/code/players/" + mediaType + "/popcorn." + mediaType + ".js", function() {
checkPlayerTypeLoaded();
});
}
else {
checkPlayerTypeLoaded();
}
}
// If Player script needed to be loaded, keep checking until it is and then fire readycallback
function isPlayerReady() {
if ( !window.Popcorn.player ) {
setTimeout( function () {
isPlayerReady();
}, 300 );
} else {
constructMedia();
}
}
// If player script isn't present, retrieve script
if ( !window.Popcorn.player && !playerTypeLoading.module ) {
playerTypeLoading.module = true;
Popcorn.getScript( PLAYER_URL, isPlayerReady );
} else {
isPlayerReady();
}
},
start: function( event, options ) {
if( options._capCont ) {
options._capCont.style.display = "";
}
/* Using this style for Start/End is required because of the flash players
* Without it on end an internal cleanup is called, causing the flash players
* to be out of sync with Popcorn, as they are then rebuilt.
*/
options._container.style.width = options.width + "px";
options._container.style.height = options.height + "px";
options._container.style.visibility = "visible";
options._container.style.overflow = "visible";
if ( options.autoplay ) {
options.popcorn.play();
}
},
end: function( event, options ) {
if( options._capCont ) {
options._capCont.style.display = "none";
}
/* Using this style for Start/End is required because of the flash players
* Without it on end an internal cleanup is called, causing the flash players
* to be out of sync with Popcorn, as they are then rebuilt.
*/
options._container.style.width = "0px";
options._container.style.height = "0px";
options._container.style.visibility = "hidden";
options._container.style.overflow = "hidden";
// Pause all popcorn instances on exit
options.popcorn.pause();
},
_teardown: function( options ) {
if ( options.popcorn && options.popcorn.destory ) {
options.popcorn.destroy();
}
document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( options._container );
}
});
})( Popcorn, this );
// PLUGIN: Code
(function ( Popcorn ) {
/**
* Code Popcorn Plug-in
*
* Adds the ability to run arbitrary code (JavaScript functions) according to video timing.
*
* @param {Object} options
*
* Required parameters: start, end, template, data, and target.
* Optional parameter: static.
*
* start: the time in seconds when the mustache template should be rendered
* in the target div.
*
* end: the time in seconds when the rendered mustache template should be
* removed from the target div.
*
* onStart: the function to be run when the start time is reached.
*
* onFrame: [optional] a function to be run on each paint call
* (e.g., called ~60 times per second) between the start and end times.
*
* onEnd: [optional] a function to be run when the end time is reached.
*
* Example:
var p = Popcorn('#video')
// onStart function only
.code({
start: 1,
end: 4,
onStart: function( options ) {
// called on start
}
})
// onStart + onEnd only
.code({
start: 6,
end: 8,
onStart: function( options ) {
// called on start
},
onEnd: function ( options ) {
// called on end
}
})
// onStart, onEnd, onFrame
.code({
start: 10,
end: 14,
onStart: function( options ) {
// called on start
},
onFrame: function ( options ) {
// called on every paint frame between start and end.
// uses mozRequestAnimationFrame, webkitRequestAnimationFrame,
// or setTimeout with 16ms window.
},
onEnd: function ( options ) {
// called on end
}
});
*
*/
Popcorn.plugin( "code" , function( options ) {
var running = false,
instance = this;
// Setup a proper frame interval function (60fps), favouring paint events.
var step = (function() {
var buildFrameRunner = function( runner ) {
return function( f, options ) {
var _f = function() {
running && f.call( instance, options );
running && runner( _f );
};
_f();
};
};
// Figure out which level of browser support we have for this
if ( window.webkitRequestAnimationFrame ) {
return buildFrameRunner( window.webkitRequestAnimationFrame );
} else if ( window.mozRequestAnimationFrame ) {
return buildFrameRunner( window.mozRequestAnimationFrame );
} else {
return buildFrameRunner( function( f ) {
window.setTimeout( f, 16 );
});
}
})();
if ( !options.onStart || typeof options.onStart !== "function" ) {
options.onStart = Popcorn.nop;
}
if ( options.onEnd && typeof options.onEnd !== "function" ) {
options.onEnd = undefined;
}
if ( options.onFrame && typeof options.onFrame !== "function" ) {
options.onFrame = undefined;
}
return {
start: function( event, options ) {
options.onStart.call( instance, options );
if ( options.onFrame ) {
running = true;
step( options.onFrame, options );
}
},
end: function( event, options ) {
if ( options.onFrame ) {
running = false;
}
if ( options.onEnd ) {
options.onEnd.call( instance, options );
}
}
};
},
{
about: {
name: "Popcorn Code Plugin",
version: "0.1",
author: "David Humphrey (@humphd)",
website: "http://vocamus.net/dave"
},
options: {
start: {
elem: "input",
type: "number",
label: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
onStart: {
elem: "input",
type: "function",
label: "onStart"
},
onFrame: {
elem: "input",
type: "function",
label: "onFrame",
optional: true
},
onEnd: {
elem: "input",
type: "function",
label: "onEnd"
}
}
});
})( Popcorn );
// PLUGIN: Flickr
(function (Popcorn) {
/**
* Flickr popcorn plug-in
* Appends a users Flickr images to an element on the page.
* Options parameter will need a start, end, target and userid or username and api_key.
* Optional parameters are numberofimages, height, width, padding, and border
* Start is the time that you want this plug-in to execute (in seconds)
* End is the time that you want this plug-in to stop executing (in seconds)
* Userid is the id of who's Flickr images you wish to show
* Tags is a mutually exclusive list of image descriptor tags
* Username is the username of who's Flickr images you wish to show
* using both userid and username is redundant
* an api_key is required when using username
* Apikey is your own api key provided by Flickr
* Target is the id of the document element that the images are
* appended to, this target element must exist on the DOM
* Numberofimages specify the number of images to retreive from flickr, defaults to 4
* Height the height of the image, defaults to '50px'
* Width the width of the image, defaults to '50px'
* Padding number of pixels between images, defaults to '5px'
* Border border size in pixels around images, defaults to '0px'
*
* @param {Object} options
*
* Example:
var p = Popcorn('#video')
.flickr({
start: 5, // seconds, mandatory
end: 15, // seconds, mandatory
userid: '35034346917@N01', // optional
tags: 'dogs,cats', // optional
numberofimages: '8', // optional
height: '50px', // optional
width: '50px', // optional
padding: '5px', // optional
border: '0px', // optional
target: 'flickrdiv' // mandatory
} )
*
*/
var idx = 0;
Popcorn.plugin( "flickr" , function( options ) {
var containerDiv,
target = document.getElementById( options.target ),
_userid,
_uri,
_link,
_image,
_count = options.numberofimages || 4,
_height = options.height || "50px",
_width = options.width || "50px",
_padding = options.padding || "5px",
_border = options.border || "0px";
// create a new div this way anything in the target div is left intact
// this is later populated with Flickr images
containerDiv = document.createElement( "div" );
containerDiv.id = "flickr" + idx;
containerDiv.style.width = "100%";
containerDiv.style.height = "100%";
containerDiv.style.display = "none";
idx++;
target && target.appendChild( containerDiv );
// get the userid from Flickr API by using the username and apikey
var isUserIDReady = function() {
if ( !_userid ) {
_uri = "http://api.flickr.com/services/rest/?method=flickr.people.findByUsername&";
_uri += "username=" + options.username + "&api_key=" + options.apikey + "&format=json&jsoncallback=flickr";
Popcorn.getJSONP( _uri, function( data ) {
_userid = data.user.nsid;
getFlickrData();
});
} else {
setTimeout(function () {
isUserIDReady();
}, 5 );
}
};
// get the photos from Flickr API by using the user_id and/or tags
var getFlickrData = function() {
_uri = "http://api.flickr.com/services/feeds/photos_public.gne?";
if ( _userid ) {
_uri += "id=" + _userid + "&";
}
if ( options.tags ) {
_uri += "tags=" + options.tags + "&";
}
_uri += "lang=en-us&format=json&jsoncallback=flickr";
Popcorn.xhr.getJSONP( _uri, function( data ) {
var fragment = document.createElement( "div" );
fragment.innerHTML = "<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: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
userid: {
elem: "input",
type: "text",
label: "User ID",
optional: true
},
tags: {
elem: "input",
type: "text",
label: "Tags"
},
username: {
elem: "input",
type: "text",
label: "Username",
optional: true
},
apikey: {
elem: "input",
type: "text",
label: "API Key",
optional: true
},
target: "flickr-container",
height: {
elem: "input",
type: "text",
label: "Height",
"default": "50px",
optional: true
},
width: {
elem: "input",
type: "text",
label: "Width",
"default": "50px",
optional: true
},
padding: {
elem: "input",
type: "text",
label: "Padding",
optional: true
},
border: {
elem: "input",
type: "text",
label: "Border",
"default": "5px",
optional: true
},
numberofimages: {
elem: "input",
type: "number",
"default": 4,
label: "Number of Images"
}
}
});
})( Popcorn );
// PLUGIN: Footnote/Text
(function ( Popcorn ) {
/**
* Footnote popcorn plug-in
* Adds text to an element on the page.
* Options parameter will need a start, end, target and text.
* Start is the time that you want this plug-in to execute
* End is the time that you want this plug-in to stop executing
* Text is the text that you want to appear in the target
* Target is the id of the document element that the text needs to be
* attached to, this target element must exist on the DOM
*
* @param {Object} options
*
* Example:
* var p = Popcorn('#video')
* .footnote({
* start: 5, // seconds
* end: 15, // seconds
* text: 'This video made exclusively for drumbeat.org',
* target: 'footnotediv'
* });
**/
Popcorn.plugin( "footnote", {
manifest: {
about: {
name: "Popcorn Footnote Plugin",
version: "0.2",
author: "@annasob, @rwaldron",
website: "annasob.wordpress.com"
},
options: {
start: {
elem: "input",
type: "number",
label: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
text: {
elem: "input",
type: "text",
label: "Text"
},
target: "footnote-container"
}
},
_setup: function( options ) {
var target = Popcorn.dom.find( options.target );
options._container = document.createElement( "div" );
options._container.style.display = "none";
options._container.innerHTML = options.text;
target.appendChild( options._container );
},
/**
* @member footnote
* The start function will be executed when the currentTime
* of the video reaches the start time provided by the
* options variable
*/
start: function( event, options ){
options._container.style.display = "inline";
},
/**
* @member footnote
* The end function will be executed when the currentTime
* of the video reaches the end time provided by the
* options variable
*/
end: function( event, options ){
options._container.style.display = "none";
},
_teardown: function( options ) {
var target = Popcorn.dom.find( options.target );
if ( target ) {
target.removeChild( options._container );
}
}
});
})( Popcorn );
// PLUGIN: Text
(function ( Popcorn ) {
/**
* Text Popcorn plug-in
*
* Places text in an element on the page. Plugin options include:
* Options parameter will need a start, end.
* Start: Is the time that you want this plug-in to execute
* End: Is the time that you want this plug-in to stop executing
* Text: Is the text that you want to appear in the target
* Escape: {true|false} Whether to escape the text (e.g., html strings)
* Multiline: {true|false} Whether newlines should be turned into <br>s
* Target: Is the ID of the element where the text should be placed. An empty target
* will be placed on top of the media element
*
* @param {Object} options
*
* Example:
* var p = Popcorn('#video')
*
* // Simple text
* .text({
* start: 5, // seconds
* end: 15, // seconds
* text: 'This video made exclusively for drumbeat.org',
* target: 'textdiv'
* })
*
* // HTML text, rendered as HTML
* .text({
* start: 15, // seconds
* end: 20, // seconds
* text: '<p>This video made <em>exclusively</em> for drumbeat.org</p>',
* target: 'textdiv'
* })
*
* // HTML text, escaped and rendered as plain text
* .text({
* start: 20, // seconds
* end: 25, // seconds
* text: 'This is an HTML p element: <p>paragraph</p>',
* escape: true,
* target: 'textdiv'
* })
*
* // Multi-Line HTML text, escaped and rendered as plain text
* .text({
* start: 25, // seconds
* end: 30, // seconds
* text: 'This is an HTML p element: <p>paragraph</p>\nThis is an HTML b element: <b>bold</b>',
* escape: true,
* multiline: true,
* target: 'textdiv'
* });
*
* // Subtitle text
* .text({
* start: 30, // seconds
* end: 40, // seconds
* text: 'This will be overlayed on the video',
* })
**/
/**
* HTML escape code from mustache.js, used under MIT Licence
* https://github.com/janl/mustache.js/blob/master/mustache.js
**/
var escapeMap = {
"&": "&",
"<": "<",
">": ">",
'"': '"',
"'": '''
};
function escapeHTML( string, multiline ) {
return String( string ).replace( /&(?!\w+;)|[<>"']/g, function ( s ) {
return escapeMap[ s ] || s;
});
}
function newlineToBreak( string ) {
// Deal with both \r\n and \n
return string.replace( /\r?\n/gm, "<br>" );
}
// Subtitle specific functionality
function createSubtitleContainer( context, id ) {
var ctxContainer = context.container = document.createElement( "div" ),
style = ctxContainer.style,
media = context.media;
var updatePosition = function() {
var position = context.position();
// the video element must have height and width defined
style.fontSize = "18px";
style.width = media.offsetWidth + "px";
style.top = position.top + media.offsetHeight - ctxContainer.offsetHeight - 40 + "px";
style.left = position.left + "px";
setTimeout( updatePosition, 10 );
};
ctxContainer.id = id || "";
style.position = "absolute";
style.color = "white";
style.textShadow = "black 2px 2px 6px";
style.fontWeight = "bold";
style.textAlign = "center";
updatePosition();
context.media.parentNode.appendChild( ctxContainer );
return ctxContainer;
}
Popcorn.plugin( "text", {
manifest: {
about: {
name: "Popcorn Text Plugin",
version: "0.1",
author: "@humphd"
},
options: {
start: {
elem: "input",
type: "number",
label: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
text: {
elem: "input",
type: "text",
label: "Text",
"default": "Popcorn.js"
},
escape: {
elem: "input",
type: "checkbox",
label: "Escape"
},
multiline: {
elem: "input",
type: "checkbox",
label: "Multiline"
}
}
},
_setup: function( options ) {
var target,
text,
container = options._container = document.createElement( "div" );
container.style.display = "none";
if ( options.target ) {
// Try to use supplied target
target = Popcorn.dom.find( options.target );
if ( !target ) {
target = createSubtitleContainer( this, options.target );
}
else if ( [ "VIDEO", "AUDIO" ].indexOf( target.nodeName ) > -1 ) {
target = createSubtitleContainer( this, options.target + "-overlay" );
}
} else if ( !this.container ) {
// Creates a div for all subtitles to use
target = createSubtitleContainer( this );
} else {
// Use subtitle container
target = this.container;
}
// cache reference to actual target container
options._target = target;
// Escape HTML text if requested
text = !!options.escape ? escapeHTML( options.text ) :
options.text;
// Swap newline for <br> if requested
text = !!options.multiline ? newlineToBreak ( text ) : text;
container.innerHTML = text || "";
target.appendChild( container );
},
/**
* @member text
* The start function will be executed when the currentTime
* of the video reaches the start time provided by the
* options variable
*/
start: function( event, options ) {
options._container.style.display = "inline";
},
/**
* @member text
* The end function will be executed when the currentTime
* of the video reaches the end time provided by the
* options variable
*/
end: function( event, options ) {
options._container.style.display = "none";
},
_teardown: function( options ) {
var target = options._target;
if ( target ) {
target.removeChild( options._container );
}
}
});
})( Popcorn );
// PLUGIN: Google Maps
var googleCallback;
(function ( Popcorn ) {
var i = 1,
_mapFired = false,
_mapLoaded = false,
geocoder, loadMaps;
//google api callback
googleCallback = function ( data ) {
// ensure all of the maps functions needed are loaded
// before setting _maploaded to true
if ( typeof google !== "undefined" && google.maps && google.maps.Geocoder && google.maps.LatLng ) {
geocoder = new google.maps.Geocoder();
Popcorn.getScript( "//maps.stamen.com/js/tile.stamen.js", function() {
_mapLoaded = true;
});
} else {
setTimeout(function () {
googleCallback( data );
}, 1);
}
};
// function that loads the google api
loadMaps = function () {
// for some reason the Google Map API adds content to the body
if ( document.body ) {
_mapFired = true;
Popcorn.getScript( "//maps.google.com/maps/api/js?sensor=false&callback=googleCallback" );
} else {
setTimeout(function () {
loadMaps();
}, 1);
}
};
function buildMap( options, location, mapDiv ) {
var type = options.type ? options.type.toUpperCase() : "HYBRID",
layer;
// See if we need to make a custom Stamen map layer
if ( type === "STAMEN-WATERCOLOR" ||
type === "STAMEN-TERRAIN" ||
type === "STAMEN-TONER" ) {
// Stamen types are lowercase
layer = type.replace("STAMEN-", "").toLowerCase();
}
var map = new google.maps.Map( mapDiv, {
// If a custom layer was specified, use that, otherwise use type
mapTypeId: layer ? layer : google.maps.MapTypeId[ type ],
// Hide the layer selection UI
mapTypeControlOptions: { mapTypeIds: [] }
});
if ( layer ) {
map.mapTypes.set( layer, new google.maps.StamenMapType( layer ));
}
map.getDiv().style.display = "none";
return map;
}
/**
* googlemap popcorn plug-in
* Adds a map to the target div centered on the location specified by the user
* Options parameter will need a start, end, target, type, zoom, lat and lng, and location
* -Start is the time that you want this plug-in to execute
* -End is the time that you want this plug-in to stop executing
* -Target is the id of the DOM element that you want the map to appear in. This element must be in the DOM
* -Type [optional] either: HYBRID (default), ROADMAP, SATELLITE, TERRAIN, STREETVIEW, or one of the
* Stamen custom map types (http://http://maps.stamen.com): STAMEN-TONER,
* STAMEN-WATERCOLOR, or STAMEN-TERRAIN.
* -Zoom [optional] defaults to 0
* -Heading [optional] STREETVIEW orientation of camera in degrees relative to true north (0 north, 90 true east, ect)
* -Pitch [optional] STREETVIEW vertical orientation of the camera (between 1 and 3 is recommended)
* -Lat and Lng: the coordinates of the map must be present if location is not specified.
* -Height [optional] the height of the map, in "px" or "%". Defaults to "100%".
* -Width [optional] the width of the map, in "px" or "%". Defaults to "100%".
* -Location: the adress you want the map to display, must be present if lat and lng are not specified.
* Note: using location requires extra loading time, also not specifying both lat/lng and location will
* cause and error.
*
* Tweening works using the following specifications:
* -location is the start point when using an auto generated route
* -tween when used in this context is a string which specifies the end location for your route
* Note that both location and tween must be present when using an auto generated route, or the map will not tween
* -interval is the speed in which the tween will be executed, a reasonable time is 1000 ( time in milliseconds )
* Heading, Zoom, and Pitch streetview values are also used in tweening with the autogenerated route
*
* -tween is an array of objects, each containing data for one frame of a tween
* -position is an object with has two paramaters, lat and lng, both which are mandatory for a tween to work
* -pov is an object which houses heading, pitch, and zoom paramters, which are all optional, if undefined, these values default to 0
* -interval is the speed in which the tween will be executed, a reasonable time is 1000 ( time in milliseconds )
*
* @param {Object} options
*
* Example:
var p = Popcorn("#video")
.googlemap({
start: 5, // seconds
end: 15, // seconds
type: "ROADMAP",
target: "map",
lat: 43.665429,
lng: -79.403323
} )
*
*/
Popcorn.plugin( "googlemap", function ( options ) {
var newdiv, map, location,
target = document.getElementById( options.target );
options.type = options.type || "ROADMAP";
options.zoom = options.zoom || 1;
options.lat = options.lat || 0;
options.lng = options.lng || 0;
// if this is the firest time running the plugins
// call the function that gets the sctipt
if ( !_mapFired ) {
loadMaps();
}
// create a new div this way anything in the target div is left intact
// this is later passed on to the maps api
newdiv = document.createElement( "div" );
newdiv.id = "actualmap" + i;
newdiv.style.width = options.width || "100%";
// height is a little more complicated than width.
if ( options.height ) {
newdiv.style.height = options.height;
} else if ( target && target.clientHeight ) {
newdiv.style.height = target.clientHeight + "px";
} else {
newdiv.style.height = "100%";
}
i++;
target && target.appendChild( newdiv );
// ensure that google maps and its functions are loaded
// before setting up the map parameters
var isMapReady = function () {
if ( _mapLoaded ) {
if ( newdiv ) {
if ( options.location ) {
// calls an anonymous google function called on separate thread
geocoder.geocode({
"address": options.location
}, function ( results, status ) {
// second check for newdiv since it could have disappeared before
// this callback is actual run
if ( newdiv && status === google.maps.GeocoderStatus.OK ) {
options.lat = results[ 0 ].geometry.location.lat();
options.lng = results[ 0 ].geometry.location.lng();
location = new google.maps.LatLng( options.lat, options.lng );
map = buildMap( options, location, newdiv );
}
});
} else {
location = new google.maps.LatLng( options.lat, options.lng );
map = buildMap( options, location, newdiv );
}
}
} else {
setTimeout(function () {
isMapReady();
}, 5);
}
};
isMapReady();
return {
/**
* @member webpage
* The start function will be executed when the currentTime
* of the video reaches the start time provided by the
* options variable
*/
start: function( event, options ) {
var that = this,
sView;
// ensure the map has been initialized in the setup function above
var isMapSetup = function() {
if ( map ) {
options._map = map;
map.getDiv().style.display = "block";
// reset the location and zoom just in case the user plaid with the map
google.maps.event.trigger( map, "resize" );
map.setCenter( location );
// make sure options.zoom is a number
if ( options.zoom && typeof options.zoom !== "number" ) {
options.zoom = +options.zoom;
}
map.setZoom( options.zoom );
//Make sure heading is a number
if ( options.heading && typeof options.heading !== "number" ) {
options.heading = +options.heading;
}
//Make sure pitch is a number
if ( options.pitch && typeof options.pitch !== "number" ) {
options.pitch = +options.pitch;
}
if ( options.type === "STREETVIEW" ) {
// Switch this map into streeview mode
map.setStreetView(
// Pass a new StreetViewPanorama instance into our map
sView = new google.maps.StreetViewPanorama( newdiv, {
position: location,
pov: {
heading: options.heading = options.heading || 0,
pitch: options.pitch = options.pitch || 0,
zoom: options.zoom
}
})
);
// Function to handle tweening using a set timeout
var tween = function( rM, t ) {
var computeHeading = google.maps.geometry.spherical.computeHeading;
setTimeout(function() {
var current_time = that.media.currentTime;
// Checks whether this is a generated route or not
if ( typeof options.tween === "object" ) {
for ( var i = 0, m = rM.length; i < m; i++ ) {
var waypoint = rM[ i ];
// Checks if this position along the tween should be displayed or not
if ( current_time >= ( waypoint.interval * ( i + 1 ) ) / 1000 &&
( current_time <= ( waypoint.interval * ( i + 2 ) ) / 1000 ||
current_time >= waypoint.interval * ( m ) / 1000 ) ) {
sView3.setPosition( new google.maps.LatLng( waypoint.position.lat, waypoint.position.lng ) );
sView3.setPov({
heading: waypoint.pov.heading || computeHeading( waypoint, rM[ i + 1 ] ) || 0,
zoom: waypoint.pov.zoom || 0,
pitch: waypoint.pov.pitch || 0
});
}
}
// Calls the tween function again at the interval set by the user
tween( rM, rM[ 0 ].interval );
} else {
for ( var k = 0, l = rM.length; k < l; k++ ) {
var interval = options.interval;
if( current_time >= (interval * ( k + 1 ) ) / 1000 &&
( current_time <= (interval * ( k + 2 ) ) / 1000 ||
current_time >= interval * ( l ) / 1000 ) ) {
sView2.setPov({
heading: computeHeading( rM[ k ], rM[ k + 1 ] ) || 0,
zoom: options.zoom,
pitch: options.pitch || 0
});
sView2.setPosition( checkpoints[ k ] );
}
}
tween( checkpoints, options.interval );
}
}, t );
};
// Determines if we should use hardcoded values ( using options.tween ),
// or if we should use a start and end location and let google generate
// the route for us
if ( options.location && typeof options.tween === "string" ) {
// Creating another variable to hold the streetview map for tweening,
// Doing this because if there was more then one streetview map, the tweening would sometimes appear in other maps
var sView2 = sView;
// Create an array to store all the lat/lang values along our route
var checkpoints = [];
// Creates a new direction service, later used to create a route
var directionsService = new google.maps.DirectionsService();
// Creates a new direction renderer using the current map
// This enables us to access all of the route data that is returned to us
var directionsDisplay = new google.maps.DirectionsRenderer( sView2 );
var request = {
origin: options.location,
destination: options.tween,
travelMode: google.maps.TravelMode.DRIVING
};
// Create the route using the direction service and renderer
directionsService.route( request, function( response, status ) {
if ( status == google.maps.DirectionsStatus.OK ) {
directionsDisplay.setDirections( response );
showSteps( response, that );
}
});
var showSteps = function ( directionResult, that ) {
// Push new google map lat and lng values into an array from our list of lat and lng values
var routes = directionResult.routes[ 0 ].overview_path;
for ( var j = 0, k = routes.length; j < k; j++ ) {
checkpoints.push( new google.maps.LatLng( routes[ j ].lat(), routes[ j ].lng() ) );
}
// Check to make sure the interval exists, if not, set to a default of 1000
options.interval = options.interval || 1000;
tween( checkpoints, 10 );
};
} else if ( typeof options.tween === "object" ) {
// Same as the above to stop streetview maps from overflowing into one another
var sView3 = sView;
for ( var i = 0, l = options.tween.length; i < l; i++ ) {
// Make sure interval exists, if not, set to 1000
options.tween[ i ].interval = options.tween[ i ].interval || 1000;
tween( options.tween, 10 );
}
}
}
if ( options.onmaploaded ) {
options.onmaploaded( options, map );
}
} else {
setTimeout(function () {
isMapSetup();
}, 13);
}
};
isMapSetup();
},
/**
* @member webpage
* The end function will be executed when the currentTime
* of the video reaches the end time provided by the
* options variable
*/
end: function ( event, options ) {
// if the map exists hide it do not delete the map just in
// case the user seeks back to time b/w start and end
if ( map ) {
map.getDiv().style.display = "none";
}
},
_teardown: function ( options ) {
var target = document.getElementById( options.target );
// the map must be manually removed
target && target.removeChild( newdiv );
newdiv = map = location = null;
options._map = null;
}
};
}, {
about: {
name: "Popcorn Google Map Plugin",
version: "0.1",
author: "@annasob",
website: "annasob.wordpress.com"
},
options: {
start: {
elem: "input",
type: "start",
label: "Start"
},
end: {
elem: "input",
type: "start",
label: "End"
},
target: "map-container",
type: {
elem: "select",
options: [ "ROADMAP", "SATELLITE", "STREETVIEW", "HYBRID", "TERRAIN", "STAMEN-WATERCOLOR", "STAMEN-TERRAIN", "STAMEN-TONER" ],
label: "Map Type",
optional: true
},
zoom: {
elem: "input",
type: "text",
label: "Zoom",
"default": 0,
optional: true
},
lat: {
elem: "input",
type: "text",
label: "Lat",
optional: true
},
lng: {
elem: "input",
type: "text",
label: "Lng",
optional: true
},
location: {
elem: "input",
type: "text",
label: "Location",
"default": "Toronto, Ontario, Canada"
},
heading: {
elem: "input",
type: "text",
label: "Heading",
"default": 0,
optional: true
},
pitch: {
elem: "input",
type: "text",
label: "Pitch",
"default": 1,
optional: true
}
}
});
})( Popcorn );
// PLUGIN: IMAGE
(function ( Popcorn ) {
/**
* Images popcorn plug-in
* Shows an image element
* Options parameter will need a start, end, href, target and src.
* Start is the time that you want this plug-in to execute
* End is the time that you want this plug-in to stop executing
* href is the url of the destination of a anchor - optional
* Target is the id of the document element that the iframe needs to be attached to,
* this target element must exist on the DOM
* Src is the url of the image that you want to display
* text is the overlayed text on the image - optional
*
* @param {Object} options
*
* Example:
var p = Popcorn('#video')
.image({
start: 5, // seconds
end: 15, // seconds
href: 'http://www.drumbeat.org/',
src: 'http://www.drumbeat.org/sites/default/files/domain-2/drumbeat_logo.png',
text: 'DRUMBEAT',
target: 'imagediv'
} )
*
*/
var VIDEO_OVERLAY_Z = 2000,
CHECK_INTERVAL_DURATION = 10;
function trackMediaElement( mediaElement ) {
var checkInterval = -1,
container = document.createElement( "div" ),
videoZ = getComputedStyle( mediaElement ).zIndex;
container.setAttribute( "data-popcorn-helper-container", true );
container.style.position = "absolute";
if ( !isNaN( videoZ ) ) {
container.style.zIndex = videoZ + 1;
}
else {
container.style.zIndex = VIDEO_OVERLAY_Z;
}
document.body.appendChild( container );
function check() {
var mediaRect = mediaElement.getBoundingClientRect(),
containerRect = container.getBoundingClientRect();
if ( containerRect.left !== mediaRect.left ) {
container.style.left = mediaRect.left + "px";
}
if ( containerRect.top !== mediaRect.top ) {
container.style.top = mediaRect.top + "px";
}
}
return {
element: container,
start: function() {
checkInterval = setInterval( check, CHECK_INTERVAL_DURATION );
},
stop: function() {
clearInterval( checkInterval );
checkInterval = -1;
},
destroy: function() {
document.body.removeChild( container );
if ( checkInterval !== -1 ) {
clearInterval( checkInterval );
}
}
};
}
Popcorn.plugin( "image", {
manifest: {
about: {
name: "Popcorn image Plugin",
version: "0.1",
author: "Scott Downe",
website: "http://scottdowne.wordpress.com/"
},
options: {
start: {
elem: "input",
type: "number",
label: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
src: {
elem: "input",
type: "url",
label: "Image URL",
"default": "http://mozillapopcorn.org/wp-content/themes/popcorn/images/for_developers.png"
},
href: {
elem: "input",
type: "url",
label: "Link",
"default": "http://mozillapopcorn.org/wp-content/themes/popcorn/images/for_developers.png",
optional: true
},
target: "image-container",
text: {
elem: "input",
type: "text",
label: "Caption",
"default": "Popcorn.js",
optional: true
}
}
},
_setup: function( options ) {
var img = document.createElement( "img" ),
target = document.getElementById( options.target );
options.anchor = document.createElement( "a" );
options.anchor.style.position = "relative";
options.anchor.style.textDecoration = "none";
options.anchor.style.display = "none";
// add the widget's div to the target div.
// if target is <video> or <audio>, create a container and routinely
// update its size/position to be that of the media
if ( target ) {
if ( [ "VIDEO", "AUDIO" ].indexOf( target.nodeName ) > -1 ) {
options.trackedContainer = trackMediaElement( target );
options.trackedContainer.element.appendChild( options.anchor );
}
else {
target && target.appendChild( options.anchor );
}
}
img.addEventListener( "load", function() {
// borders look really bad, if someone wants it they can put it on their div target
img.style.borderStyle = "none";
options.anchor.href = options.href || options.src || "#";
options.anchor.target = "_blank";
var fontHeight, divText;
img.style.height = target.style.height;
img.style.width = target.style.width;
options.anchor.appendChild( img );
// If display text was provided, display it:
if ( options.text ) {
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.style.width || img.width + "px",
zIndex: "10"
});
divText.innerHTML = options.text || "";
divText.style.top = ( ( img.style.height.replace( "px", "" ) || img.height ) / 2 ) - ( divText.offsetHeight / 2 ) + "px";
options.anchor.insertBefore( divText, img );
}
}, 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.anchor.style.display = "inline";
if ( options.trackedContainer ) {
options.trackedContainer.start();
}
},
/**
* @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.anchor.style.display = "none";
if ( options.trackedContainer ) {
options.trackedContainer.stop();
}
},
_teardown: function( options ) {
if ( options.trackedContainer ) {
options.trackedContainer.destroy();
}
else if ( options.anchor.parentNode ) {
options.anchor.parentNode.removeChild( options.anchor );
}
}
});
})( Popcorn );
// PLUGIN: Google Feed
(function ( Popcorn ) {
var i = 1,
scriptLoaded = false;
/**
* 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 ) {
var 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 = "//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( "//www.google.com/jsapi", function() {
google.load( "feeds", "1", {
callback: function () {
dynamicFeedLoad();
}
});
});
} else {
dynamicFeedLoad();
}
// 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++;
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: "number",
label: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
target: "feed-container",
url: {
elem: "input",
type: "url",
label: "Feed URL",
"default": "http://planet.mozilla.org/rss20.xml"
},
title: {
elem: "input",
type: "text",
label: "Title",
"default": "Planet Mozilla",
optional: true
},
orientation: {
elem: "select",
options: [ "Vertical", "Horizontal" ],
label: "Orientation",
"default": "Vertical",
optional: true
}
}
});
})( Popcorn );
// PLUGIN: Subtitle
(function ( Popcorn ) {
var i = 0,
createDefaultContainer = function( context, id ) {
var ctxContainer = context.container = document.createElement( "div" ),
style = ctxContainer.style,
media = context.media;
var updatePosition = function() {
var position = context.position();
// the video element must have height and width defined
style.fontSize = "18px";
style.width = media.offsetWidth + "px";
style.top = position.top + media.offsetHeight - ctxContainer.offsetHeight - 40 + "px";
style.left = position.left + "px";
setTimeout( updatePosition, 10 );
};
ctxContainer.id = id || 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 );
return ctxContainer;
};
/**
* 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.
*
* @param {Object} options
*
* Example:
var p = Popcorn('#video')
.subtitle({
start: 5, // seconds, mandatory
end: 15, // seconds, mandatory
text: 'Hellow world', // optional
target: 'subtitlediv', // optional
} )
*
*/
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: "Start"
},
end: {
elem: "input",
type: "text",
label: "End"
},
target: "subtitle-container",
text: {
elem: "input",
type: "text",
label: "Text"
}
}
},
_setup: function( options ) {
var newdiv = document.createElement( "div" );
newdiv.id = "subtitle-" + i++;
newdiv.style.display = "none";
// 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" ) {
// In case the target doesn't exist in the DOM
options.container = document.getElementById( options.target ) || createDefaultContainer( this, options.target );
} else {
// use shared default container
options.container = this.container;
}
document.getElementById( options.container.id ) && document.getElementById( options.container.id ).appendChild( newdiv );
options.innerContainer = newdiv;
options.showSubtitle = function() {
options.innerContainer.innerHTML = options.text || "";
};
},
/**
* @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: 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: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
src: {
elem: "input",
type: "text",
label: "Tweet Source (# or @)",
"default": "@popcornjs"
},
target: "twitter-container",
height: {
elem: "input",
type: "number",
label: "Height",
"default": "200",
optional: true
},
width: {
elem: "input",
type: "number",
label: "Width",
"default": "250",
optional: true
}
}
},
_setup: function( options ) {
if ( !window.TWTR && !scriptLoading ) {
scriptLoading = true;
Popcorn.getScript( "//widgets.twimg.com/j/2/widget.js" );
}
var target = document.getElementById( options.target );
// create the div to store the widget
// setup widget div that is unique per track
options.container = document.createElement( "div" );
// use this id to connect it to the widget
options.container.setAttribute( "id", Popcorn.guid() );
// display none by default
options.container.style.display = "none";
// 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 ),
widgetOptions = {
version: 2,
// use this id to connect it to the div
id: options.container.getAttribute( "id" ),
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 {
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 );
// 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",
optional: true
},
start: {
elem: "input",
type: "number",
label: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
src: {
elem: "input",
type: "url",
label: "Webpage URL",
"default": "http://mozillapopcorn.org"
},
target: "iframe-container"
}
},
_setup: function( options ) {
var target = document.getElementById( options.target );
// make src an iframe acceptable string
options.src = options.src.replace( /^(https?:)?(\/\/)?/, "//" );
// 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";
// 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: 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: "number",
label: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
lang: {
elem: "input",
type: "text",
label: "Language",
"default": "english",
optional: true
},
src: {
elem: "input",
type: "url",
label: "Wikipedia URL",
"default": "http://en.wikipedia.org/wiki/Cat"
},
title: {
elem: "input",
type: "text",
label: "Title",
"default": "Cats",
optional: true
},
numberofwords: {
elem: "input",
type: "number",
label: "Number of Words",
"default": "200",
optional: true
},
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, "" );
_text = _text.split( " " );
options._desc.innerHTML = ( _text.slice( 0, ( _text.length >= options.numberofwords ? options.numberofwords : _text.length ) ).join (" ") + " ..." ) ;
options._fired = true;
};
if ( options.src ) {
Popcorn.getScript( "//" + options.lang + ".wikipedia.org/w/api.php?action=parse&props=text&redirects&page=" +
options.src.slice( options.src.lastIndexOf( "/" ) + 1 ) + "&format=json&callback=wikiCallback" + _guid );
}
},
/**
* @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: 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( "http://mustache.github.com/extras/mustache.js" );
var shouldReload = !!options.dynamic,
typeOfTemplate = typeof options.template,
typeOfData = typeof options.data,
target = document.getElementById( options.target );
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 {
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 {
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: "number",
label: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
target: "mustache-container",
template: {
elem: "input",
type: "text",
label: "Template"
},
data: {
elem: "input",
type: "text",
label: "Data"
},
dynamic: {
elem: "input",
type: "checkbox",
label: "Dynamic",
"default": true
}
}
});
})( 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).
* The Stamen custom map types can also be used (http://maps.stamen.com): STAMEN-TONER,
* STAMEN-TERRAIN, or STAMEN-WATERCOLOR.
* -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;
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++;
target && target.appendChild( newdiv );
// callback function fires when the script is run
isGeoReady = function() {
if ( ! ( window.OpenLayers && window.OpenLayers.Layer.Stamen ) ) {
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(
"//tinygeocoder.com/create-api.php?q=" + options.location + "&callback=jsonp",
function( latlng ) {
centerlonlat = new OpenLayers.LonLat( latlng[ 1 ], latlng[ 0 ] );
}
);
} else {
centerlonlat = new OpenLayers.LonLat( options.lng, options.lat );
}
options.type = options.type || "ROADMAP";
switch( options.type ) {
case "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",
"//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" );
break;
case "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",
"//terraserver-usa.org/ogcmap.ashx?",
{ layers: "DRG" }
);
options.map.addLayer( relief );
break;
case "STAMEN-TONER":
case "STAMEN-WATERCOLOR":
case "STAMEN-TERRAIN":
var layerName = options.type.replace("STAMEN-", "").toLowerCase();
var sLayer = new OpenLayers.Layer.Stamen( layerName );
displayProjection = new OpenLayers.Projection( "EPSG:4326" );
projection = new OpenLayers.Projection( 'EPSG:900913' );
centerlonlat = centerlonlat.transform( displayProjection, projection );
options.map = new OpenLayers.Map( {
div: newdiv,
projection: projection,
displayProjection: displayProjection,
controls: [
new OpenLayers.Control.Navigation(),
new OpenLayers.Control.PanPanel(),
new OpenLayers.Control.ZoomPanel()
]
} );
options.map.addLayer( sLayer );
break;
default: /* case "ROADMAP": */
// 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 );
break;
}
if ( options.map ) {
options.map.setCenter(centerlonlat, options.zoom || 10);
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 ) {
// insert openlayers api script once
if ( !window.OpenLayers ) {
Popcorn.getScript( "//openlayers.org/api/OpenLayers.js", function() {
Popcorn.getScript( "//maps.stamen.com/js/tile.stamen.js" );
} );
}
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(
"//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: "number",
label: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
target: "map-container",
type: {
elem: "select",
options: [ "ROADMAP", "SATELLITE", "TERRAIN" ],
label: "Map Type",
optional: true
},
zoom: {
elem: "input",
type: "number",
label: "Zoom",
"default": 2
},
lat: {
elem: "input",
type: "text",
label: "Lat",
optional: true
},
lng: {
elem: "input",
type: "text",
label: "Lng",
optional: true
},
location: {
elem: "input",
type: "text",
label: "Location",
"default": "Toronto, Ontario, Canada"
},
markers: {
elem: "input",
type: "text",
label: "List Markers",
optional: true
}
}
});
}) ( 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: 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: "number",
label: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
target: "wordriver-container",
text: {
elem: "input",
type: "text",
label: "Text",
"default": "Popcorn.js"
},
color: {
elem: "input",
type: "text",
label: "Color",
"default": "Green",
optional: true
}
}
},
_setup: function( options ) {
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 ) {
var target = document.getElementById( options.target );
// 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 &&
target && target.removeChild( container[ options.target ] ) &&
delete container[ options.target ];
}
});
})( 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 timeline 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;
Popcorn.plugin( "timeline" , function( options ) {
var target = document.getElementById( options.target ),
contentDiv = document.createElement( "div" ),
container,
goingUp = true;
if ( target && !target.firstChild ) {
target.appendChild ( container = document.createElement( "div" ) );
container.style.width = "inherit";
container.style.height = "inherit";
container.style.overflow = "auto";
} else {
container = target.firstChild;
}
contentDiv.style.display = "none";
contentDiv.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() === "down" ) {
goingUp = false;
}
if ( target && container ) {
// if this isnt the first div added to the target div
if( goingUp ){
// insert the current div before the previous div inserted
container.insertBefore( contentDiv, container.firstChild );
}
else {
container.appendChild( contentDiv );
}
}
i++;
// Default to empty if not used
//options.innerHTML = options.innerHTML || "";
contentDiv.innerHTML = "<p><span id='big' style='font-size:24px; line-height: 130%;' >" + options.title + "</span><br />" +
"<span id='mid' style='font-size: 16px;'>" + options.text + "</span><br />" + options.innerHTML;
return {
start: function( event, options ) {
contentDiv.style.display = "block";
if( options.direction === "down" ) {
container.scrollTop = container.scrollHeight;
}
},
end: function( event, options ) {
contentDiv.style.display = "none";
},
_teardown: function( options ) {
( container && contentDiv ) && container.removeChild( contentDiv ) && !container.firstChild && target.removeChild( container );
}
};
},
{
about: {
name: "Popcorn Timeline Plugin",
version: "0.1",
author: "David Seifried @dcseifried",
website: "dseifried.wordpress.com"
},
options: {
start: {
elem: "input",
type: "number",
label: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
target: "feed-container",
title: {
elem: "input",
type: "text",
label: "Title"
},
text: {
elem: "input",
type: "text",
label: "Text"
},
innerHTML: {
elem: "input",
type: "text",
label: "HTML Code",
optional: true
},
direction: {
elem: "select",
options: [ "DOWN", "UP" ],
label: "Direction",
optional: true
}
}
});
})( Popcorn );
// PLUGIN: documentcloud
(function( Popcorn, document ) {
/**
* Document Cloud popcorn plug-in
*
* @param {Object} options
*
* Example:
* var p = Popcorn("#video")
* // Let the pdf plugin load your PDF file for you using pdfUrl.
* .documentcloud({
* start: 45
* url: "http://www.documentcloud.org/documents/70050-urbina-day-1-in-progress.html", // or .js
* width: ...,
* height: ...,
* zoom: ...,
* page: ...,
* container: ...
* });
api - https://github.com/documentcloud/document-viewer/blob/master/public/javascripts/DV/controllers/api.js
*/
// track registered plugins by document
var documentRegistry = {};
Popcorn.plugin( "documentcloud", {
manifest: {
about: {
name: "Popcorn Document Cloud Plugin",
version: "0.1",
author: "@humphd, @ChrisDeCairos",
website: "http://vocamus.net/dave"
},
options: {
start: {
elem: "input",
type: "number",
label: "Start"
},
end: {
elem: "input",
type: "number",
label: "End"
},
target: "documentcloud-container",
width: {
elem: "input",
type: "text",
label: "Width",
optional: true
},
height: {
elem: "input",
type: "text",
label: "Height",
optional: true
},
src: {
elem: "input",
type: "url",
label: "PDF URL",
"default": "http://www.documentcloud.org/documents/70050-urbina-day-1-in-progress.html"
},
preload: {
elem: "input",
type: "checkbox",
label: "Preload",
"default": true
},
page: {
elem: "input",
type: "number",
label: "Page Number",
optional: true
},
aid: {
elem: "input",
type: "number",
label: "Annotation Id",
optional: true
}
}
},
_setup: function( options ) {
var DV = window.DV = window.DV || {},
that = this;
//setup elem...
function load() {
DV.loaded = false;
// swap .html URL to .js for API call
var url = options.url.replace( /\.html$/, ".js" ),
target = options.target,
targetDiv = document.getElementById( target ),
containerDiv = document.createElement( "div" ),
containerDivSize = Popcorn.position( targetDiv ),
// need to use size of div if not given
width = options.width || containerDivSize.width,
height = options.height || containerDivSize.height,
sidebar = options.sidebar || true,
text = options.text || true,
pdf = options.pdf || true,
showAnnotations = options.showAnnotations || true,
zoom = options.zoom || 700,
search = options.search || true,
page = options.page,
container;
function setOptions( viewer ) {
options._key = viewer.api.getId();
options._changeView = function ( viewer ) {
if ( options.aid ) {
viewer.pageSet.showAnnotation( viewer.api.getAnnotation( options.aid ) );
} else {
viewer.api.setCurrentPage( options.page );
}
};
}
function documentIsLoaded( url ) {
var found = false;
Popcorn.forEach( DV.viewers, function( viewer, idx ) {
if( viewer.api.getSchema().canonicalURL === url ) {
var targetDoc;
setOptions( viewer );
targetDoc = documentRegistry[ options._key ];
options._containerId = targetDoc.id;
targetDoc.num += 1;
found = true;
DV.loaded = true;
}
});
return found;
}
function createRegistryEntry() {
var entry = {
num: 1,
id: options._containerId
};
documentRegistry[ options._key ] = entry;
DV.loaded = true;
}
if ( !documentIsLoaded( options.url ) ) {
containerDiv.id = options._containerId = Popcorn.guid( target );
container = "#" + containerDiv.id;
targetDiv.appendChild( containerDiv );
that.trigger( "documentready" );
// Figure out if we need a callback to change the page #
var afterLoad = options.page || options.aid ?
function( viewer ) {
setOptions( viewer );
options._changeView( viewer );
containerDiv.style.visibility = "hidden";
viewer.elements.pages.hide();
createRegistryEntry();
} :
function( viewer ) {
setOptions( viewer );
createRegistryEntry();
containerDiv.style.visibility = "hidden";
viewer.elements.pages.hide();
};
DV.load( url, {
width: width,
height: height,
sidebar: sidebar,
text: text,
pdf: pdf,
showAnnotations: showAnnotations,
zoom: zoom,
search: search,
container: container,
afterLoad: afterLoad
});
}
}
function readyCheck() {
if( window.DV.loaded ) {
load();
} else {
setTimeout( readyCheck, 25 );
}
}
// If the viewer is already loaded, don't repeat the process.
if ( !DV.loading ) {
DV.loading = true;
DV.recordHit = "//www.documentcloud.org/pixel.gif";
var link = document.createElement( "link" ),
head = document.getElementsByTagName( "head" )[ 0 ];
link.rel = "stylesheet";
link.type = "text/css";
link.media = "screen";
link.href = "//s3.documentcloud.org/viewer/viewer-datauri.css";
head.appendChild( link );
// Record the fact that the viewer is loaded.
DV.loaded = false;
// Request the viewer JavaScript.
Popcorn.getScript( "http://s3.documentcloud.org/viewer/viewer.js", function() {
DV.loading = false;
load();
});
} else {
readyCheck();
}
},
start: function( event, options ) {
var elem = document.getElementById( options._containerId ),
viewer = DV.viewers[ options._key ];
( options.page || options.aid ) && viewer &&
options._changeView( viewer );
if ( elem && viewer) {
elem.style.visibility = "visible";
viewer.elements.pages.show();
}
},
end: function( event, options ) {
var elem = document.getElementById( options._containerId );
if ( elem && DV.viewers[ options._key ] ) {
elem.style.visibility = "hidden";
DV.viewers[ options._key ].elements.pages.hide();
}
},
_teardown: function( options ) {
var elem = document.getElementById( options._containerId ),
key = options._key;
if ( key && DV.viewers[ key ] && --documentRegistry[ key ].num === 0 ) {
DV.viewers[ key ].api.unload();
while ( elem.hasChildNodes() ) {
elem.removeChild( elem.lastChild );
}
elem.parentNode.removeChild( elem );
}
}
});
})( Popcorn, window.document );
// 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.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,
idx = 0,
lines,
time,
text,
endIdx,
sub;
// Here is where the magic happens
// Split on line breaks
lines = data.text.split( /(?:\r\n|\r|\n)/gm );
endIdx = lastNonEmptyLine( lines ) + 1;
for( i=0; i < endIdx; 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 < endIdx && 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, "<" ).replace( />/g, ">" );
// 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( /<(\/?(font|b|u|i|s))((\s+(\w|\w[\w\-]*\w)(\s*=\s*(?:\".*?\"|'.*?'|[^'\">\s]+))?)+\s*|\s*)(\/?)>/gi, "<$1$3$7>" );
sub.text = sub.text.replace( /\\N/gi, "<br />" );
subs.push( createTrack( "subtitle", sub ) );
}
retObj.data = subs;
return retObj;
});
function createTrack( name, attributes ) {
var track = {};
track[name] = attributes;
return track;
}
// Simple function to convert HH:MM:SS,MMM or HH:MM:SS.MMM to SS.MMM
// Assume valid, returns 0 on error
function toSeconds( 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;
}
}
function lastNonEmptyLine( linesArray ) {
var idx = linesArray.length - 1;
while ( idx >= 0 && !linesArray[idx] ) {
idx--;
}
return idx;
}
})( 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: [ ]
},
rNewLineFile = /(?:\r\n|\r|\n)/gm,
subs = [ ],
lines,
headers,
i = 0,
len;
// Here is where the magic happens
// Split on line breaks
lines = data.text.split( rNewLineFile );
len = lines.length;
// Ignore non-textual info
while ( i < len && lines[ i ] !== "[Events]" ) {
i++;
}
headers = parseFieldHeaders( lines[ ++i ] );
while ( ++i < len && lines[ i ] && lines[ i ][ 0 ] !== "[" ) {
try {
subs.push( createTrack( "subtitle", parseSub( lines[ i ], headers ) ) );
} catch ( e ) {}
}
retObj.data = subs;
return retObj;
});
function parseSub( line, headers ) {
// Trim beginning 'Dialogue: ' and split on delim
var fields = line.substr( 10 ).split( "," ),
rAdvancedStyles = /\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}/gi,
rNewLineSSA = /\\N/gi,
sub;
sub = {
start: toSeconds( fields[ headers.start ] ),
end: toSeconds( fields[ headers.end ] )
};
// Invalid time, skip
if ( sub.start === -1 || sub.end === -1 ) {
throw "Invalid time";
}
// Eliminate advanced styles and convert forced line breaks
sub.text = getTextFromFields( fields, headers.text ).replace( rAdvancedStyles, "" ).replace( rNewLineSSA, "<br />" );
return sub;
}
// h:mm:ss.cc (centisec) string to SS.mmm
// Returns -1 if invalid
function toSeconds( t_in ) {
var t = t_in.split( ":" );
// Not all there
if ( t_in.length !== 10 || t.length < 3 ) {
return -1;
}
return parseInt( t[ 0 ], 10 ) * 3600 + parseInt( t[ 1 ], 10 ) * 60 + parseFloat( t[ 2 ], 10 );
}
function getTextFromFields( fields, startIdx ) {
var fieldLen = fields.length,
text = [ ],
i = startIdx;
// There may be commas in the text which were split, append back together into one line
for( ; i < fieldLen; i++ ) {
text.push( fields[ i ] );
}
return text.join( "," );
}
function createTrack( name, attributes ) {
var track = {};
track[ name ] = attributes;
return track;
}
function parseFieldHeaders( line ) {
// Trim 'Format: ' off front, split on delim
var fields = line.substr( 8 ).split( ", " ),
result = {},
len,
i;
//Find where in Dialogue string the start, end and text info is
for ( i = 0, len = fields.length; i < len; i++ ) {
if ( fields[ i ] === "Start" ) {
result.start = i;
} else if ( fields[ i ] === "End" ) {
result.end = i;
} else if ( fields[ i ] === "Text" ) {
result.text = i;
}
}
return result;
}
})( Popcorn );
// PARSER: 1.0 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>
*/
var rWhitespace = /^[\s]+|[\s]+$/gm,
rLineBreak = /(?:\r\n|\r|\n)/gm;
Popcorn.parser( "parseTTML", function( data ) {
var returnData = {
title: "",
remote: "",
data: []
},
node;
// Null checks
if ( !data.xml || !data.xml.documentElement ) {
return returnData;
}
node = data.xml.documentElement.firstChild;
if ( !node ) {
return returnData;
}
// Find body tag
while ( node.nodeName !== "body" ) {
node = node.nextSibling;
}
if ( node ) {
returnData.data = parseChildren( node, 0 );
}
return returnData;
});
// Parse the children of the given node
function parseChildren( node, timeOffset, region ) {
var currNode = node.firstChild,
currRegion = getNodeRegion( node, region ),
retVal = [],
newOffset;
while ( currNode ) {
if ( currNode.nodeType === 1 ) {
if ( currNode.nodeName === "p" ) {
// p is a textual node, process contents as subtitle
retVal.push( parseNode( currNode, timeOffset, currRegion ) );
} else if ( currNode.nodeName === "div" ) {
// div is container for subtitles, recurse
newOffset = toSeconds( currNode.getAttribute( "begin" ) );
if (newOffset < 0 ) {
newOffset = timeOffset;
}
retVal.push.apply( retVal, parseChildren( currNode, newOffset, currRegion ) );
}
}
currNode = currNode.nextSibling;
}
return retVal;
}
// Get the "region" attribute of a node, to know where to put the subtitles
function getNodeRegion( node, defaultTo ) {
var region = node.getAttribute( "region" );
if ( region !== null ) {
return region;
} else {
return defaultTo || "";
}
}
// Parse a node for text content
function parseNode( node, timeOffset, region ) {
var sub = {};
// Trim left and right whitespace from text and convert non-explicit line breaks
sub.text = ( node.textContent || node.text ).replace( rWhitespace, "" ).replace( rLineBreak, "<br />" );
sub.id = node.getAttribute( "xml:id" ) || node.getAttribute( "id" );
sub.start = toSeconds ( node.getAttribute( "begin" ), timeOffset );
sub.end = toSeconds( node.getAttribute( "end" ), timeOffset );
sub.target = getNodeRegion( node, 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 { subtitle : sub };
}
// Convert time expression to SS.mmm
// Expression may be absolute to timeline (hh:mm:ss.ms)
// or relative ( decimal followed by metric ) ex: 3.4s, 5.7m
// Returns -1 if invalid
function toSeconds( t_in, offset ) {
var i;
if ( !t_in ) {
return -1;
}
try {
return Popcorn.util.toSeconds( t_in );
} catch ( e ) {
i = getMetricIndex( t_in );
return parseFloat( t_in.substring( 0, i ) ) * getMultipler( t_in.substring( i ) ) + ( offset || 0 );
}
}
// In a time string such as 3.4ms, get the index of the first character (m) of the time metric (ms)
function getMetricIndex( t_in ) {
var i = t_in.length - 1;
while ( i >= 0 && t_in[ i ] <= "9" && t_in[ i ] >= "0" ) {
i--;
}
return i;
}
// Determine multiplier for metric relative to seconds
function getMultipler( metric ) {
return {
"h" : 3600,
"m" : 60,
"s" : 1,
"ms" : 0.001
}[ metric ] || -1;
}
})( 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 WebSRT/VTT
(function ( Popcorn ) {
/**
* WebVTT popcorn parser plug-in
* Parses subtitle files in the WebVTT format.
* Specification here: http://www.whatwg.org/specs/web-apps/current-work/webvtt.html
* Styles which appear after timing information are presently ignored.
* Inline styling tags follow HTML conventions and are left in for the browser to handle (or ignore if VTT-specific)
* Data parameter is given by Popcorn, text property holds file contents.
* Text is the file contents to be parsed
*
* @param {Object} data
*
* Example:
00:32.500 --> 00:00:33.500 A:start S:50% D:vertical L:98%
<v Neil DeGrass Tyson><i>Laughs</i>
*/
Popcorn.parser( "parseVTT", function( data ) {
// declare needed variables
var retObj = {
title: "",
remote: "",
data: []
},
subs = [],
i = 0,
len = 0,
lines,
text,
sub,
rNewLine = /(?:\r\n|\r|\n)/gm;
// Here is where the magic happens
// Split on line breaks
lines = data.text.split( rNewLine );
len = lines.length;
// Check for BOF token
if ( len === 0 || lines[ 0 ] !== "WEBVTT" ) {
return retObj;
}
i++;
while ( i < len ) {
text = [];
try {
i = skipWhitespace( lines, len, i );
sub = parseCueHeader( lines[ i++ ] );
// 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 text
sub.text = text.join( "<br />" );
subs.push( createTrack( "subtitle", sub ) );
} catch ( e ) {
i = skipNonWhitespace( lines, len, i );
}
}
retObj.data = subs;
return retObj;
});
// [HH:]MM:SS.mmm string to SS.mmm float
// Throws exception if invalid
function toSeconds ( 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;
}
function createTrack( name, attributes ) {
var track = {};
track[ name ] = attributes;
return track;
}
function parseCueHeader ( line ) {
var lineSegments,
args,
sub = {},
rToken = /-->/,
rWhitespace = /[\t ]+/;
if ( !line || line.indexOf( "-->" ) === -1 ) {
throw "Bad cue";
}
lineSegments = line.replace( rToken, " --> " ).split( rWhitespace );
if ( lineSegments.length < 2 ) {
throw "Bad cue";
}
sub.id = line;
sub.start = toSeconds( lineSegments[ 0 ] );
sub.end = toSeconds( lineSegments[ 2 ] );
return sub;
}
function skipWhitespace ( lines, len, i ) {
while ( i < len && !lines[ i ] ) {
i++;
}
return i;
}
function skipNonWhitespace ( lines, len, i ) {
while ( i < len && lines[ i ] ) {
i++;
}
return i;
}
})( 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,
manifestItem = manifestData[ data ];
// 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" ) {
for ( var item in manifestItem ) {
if ( manifestItem.hasOwnProperty( item ) ) {
if ( !returnObject[ item ] && item !== "id" ) {
returnObject[ item ] = manifestItem[ item ];
}
}
}
// 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 || node.text } );
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 );
(function() {
var scriptLoaded = false,
loading = false;
Popcorn.player( "soundcloud", {
_canPlayType: function( nodeName, url ) {
return (/(?:http:\/\/www\.|http:\/\/|www\.|\.|^)(soundcloud)/).test( url ) && nodeName.toLowerCase() !== "video";
},
_setup: function( options ) {
var media = this,
container = document.createElement( "iframe" ),
lastVolume = 1,
currentTime = 0,
paused = true,
realPaused = true,
widget,
duration = 0,
muted = false,
playerQueue = Popcorn.player.playerQueue();
options._container = container;
media.style.visibility = "hidden";
media.play = function() {
paused = false;
playerQueue.add(function() {
if ( realPaused ) {
widget && widget.play();
} else {
playerQueue.next();
}
});
};
media.pause = function() {
paused = true;
playerQueue.add(function() {
if ( !realPaused ) {
widget && widget.pause();
} else {
playerQueue.next();
}
});
};
// getter and setter for muted property, multiply volume by 100 as that is the scale soundcloud works on
Object.defineProperties( media, {
muted: {
set: function( val ) {
if ( val ) {
widget && widget.getVolume(function( data ) {
lastVolume = data / 100;
});
widget && widget.setVolume( 0 );
muted = true;
} else {
widget && widget.setVolume( lastVolume * 100 );
muted = false;
}
media.dispatchEvent( "volumechange" );
},
get: function() {
return muted;
}
},
volume: {
set: function( val ) {
widget && widget.setVolume( val * 100 );
lastVolume = val ;
media.dispatchEvent( "volumechange" );
},
get: function() {
return muted ? 0 : lastVolume;
}
},
currentTime: {
set: function( val ) {
currentTime = val;
widget && widget.seekTo( val * 1000 );
media.dispatchEvent( "seeked" );
media.dispatchEvent( "timeupdate" );
},
get: function() {
return currentTime;
}
},
duration: {
get: function() {
return duration;
}
},
paused: {
get: function() {
return paused;
}
}
});
// called when the SoundCloud api script has loaded
function scriptReady() {
scriptLoaded = true;
SC.initialize({
client_id: "PRaNFlda6Bhf5utPjUsptg"
});
SC.get( "/resolve", {
url: media.src
}, function( data ) {
media.width = media.style.width ? "" + media.offsetWidth : "560";
media.height = media.style.height ? "" + media.offsetHeight : "315";
// TODO: There are quite a few options here that we should pass on to the user
container.scrolling = "no";
container.frameborder = "no";
container.id = "soundcloud-" + Popcorn.guid();
container.src = "http://w.soundcloud.com/player/?url=" + data.uri +
"&show_artwork=false" +
"&buying=false" +
"&liking=false" +
"&sharing=false";
container.width = "100%";
container.height = "100%";
options.loadListener = function( e ) {
options.widget = widget = SC.Widget( container.id );
// setup all of our listeners
widget.bind(SC.Widget.Events.FINISH, function() {
media.pause();
media.dispatchEvent( "ended" );
});
widget.bind(SC.Widget.Events.PLAY_PROGRESS, function( data ) {
currentTime = data.currentPosition / 1000;
media.dispatchEvent( "timeupdate" );
});
widget.bind(SC.Widget.Events.PLAY, function( data ) {
paused = realPaused = false;
media.dispatchEvent( "play" );
media.dispatchEvent( "playing" );
media.currentTime = currentTime;
playerQueue.next();
});
widget.bind(SC.Widget.Events.PAUSE, function( data ) {
paused = realPaused = true;
media.dispatchEvent( "pause" );
playerQueue.next();
});
widget.bind(SC.Widget.Events.READY, function( data ) {
widget.getDuration(function( data ) {
duration = data / 1000;
media.style.visibility = "visible";
media.dispatchEvent( "durationchange" );
// update the readyState after we have the duration
media.readyState = 4;
media.dispatchEvent( "readystatechange" );
media.dispatchEvent( "loadedmetadata" );
media.dispatchEvent( "loadeddata" );
media.dispatchEvent( "canplaythrough" );
media.dispatchEvent( "load" );
!media.paused && media.play();
});
widget.getVolume(function( data ) {
lastVolume = data / 100;
});
});
};
container.addEventListener( "load", options.loadListener, false );
media.appendChild( container );
});
}
// load the SoundCloud API script if it doesn't exist
function loadScript() {
if ( !loading ) {
loading = true;
Popcorn.getScript( "http://w.soundcloud.com/player/api.js", function() {
Popcorn.getScript( "http://connect.soundcloud.com/sdk.js", function() {
scriptReady();
});
});
} else {
(function isReady() {
setTimeout(function() {
if ( !scriptLoaded ) {
isReady();
} else {
scriptReady();
}
}, 100 );
})();
}
}
if ( !scriptLoaded ) {
loadScript();
} else {
scriptReady();
}
},
_teardown: function( options ) {
var widget = options.widget,
events = SC.Widget.Events,
container = options._container,
parentContainer = container.parentNode;
options.destroyed = true;
// if the widget never got setup, remove the containers load listener and return
if ( !widget ) {
container.removeEventListener( "load", options.loadEventListener, false );
return;
}
// remove all bound soundcloud listeners
for ( var prop in events ) {
widget && widget.unbind( events[ prop ] );
}
}
});
})();
(function() {
// parseUri 1.2.2
// http://blog.stevenlevithan.com/archives/parseuri
// (c) Steven Levithan <stevenlevithan.com>
// MIT License
function parseUri (str) {
var o = parseUri.options,
m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
uri = {},
i = 14;
while (i--) {
uri[o.key[i]] = m[i] || "";
}
uri[o.q.name] = {};
uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
if ($1) {
uri[o.q.name][$1] = $2;
}
});
return uri;
}
parseUri.options = {
strictMode: false,
key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
q: {
name: "queryKey",
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
},
parser: {
strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
}
};
function canPlayType( nodeName, url ) {
return ( /player.vimeo.com\/video\/\d+/ ).test( url ) ||
( /vimeo.com\/\d+/ ).test( url );
}
Popcorn.player( "vimeo", {
_canPlayType: canPlayType,
_setup: function( options ) {
var TIMEUPDATE_INTERVAL_MS = 250,
CURRENT_TIME_MONITOR_MS = 16,
MediaErrorInterface = {
MEDIA_ERR_ABORTED: 1,
MEDIA_ERR_NETWORK: 2,
MEDIA_ERR_DECODE: 3,
MEDIA_ERR_SRC_NOT_SUPPORTED: 4
},
guid,
media = this,
commands = {
q: [],
queue: function queue( fn ) {
this.q.push( fn );
this.process();
},
process: function process() {
if ( !vimeoReady ) {
return;
}
while ( this.q.length ) {
var fn = this.q.shift();
fn();
}
}
},
currentTimeId,
timeUpdateId,
vimeoReady,
vimeoContainer = document.createElement( "iframe" ),
// Loosely based on HTMLMediaElement + HTMLVideoElement IDL
impl = {
// error state
error: null,
// network state
src: media.src,
NETWORK_EMPTY: 0,
NETWORK_IDLE: 1,
NETWORK_LOADING: 2,
NETWORK_NO_SOURCE: 3,
networkState: 0,
// ready state
HAVE_NOTHING: 0,
HAVE_METADATA: 1,
HAVE_CURRENT_DATA: 2,
HAVE_FUTURE_DATA: 3,
HAVE_ENOUGH_DATA: 4,
readyState: 0,
seeking: false,
// playback state
currentTime: 0,
duration: NaN,
paused: true,
ended: false,
autoplay: false,
loop: false,
// controls
volume: 1,
muted: false,
// Video attributes
width: 0,
height: 0
};
var readOnlyAttrs = "error networkState readyState seeking duration paused ended";
Popcorn.forEach( readOnlyAttrs.split(" "), function( value ) {
Object.defineProperty( media, value, {
get: function() {
return impl[ value ];
}
});
});
Object.defineProperties( media, {
"src": {
get: function() {
return impl.src;
},
set: function( value ) {
// Is there any sort of logic that determines whether to load the video or not?
impl.src = value;
media.load();
}
},
"currentTime": {
get: function() {
return impl.currentTime;
},
set: function( value ) {
commands.queue(function() {
sendMessage( "seekTo", value );
});
impl.seeking = true;
media.dispatchEvent( "seeking" );
}
},
"autoplay": {
get: function() {
return impl.autoplay;
},
set: function( value ) {
impl.autoplay = !!value;
}
},
"loop": {
get: function() {
return impl.loop;
},
set: function( value) {
impl.loop = !!value;
commands.queue(function() {
sendMessage( "setLoop", loop );
});
}
},
"volume": {
get: function() {
return impl.volume;
},
set: function( value ) {
impl.volume = value;
commands.queue(function() {
sendMessage( "setVolume", impl.muted ? 0 : impl.volume );
});
media.dispatchEvent( "volumechange" );
}
},
"muted": {
get: function() {
return impl.muted;
},
set: function( value ) {
impl.muted = !!value;
commands.queue(function() {
sendMessage( "setVolume", impl.muted ? 0 : impl.volume );
});
media.dispatchEvent( "volumechange" );
}
},
"width": {
get: function() {
return vimeoContainer.width;
},
set: function( value ) {
vimeoContainer.width = value;
}
},
"height": {
get: function() {
return vimeoContainer.height;
},
set: function( value ) {
vimeoContainer.height = value;
}
}
});
function sendMessage( method, params ) {
var url = vimeoContainer.src.split( "?" )[ 0 ],
data = JSON.stringify({
method: method,
value: params
});
if ( url.substr( 0, 2 ) === "//" ) {
url = window.location.protocol + url;
}
// The iframe has been destroyed, it just doesn't know it
if ( !vimeoContainer.contentWindow ) {
media.unload();
return;
}
vimeoContainer.contentWindow.postMessage( data, url );
}
var vimeoAPIMethods = {
"getCurrentTime": function( data ) {
impl.currentTime = parseFloat( data.value );
},
"getDuration": function( data ) {
impl.duration = parseFloat( data.value );
maybeReady();
},
"getVolume": function( data ) {
impl.volume = parseFloat( data.value );
}
};
var vimeoAPIEvents = {
"ready": function( data ) {
sendMessage( "addEventListener", "loadProgress" );
sendMessage( "addEventListener", "playProgress" );
sendMessage( "addEventListener", "play" );
sendMessage( "addEventListener", "pause" );
sendMessage( "addEventListener", "finish" );
sendMessage( "addEventListener", "seek" );
sendMessage( "getDuration" );
vimeoReady = true;
commands.process();
media.dispatchEvent( "loadstart" );
},
"loadProgress": function( data ) {
media.dispatchEvent( "progress" );
// loadProgress has a more accurate duration than getDuration
impl.duration = parseFloat( data.data.duration );
},
"playProgress": function( data ) {
impl.currentTime = parseFloat( data.data.seconds );
},
"play": function( data ) {
// Vimeo plays video if seeking from an unloaded state
if ( impl.seeking ) {
impl.seeking = false;
media.dispatchEvent( "seeked" );
}
impl.paused = false;
impl.ended = false;
startUpdateLoops();
media.dispatchEvent( "play" );
},
"pause": function( data ) {
impl.paused = true;
stopUpdateLoops();
media.dispatchEvent( "pause" );
},
"finish": function( data ) {
impl.ended = true;
stopUpdateLoops();
media.dispatchEvent( "ended" );
},
"seek": function( data ) {
impl.currentTime = parseFloat( data.data.seconds );
impl.seeking = false;
impl.ended = false;
media.dispatchEvent( "timeupdate" );
media.dispatchEvent( "seeked" );
}
};
function messageListener( event ) {
if ( event.origin !== "http://player.vimeo.com" ) {
return;
}
var data;
try {
data = JSON.parse( event.data );
} catch ( ex ) {
console.warn( ex );
}
if ( data.player_id != guid ) {
return;
}
// Methods
if ( data.method && vimeoAPIMethods[ data.method ] ) {
vimeoAPIMethods[ data.method ]( data );
}
// Events
if ( data.event && vimeoAPIEvents[ data.event ] ) {
vimeoAPIEvents[ data.event ]( data );
}
}
media.load = function() {
vimeoReady = false;
guid = Popcorn.guid();
var src = parseUri( impl.src ),
combinedOptions = {},
optionsArray = [],
vimeoAPIOptions = {
api: 1,
player_id: guid
};
if ( !canPlayType( media.nodeName, src.source ) ) {
setErrorAttr( impl.MEDIA_ERR_SRC_NOT_SUPPORTED );
return;
}
// Add Popcorn ctor options, url options, then the Vimeo API options
Popcorn.extend( combinedOptions, options );
Popcorn.extend( combinedOptions, src.queryKey );
Popcorn.extend( combinedOptions, vimeoAPIOptions );
// Create the base vimeo player string. It will always have query string options
src = "http://player.vimeo.com/video/" + ( /\d+$/ ).exec( src.path ) + "?";
for ( var key in combinedOptions ) {
if ( combinedOptions.hasOwnProperty( key ) ) {
optionsArray.push( encodeURIComponent( key ) + "=" + encodeURIComponent( combinedOptions[ key ] ) );
}
}
src += optionsArray.join( "&" );
impl.loop = !!src.match( /loop=1/ );
impl.autoplay = !!src.match( /autoplay=1/ );
vimeoContainer.width = media.style.width ? media.style.width : 500;
vimeoContainer.height = media.style.height ? media.style.height : 281;
vimeoContainer.frameBorder = 0;
vimeoContainer.webkitAllowFullScreen = true;
vimeoContainer.mozAllowFullScreen = true;
vimeoContainer.allowFullScreen = true;
vimeoContainer.src = src;
media.appendChild( vimeoContainer );
};
function setErrorAttr( value ) {
impl.error = {};
Popcorn.extend( impl.error, MediaErrorInterface );
impl.error.code = value;
media.dispatchEvent( "error" );
}
function maybeReady() {
if ( !isNaN( impl.duration ) ) {
impl.readyState = 4;
media.dispatchEvent( "durationchange" );
media.dispatchEvent( "loadedmetadata" );
media.dispatchEvent( "loadeddata" );
media.dispatchEvent( "canplay" );
media.dispatchEvent( "canplaythrough" );
}
}
function startUpdateLoops() {
if ( !timeUpdateId ) {
timeUpdateId = setInterval(function() {
media.dispatchEvent( "timeupdate" );
}, TIMEUPDATE_INTERVAL_MS );
}
if ( !currentTimeId ) {
currentTimeId = setInterval(function() {
sendMessage( "getCurrentTime" );
}, CURRENT_TIME_MONITOR_MS );
}
}
function stopUpdateLoops() {
if ( timeUpdateId ) {
clearInterval( timeUpdateId );
timeUpdateId = 0;
}
if ( currentTimeId ) {
clearInterval( currentTimeId );
currentTimeId = 0;
}
}
media.unload = function() {
stopUpdateLoops();
window.removeEventListener( "message", messageListener, false );
};
media.play = function() {
commands.queue(function() {
sendMessage( "play" );
});
};
media.pause = function() {
commands.queue(function() {
sendMessage( "pause" );
});
};
// Start the load process now, players behave like `preload="metadata"` is set
// Do it asynchronously so that users can attach event listeners
setTimeout(function() {
window.addEventListener( "message", messageListener, false );
media.load();
}, 0 );
},
_teardown: function( options ) {
// If the baseplayer doesn't call _setup
if ( this.unload ) {
this.unload();
}
}
});
})();
(function( window, Popcorn ) {
// A global callback for youtube... that makes me angry
window.onYouTubePlayerAPIReady = function() {
onYouTubePlayerAPIReady.ready = true;
for ( var i = 0; i < onYouTubePlayerAPIReady.waiting.length; i++ ) {
onYouTubePlayerAPIReady.waiting[ i ]();
}
};
// existing youtube references can break us.
// remove it and use the one we can trust.
if ( window.YT ) {
window.quarantineYT = window.YT;
window.YT = null;
}
onYouTubePlayerAPIReady.waiting = [];
var _loading = false;
Popcorn.player( "youtube", {
_canPlayType: function( nodeName, url ) {
return typeof url === "string" && (/(?:http:\/\/www\.|http:\/\/|www\.|\.|^)(youtu)/).test( url ) && nodeName.toLowerCase() !== "video";
},
_setup: function( options ) {
if ( !window.YT && !_loading ) {
_loading = true;
Popcorn.getScript( "//youtube.com/player_api" );
}
var media = this,
autoPlay = false,
container = document.createElement( "div" ),
currentTime = 0,
paused = true,
seekTime = 0,
firstGo = true,
seeking = false,
fragmentStart = 0,
// state code for volume changed polling
lastMuted = false,
lastVolume = 100,
playerQueue = Popcorn.player.playerQueue();
var createProperties = function() {
Popcorn.player.defineProperty( media, "currentTime", {
set: function( val ) {
if ( options.destroyed ) {
return;
}
seeking = true;
// make sure val is a number
currentTime = Math.round( +val * 100 ) / 100;
},
get: function() {
return currentTime;
}
});
Popcorn.player.defineProperty( media, "paused", {
get: function() {
return paused;
}
});
Popcorn.player.defineProperty( media, "muted", {
set: function( val ) {
if ( options.destroyed ) {
return val;
}
if ( options.youtubeObject.isMuted() !== val ) {
if ( val ) {
options.youtubeObject.mute();
} else {
options.youtubeObject.unMute();
}
lastMuted = options.youtubeObject.isMuted();
media.dispatchEvent( "volumechange" );
}
return options.youtubeObject.isMuted();
},
get: function() {
if ( options.destroyed ) {
return 0;
}
return options.youtubeObject.isMuted();
}
});
Popcorn.player.defineProperty( media, "volume", {
set: function( val ) {
if ( options.destroyed ) {
return val;
}
if ( options.youtubeObject.getVolume() / 100 !== val ) {
options.youtubeObject.setVolume( val * 100 );
lastVolume = options.youtubeObject.getVolume();
media.dispatchEvent( "volumechange" );
}
return options.youtubeObject.getVolume() / 100;
},
get: function() {
if ( options.destroyed ) {
return 0;
}
return options.youtubeObject.getVolume() / 100;
}
});
media.play = function() {
if ( options.destroyed ) {
return;
}
paused = false;
playerQueue.add(function() {
if ( options.youtubeObject.getPlayerState() !== 1 ) {
seeking = false;
options.youtubeObject.playVideo();
} else {
playerQueue.next();
}
});
};
media.pause = function() {
if ( options.destroyed ) {
return;
}
paused = true;
playerQueue.add(function() {
if ( options.youtubeObject.getPlayerState() !== 2 ) {
options.youtubeObject.pauseVideo();
} else {
playerQueue.next();
}
});
};
};
container.id = media.id + Popcorn.guid();
options._container = container;
media.appendChild( container );
var youtubeInit = function() {
var src, query, params, playerVars, queryStringItem, firstPlay = true;
var timeUpdate = function() {
if ( options.destroyed ) {
return;
}
if ( !seeking ) {
currentTime = options.youtubeObject.getCurrentTime();
media.dispatchEvent( "timeupdate" );
} else if ( currentTime === options.youtubeObject.getCurrentTime() ) {
seeking = false;
media.dispatchEvent( "seeked" );
media.dispatchEvent( "timeupdate" );
} else {
// keep trying the seek until it is right.
options.youtubeObject.seekTo( currentTime );
}
setTimeout( timeUpdate, 250 );
};
// delay is in seconds
var fetchDuration = function( delay ) {
var ytDuration = options.youtubeObject.getDuration();
if ( isNaN( ytDuration ) || ytDuration === 0 ) {
setTimeout( function() {
fetchDuration( delay * 2 );
}, delay*1000 );
} else {
// set duration and dispatch ready events
media.duration = ytDuration;
media.dispatchEvent( "durationchange" );
media.dispatchEvent( "loadedmetadata" );
media.dispatchEvent( "loadeddata" );
media.readyState = 4;
timeUpdate();
media.dispatchEvent( "canplaythrough" );
}
};
options.controls = +options.controls === 0 || +options.controls === 1 ? options.controls : 1;
options.annotations = +options.annotations === 1 || +options.annotations === 3 ? options.annotations : 1;
src = /^.*(?:\/|v=)(.{11})/.exec( media.src )[ 1 ];
query = ( media.src.split( "?" )[ 1 ] || "" )
.replace( /v=.{11}/, "" );
query = query.replace( /&t=(?:(\d+)m)?(?:(\d+)s)?/, function( all, minutes, seconds ) {
// Make sure we have real zeros
minutes = minutes | 0; // bit-wise OR
seconds = seconds | 0; // bit-wise OR
fragmentStart = ( +seconds + ( minutes * 60 ) );
return "";
});
query = query.replace( /&start=(\d+)?/, function( all, seconds ) {
// Make sure we have real zeros
seconds = seconds | 0; // bit-wise OR
fragmentStart = seconds;
return "";
});
autoPlay = ( /autoplay=1/.test( query ) );
params = query.split( /[\&\?]/g );
playerVars = { wmode: "transparent" };
for( var i = 0; i < params.length; i++ ) {
queryStringItem = params[ i ].split( "=" );
playerVars[ queryStringItem[ 0 ] ] = queryStringItem[ 1 ];
}
options.youtubeObject = new YT.Player( container.id, {
height: "100%",
width: "100%",
wmode: "transparent",
playerVars: playerVars,
videoId: src,
events: {
"onReady": function(){
// pulling initial volume states form baseplayer
lastVolume = media.volume;
lastMuted = media.muted;
volumeupdate();
paused = media.paused;
createProperties();
options.youtubeObject.playVideo();
media.currentTime = fragmentStart;
// wait to dispatch ready events until we get a duration
},
"onStateChange": function( state ){
if ( options.destroyed || state.data === -1 ) {
return;
}
// state.data === 2 is for pause events
// state.data === 1 is for play events
if ( state.data === 2 ) {
paused = true;
media.dispatchEvent( "pause" );
playerQueue.next();
} else if ( state.data === 1 && !firstPlay ) {
paused = false;
media.dispatchEvent( "play" );
media.dispatchEvent( "playing" );
playerQueue.next();
} else if ( state.data === 0 ) {
media.dispatchEvent( "ended" );
} else if ( state.data === 1 && firstPlay ) {
firstPlay = false;
// pulling initial paused state from autoplay or the baseplayer
// also need to explicitly set to paused otherwise.
if ( autoPlay || !media.paused ) {
paused = false;
}
if ( paused ) {
options.youtubeObject.pauseVideo();
}
fetchDuration( 0.025 );
}
},
"onError": function( error ) {
if ( [ 2, 100, 101, 150 ].indexOf( error.data ) !== -1 ) {
media.error = {
customCode: error.data
};
media.dispatchEvent( "error" );
}
}
}
});
var volumeupdate = function() {
if ( options.destroyed ) {
return;
}
if ( lastMuted !== options.youtubeObject.isMuted() ) {
lastMuted = options.youtubeObject.isMuted();
media.dispatchEvent( "volumechange" );
}
if ( lastVolume !== options.youtubeObject.getVolume() ) {
lastVolume = options.youtubeObject.getVolume();
media.dispatchEvent( "volumechange" );
}
setTimeout( volumeupdate, 250 );
};
};
if ( onYouTubePlayerAPIReady.ready ) {
youtubeInit();
} else {
onYouTubePlayerAPIReady.waiting.push( youtubeInit );
}
},
_teardown: function( options ) {
options.destroyed = true;
var youtubeObject = options.youtubeObject;
if( youtubeObject ){
youtubeObject.stopVideo();
youtubeObject.clearVideo && youtubeObject.clearVideo();
}
this.removeChild( document.getElementById( options._container.id ) );
}
});
}( window, Popcorn ));
// 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 );