popcorn.js 62KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213
  1. (function(global, document) {
  2. // Popcorn.js does not support archaic browsers
  3. if ( !document.addEventListener ) {
  4. global.Popcorn = {
  5. isSupported: false
  6. };
  7. var methods = ( "byId forEach extend effects error guid sizeOf isArray nop position disable enable destroy" +
  8. "addTrackEvent removeTrackEvent getTrackEvents getTrackEvent getLastTrackEventId " +
  9. "timeUpdate plugin removePlugin compose effect xhr getJSONP getScript" ).split(/\s+/);
  10. while ( methods.length ) {
  11. global.Popcorn[ methods.shift() ] = function() {};
  12. }
  13. return;
  14. }
  15. var
  16. AP = Array.prototype,
  17. OP = Object.prototype,
  18. forEach = AP.forEach,
  19. slice = AP.slice,
  20. hasOwn = OP.hasOwnProperty,
  21. toString = OP.toString,
  22. // Copy global Popcorn (may not exist)
  23. _Popcorn = global.Popcorn,
  24. // Ready fn cache
  25. readyStack = [],
  26. readyBound = false,
  27. readyFired = false,
  28. // Non-public internal data object
  29. internal = {
  30. events: {
  31. hash: {},
  32. apis: {}
  33. }
  34. },
  35. // Non-public `requestAnimFrame`
  36. // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
  37. requestAnimFrame = (function(){
  38. return global.requestAnimationFrame ||
  39. global.webkitRequestAnimationFrame ||
  40. global.mozRequestAnimationFrame ||
  41. global.oRequestAnimationFrame ||
  42. global.msRequestAnimationFrame ||
  43. function( callback, element ) {
  44. global.setTimeout( callback, 16 );
  45. };
  46. }()),
  47. // Non-public `getKeys`, return an object's keys as an array
  48. getKeys = function( obj ) {
  49. return Object.keys ? Object.keys( obj ) : (function( obj ) {
  50. var item,
  51. list = [];
  52. for ( item in obj ) {
  53. if ( hasOwn.call( obj, item ) ) {
  54. list.push( item );
  55. }
  56. }
  57. return list;
  58. })( obj );
  59. },
  60. // Declare constructor
  61. // Returns an instance object.
  62. Popcorn = function( entity, options ) {
  63. // Return new Popcorn object
  64. return new Popcorn.p.init( entity, options || null );
  65. };
  66. // Popcorn API version, automatically inserted via build system.
  67. Popcorn.version = "@VERSION";
  68. // Boolean flag allowing a client to determine if Popcorn can be supported
  69. Popcorn.isSupported = true;
  70. // Instance caching
  71. Popcorn.instances = [];
  72. // Declare a shortcut (Popcorn.p) to and a definition of
  73. // the new prototype for our Popcorn constructor
  74. Popcorn.p = Popcorn.prototype = {
  75. init: function( entity, options ) {
  76. var matches, nodeName,
  77. self = this;
  78. // Supports Popcorn(function () { /../ })
  79. // Originally proposed by Daniel Brooks
  80. if ( typeof entity === "function" ) {
  81. // If document ready has already fired
  82. if ( document.readyState === "complete" ) {
  83. entity( document, Popcorn );
  84. return;
  85. }
  86. // Add `entity` fn to ready stack
  87. readyStack.push( entity );
  88. // This process should happen once per page load
  89. if ( !readyBound ) {
  90. // set readyBound flag
  91. readyBound = true;
  92. var DOMContentLoaded = function() {
  93. readyFired = true;
  94. // Remove global DOM ready listener
  95. document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
  96. // Execute all ready function in the stack
  97. for ( var i = 0, readyStackLength = readyStack.length; i < readyStackLength; i++ ) {
  98. readyStack[ i ].call( document, Popcorn );
  99. }
  100. // GC readyStack
  101. readyStack = null;
  102. };
  103. // Register global DOM ready listener
  104. document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
  105. }
  106. return;
  107. }
  108. if ( typeof entity === "string" ) {
  109. try {
  110. matches = document.querySelector( entity );
  111. } catch( e ) {
  112. throw new Error( "Popcorn.js Error: Invalid media element selector: " + entity );
  113. }
  114. }
  115. // Get media element by id or object reference
  116. this.media = matches || entity;
  117. // inner reference to this media element's nodeName string value
  118. nodeName = ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video";
  119. // Create an audio or video element property reference
  120. this[ nodeName ] = this.media;
  121. this.options = options || {};
  122. // Resolve custom ID or default prefixed ID
  123. this.id = this.options.id || Popcorn.guid( nodeName );
  124. // Throw if an attempt is made to use an ID that already exists
  125. if ( Popcorn.byId( this.id ) ) {
  126. throw new Error( "Popcorn.js Error: Cannot use duplicate ID (" + this.id + ")" );
  127. }
  128. this.isDestroyed = false;
  129. this.data = {
  130. // data structure of all
  131. running: {
  132. cue: []
  133. },
  134. // Executed by either timeupdate event or in rAF loop
  135. timeUpdate: Popcorn.nop,
  136. // Allows disabling a plugin per instance
  137. disabled: {},
  138. // Stores DOM event queues by type
  139. events: {},
  140. // Stores Special event hooks data
  141. hooks: {},
  142. // Store track event history data
  143. history: [],
  144. // Stores ad-hoc state related data]
  145. state: {
  146. volume: this.media.volume
  147. },
  148. // Store track event object references by trackId
  149. trackRefs: {},
  150. // Playback track event queues
  151. trackEvents: {
  152. byStart: [{
  153. start: -1,
  154. end: -1
  155. }],
  156. byEnd: [{
  157. start: -1,
  158. end: -1
  159. }],
  160. animating: [],
  161. startIndex: 0,
  162. endIndex: 0,
  163. previousUpdateTime: -1
  164. }
  165. };
  166. // Register new instance
  167. Popcorn.instances.push( this );
  168. // function to fire when video is ready
  169. var isReady = function() {
  170. // chrome bug: http://code.google.com/p/chromium/issues/detail?id=119598
  171. // it is possible the video's time is less than 0
  172. // this has the potential to call track events more than once, when they should not
  173. // start: 0, end: 1 will start, end, start again, when it should just start
  174. // just setting it to 0 if it is below 0 fixes this issue
  175. if ( self.media.currentTime < 0 ) {
  176. self.media.currentTime = 0;
  177. }
  178. self.media.removeEventListener( "loadeddata", isReady, false );
  179. var duration, videoDurationPlus,
  180. runningPlugins, runningPlugin, rpLength, rpNatives;
  181. // Adding padding to the front and end of the arrays
  182. // this is so we do not fall off either end
  183. duration = self.media.duration;
  184. // Check for no duration info (NaN)
  185. videoDurationPlus = duration != duration ? Number.MAX_VALUE : duration + 1;
  186. Popcorn.addTrackEvent( self, {
  187. start: videoDurationPlus,
  188. end: videoDurationPlus
  189. });
  190. if ( self.options.frameAnimation ) {
  191. // if Popcorn is created with frameAnimation option set to true,
  192. // requestAnimFrame is used instead of "timeupdate" media event.
  193. // This is for greater frame time accuracy, theoretically up to
  194. // 60 frames per second as opposed to ~4 ( ~every 15-250ms)
  195. self.data.timeUpdate = function () {
  196. Popcorn.timeUpdate( self, {} );
  197. // fire frame for each enabled active plugin of every type
  198. Popcorn.forEach( Popcorn.manifest, function( key, val ) {
  199. runningPlugins = self.data.running[ val ];
  200. // ensure there are running plugins on this type on this instance
  201. if ( runningPlugins ) {
  202. rpLength = runningPlugins.length;
  203. for ( var i = 0; i < rpLength; i++ ) {
  204. runningPlugin = runningPlugins[ i ];
  205. rpNatives = runningPlugin._natives;
  206. rpNatives && rpNatives.frame &&
  207. rpNatives.frame.call( self, {}, runningPlugin, self.currentTime() );
  208. }
  209. }
  210. });
  211. self.emit( "timeupdate" );
  212. !self.isDestroyed && requestAnimFrame( self.data.timeUpdate );
  213. };
  214. !self.isDestroyed && requestAnimFrame( self.data.timeUpdate );
  215. } else {
  216. self.data.timeUpdate = function( event ) {
  217. Popcorn.timeUpdate( self, event );
  218. };
  219. if ( !self.isDestroyed ) {
  220. self.media.addEventListener( "timeupdate", self.data.timeUpdate, false );
  221. }
  222. }
  223. };
  224. Object.defineProperty( this, "error", {
  225. get: function() {
  226. return self.media.error;
  227. }
  228. });
  229. if ( self.media.readyState >= 2 ) {
  230. isReady();
  231. } else {
  232. self.media.addEventListener( "loadeddata", isReady, false );
  233. }
  234. return this;
  235. }
  236. };
  237. // Extend constructor prototype to instance prototype
  238. // Allows chaining methods to instances
  239. Popcorn.p.init.prototype = Popcorn.p;
  240. Popcorn.byId = function( str ) {
  241. var instances = Popcorn.instances,
  242. length = instances.length,
  243. i = 0;
  244. for ( ; i < length; i++ ) {
  245. if ( instances[ i ].id === str ) {
  246. return instances[ i ];
  247. }
  248. }
  249. return null;
  250. };
  251. Popcorn.forEach = function( obj, fn, context ) {
  252. if ( !obj || !fn ) {
  253. return {};
  254. }
  255. context = context || this;
  256. var key, len;
  257. // Use native whenever possible
  258. if ( forEach && obj.forEach === forEach ) {
  259. return obj.forEach( fn, context );
  260. }
  261. if ( toString.call( obj ) === "[object NodeList]" ) {
  262. for ( key = 0, len = obj.length; key < len; key++ ) {
  263. fn.call( context, obj[ key ], key, obj );
  264. }
  265. return obj;
  266. }
  267. for ( key in obj ) {
  268. if ( hasOwn.call( obj, key ) ) {
  269. fn.call( context, obj[ key ], key, obj );
  270. }
  271. }
  272. return obj;
  273. };
  274. Popcorn.extend = function( obj ) {
  275. var dest = obj, src = slice.call( arguments, 1 );
  276. Popcorn.forEach( src, function( copy ) {
  277. for ( var prop in copy ) {
  278. dest[ prop ] = copy[ prop ];
  279. }
  280. });
  281. return dest;
  282. };
  283. // A Few reusable utils, memoized onto Popcorn
  284. Popcorn.extend( Popcorn, {
  285. noConflict: function( deep ) {
  286. if ( deep ) {
  287. global.Popcorn = _Popcorn;
  288. }
  289. return Popcorn;
  290. },
  291. error: function( msg ) {
  292. throw new Error( msg );
  293. },
  294. guid: function( prefix ) {
  295. Popcorn.guid.counter++;
  296. return ( prefix ? prefix : "" ) + ( +new Date() + Popcorn.guid.counter );
  297. },
  298. sizeOf: function( obj ) {
  299. var size = 0;
  300. for ( var prop in obj ) {
  301. size++;
  302. }
  303. return size;
  304. },
  305. isArray: Array.isArray || function( array ) {
  306. return toString.call( array ) === "[object Array]";
  307. },
  308. nop: function() {},
  309. position: function( elem ) {
  310. var clientRect = elem.getBoundingClientRect(),
  311. bounds = {},
  312. doc = elem.ownerDocument,
  313. docElem = document.documentElement,
  314. body = document.body,
  315. clientTop, clientLeft, scrollTop, scrollLeft, top, left;
  316. // Determine correct clientTop/Left
  317. clientTop = docElem.clientTop || body.clientTop || 0;
  318. clientLeft = docElem.clientLeft || body.clientLeft || 0;
  319. // Determine correct scrollTop/Left
  320. scrollTop = ( global.pageYOffset && docElem.scrollTop || body.scrollTop );
  321. scrollLeft = ( global.pageXOffset && docElem.scrollLeft || body.scrollLeft );
  322. // Temp top/left
  323. top = Math.ceil( clientRect.top + scrollTop - clientTop );
  324. left = Math.ceil( clientRect.left + scrollLeft - clientLeft );
  325. for ( var p in clientRect ) {
  326. bounds[ p ] = Math.round( clientRect[ p ] );
  327. }
  328. return Popcorn.extend({}, bounds, { top: top, left: left });
  329. },
  330. disable: function( instance, plugin ) {
  331. if ( !instance.data.disabled[ plugin ] ) {
  332. instance.data.disabled[ plugin ] = true;
  333. for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) {
  334. event = instance.data.running[ plugin ][ i ];
  335. event._natives.end.call( instance, null, event );
  336. }
  337. }
  338. return instance;
  339. },
  340. enable: function( instance, plugin ) {
  341. if ( instance.data.disabled[ plugin ] ) {
  342. instance.data.disabled[ plugin ] = false;
  343. for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) {
  344. event = instance.data.running[ plugin ][ i ];
  345. event._natives.start.call( instance, null, event );
  346. }
  347. }
  348. return instance;
  349. },
  350. destroy: function( instance ) {
  351. var events = instance.data.events,
  352. trackEvents = instance.data.trackEvents,
  353. singleEvent, item, fn, plugin;
  354. // Iterate through all events and remove them
  355. for ( item in events ) {
  356. singleEvent = events[ item ];
  357. for ( fn in singleEvent ) {
  358. delete singleEvent[ fn ];
  359. }
  360. events[ item ] = null;
  361. }
  362. // remove all plugins off the given instance
  363. for ( plugin in Popcorn.registryByName ) {
  364. Popcorn.removePlugin( instance, plugin );
  365. }
  366. // Remove all data.trackEvents #1178
  367. trackEvents.byStart.length = 0;
  368. trackEvents.byEnd.length = 0;
  369. if ( !instance.isDestroyed ) {
  370. instance.data.timeUpdate && instance.media.removeEventListener( "timeupdate", instance.data.timeUpdate, false );
  371. instance.isDestroyed = true;
  372. }
  373. }
  374. });
  375. // Memoized GUID Counter
  376. Popcorn.guid.counter = 1;
  377. // Factory to implement getters, setters and controllers
  378. // as Popcorn instance methods. The IIFE will create and return
  379. // an object with defined methods
  380. Popcorn.extend(Popcorn.p, (function() {
  381. var methods = "load play pause currentTime playbackRate volume duration preload playbackRate " +
  382. "autoplay loop controls muted buffered readyState seeking paused played seekable ended",
  383. ret = {};
  384. // Build methods, store in object that is returned and passed to extend
  385. Popcorn.forEach( methods.split( /\s+/g ), function( name ) {
  386. ret[ name ] = function( arg ) {
  387. var previous;
  388. if ( typeof this.media[ name ] === "function" ) {
  389. // Support for shorthanded play(n)/pause(n) jump to currentTime
  390. // If arg is not null or undefined and called by one of the
  391. // allowed shorthandable methods, then set the currentTime
  392. // Supports time as seconds or SMPTE
  393. if ( arg != null && /play|pause/.test( name ) ) {
  394. this.media.currentTime = Popcorn.util.toSeconds( arg );
  395. }
  396. this.media[ name ]();
  397. return this;
  398. }
  399. if ( arg != null ) {
  400. // Capture the current value of the attribute property
  401. previous = this.media[ name ];
  402. // Set the attribute property with the new value
  403. this.media[ name ] = arg;
  404. // If the new value is not the same as the old value
  405. // emit an "attrchanged event"
  406. if ( previous !== arg ) {
  407. this.emit( "attrchange", {
  408. attribute: name,
  409. previousValue: previous,
  410. currentValue: arg
  411. });
  412. }
  413. return this;
  414. }
  415. return this.media[ name ];
  416. };
  417. });
  418. return ret;
  419. })()
  420. );
  421. Popcorn.forEach( "enable disable".split(" "), function( method ) {
  422. Popcorn.p[ method ] = function( plugin ) {
  423. return Popcorn[ method ]( this, plugin );
  424. };
  425. });
  426. Popcorn.extend(Popcorn.p, {
  427. // Rounded currentTime
  428. roundTime: function() {
  429. return Math.round( this.media.currentTime );
  430. },
  431. // Attach an event to a single point in time
  432. exec: function( id, time, fn ) {
  433. var length = arguments.length,
  434. trackEvent, sec;
  435. // Check if first could possibly be a SMPTE string
  436. // p.cue( "smpte string", fn );
  437. // try/catch avoid awful throw in Popcorn.util.toSeconds
  438. // TODO: Get rid of that, replace with NaN return?
  439. try {
  440. sec = Popcorn.util.toSeconds( id );
  441. } catch ( e ) {}
  442. // If it can be converted into a number then
  443. // it's safe to assume that the string was SMPTE
  444. if ( typeof sec === "number" ) {
  445. id = sec;
  446. }
  447. // Shift arguments based on use case
  448. //
  449. // Back compat for:
  450. // p.cue( time, fn );
  451. if ( typeof id === "number" && length === 2 ) {
  452. fn = time;
  453. time = id;
  454. id = Popcorn.guid( "cue" );
  455. } else {
  456. // Support for new forms
  457. // p.cue( "empty-cue" );
  458. if ( length === 1 ) {
  459. // Set a time for an empty cue. It's not important what
  460. // the time actually is, because the cue is a no-op
  461. time = -1;
  462. } else {
  463. // Get the trackEvent that matches the given id.
  464. trackEvent = this.getTrackEvent( id );
  465. if ( trackEvent ) {
  466. // p.cue( "my-id", 12 );
  467. // p.cue( "my-id", function() { ... });
  468. if ( typeof id === "string" && length === 2 ) {
  469. // p.cue( "my-id", 12 );
  470. // The path will update the cue time.
  471. if ( typeof time === "number" ) {
  472. // Re-use existing trackEvent start callback
  473. fn = trackEvent._natives.start;
  474. }
  475. // p.cue( "my-id", function() { ... });
  476. // The path will update the cue function
  477. if ( typeof time === "function" ) {
  478. fn = time;
  479. // Re-use existing trackEvent start time
  480. time = trackEvent.start;
  481. }
  482. }
  483. } else {
  484. if ( length >= 2 ) {
  485. // p.cue( "a", "00:00:00");
  486. if ( typeof time === "string" ) {
  487. try {
  488. sec = Popcorn.util.toSeconds( time );
  489. } catch ( e ) {}
  490. time = sec;
  491. }
  492. // p.cue( "b", 11 );
  493. if ( typeof time === "number" ) {
  494. fn = Popcorn.nop();
  495. }
  496. // p.cue( "c", function() {});
  497. if ( typeof time === "function" ) {
  498. fn = time;
  499. time = -1;
  500. }
  501. }
  502. }
  503. }
  504. }
  505. // Creating a one second track event with an empty end
  506. // Or update an existing track event with new values
  507. Popcorn.addTrackEvent( this, {
  508. id: id,
  509. start: time,
  510. end: time + 1,
  511. _running: false,
  512. _natives: {
  513. start: fn || Popcorn.nop,
  514. end: Popcorn.nop,
  515. type: "cue"
  516. }
  517. });
  518. return this;
  519. },
  520. // Mute the calling media, optionally toggle
  521. mute: function( toggle ) {
  522. var event = toggle == null || toggle === true ? "muted" : "unmuted";
  523. // If `toggle` is explicitly `false`,
  524. // unmute the media and restore the volume level
  525. if ( event === "unmuted" ) {
  526. this.media.muted = false;
  527. this.media.volume = this.data.state.volume;
  528. }
  529. // If `toggle` is either null or undefined,
  530. // save the current volume and mute the media element
  531. if ( event === "muted" ) {
  532. this.data.state.volume = this.media.volume;
  533. this.media.muted = true;
  534. }
  535. // Trigger either muted|unmuted event
  536. this.emit( event );
  537. return this;
  538. },
  539. // Convenience method, unmute the calling media
  540. unmute: function( toggle ) {
  541. return this.mute( toggle == null ? false : !toggle );
  542. },
  543. // Get the client bounding box of an instance element
  544. position: function() {
  545. return Popcorn.position( this.media );
  546. },
  547. // Toggle a plugin's playback behaviour (on or off) per instance
  548. toggle: function( plugin ) {
  549. return Popcorn[ this.data.disabled[ plugin ] ? "enable" : "disable" ]( this, plugin );
  550. },
  551. // Set default values for plugin options objects per instance
  552. defaults: function( plugin, defaults ) {
  553. // If an array of default configurations is provided,
  554. // iterate and apply each to this instance
  555. if ( Popcorn.isArray( plugin ) ) {
  556. Popcorn.forEach( plugin, function( obj ) {
  557. for ( var name in obj ) {
  558. this.defaults( name, obj[ name ] );
  559. }
  560. }, this );
  561. return this;
  562. }
  563. if ( !this.options.defaults ) {
  564. this.options.defaults = {};
  565. }
  566. if ( !this.options.defaults[ plugin ] ) {
  567. this.options.defaults[ plugin ] = {};
  568. }
  569. Popcorn.extend( this.options.defaults[ plugin ], defaults );
  570. return this;
  571. }
  572. });
  573. Popcorn.Events = {
  574. UIEvents: "blur focus focusin focusout load resize scroll unload",
  575. MouseEvents: "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave click dblclick",
  576. Events: "loadstart progress suspend emptied stalled play pause error " +
  577. "loadedmetadata loadeddata waiting playing canplay canplaythrough " +
  578. "seeking seeked timeupdate ended ratechange durationchange volumechange"
  579. };
  580. Popcorn.Events.Natives = Popcorn.Events.UIEvents + " " +
  581. Popcorn.Events.MouseEvents + " " +
  582. Popcorn.Events.Events;
  583. internal.events.apiTypes = [ "UIEvents", "MouseEvents", "Events" ];
  584. // Privately compile events table at load time
  585. (function( events, data ) {
  586. var apis = internal.events.apiTypes,
  587. eventsList = events.Natives.split( /\s+/g ),
  588. idx = 0, len = eventsList.length, prop;
  589. for( ; idx < len; idx++ ) {
  590. data.hash[ eventsList[idx] ] = true;
  591. }
  592. apis.forEach(function( val, idx ) {
  593. data.apis[ val ] = {};
  594. var apiEvents = events[ val ].split( /\s+/g ),
  595. len = apiEvents.length,
  596. k = 0;
  597. for ( ; k < len; k++ ) {
  598. data.apis[ val ][ apiEvents[ k ] ] = true;
  599. }
  600. });
  601. })( Popcorn.Events, internal.events );
  602. Popcorn.events = {
  603. isNative: function( type ) {
  604. return !!internal.events.hash[ type ];
  605. },
  606. getInterface: function( type ) {
  607. if ( !Popcorn.events.isNative( type ) ) {
  608. return false;
  609. }
  610. var eventApi = internal.events,
  611. apis = eventApi.apiTypes,
  612. apihash = eventApi.apis,
  613. idx = 0, len = apis.length, api, tmp;
  614. for ( ; idx < len; idx++ ) {
  615. tmp = apis[ idx ];
  616. if ( apihash[ tmp ][ type ] ) {
  617. api = tmp;
  618. break;
  619. }
  620. }
  621. return api;
  622. },
  623. // Compile all native events to single array
  624. all: Popcorn.Events.Natives.split( /\s+/g ),
  625. // Defines all Event handling static functions
  626. fn: {
  627. trigger: function( type, data ) {
  628. var eventInterface, evt;
  629. // setup checks for custom event system
  630. if ( this.data.events[ type ] && Popcorn.sizeOf( this.data.events[ type ] ) ) {
  631. eventInterface = Popcorn.events.getInterface( type );
  632. if ( eventInterface ) {
  633. evt = document.createEvent( eventInterface );
  634. evt.initEvent( type, true, true, global, 1 );
  635. this.media.dispatchEvent( evt );
  636. return this;
  637. }
  638. // Custom events
  639. Popcorn.forEach( this.data.events[ type ], function( obj, key ) {
  640. obj.call( this, data );
  641. }, this );
  642. }
  643. return this;
  644. },
  645. listen: function( type, fn ) {
  646. var self = this,
  647. hasEvents = true,
  648. eventHook = Popcorn.events.hooks[ type ],
  649. origType = type,
  650. tmp;
  651. if ( !this.data.events[ type ] ) {
  652. this.data.events[ type ] = {};
  653. hasEvents = false;
  654. }
  655. // Check and setup event hooks
  656. if ( eventHook ) {
  657. // Execute hook add method if defined
  658. if ( eventHook.add ) {
  659. eventHook.add.call( this, {}, fn );
  660. }
  661. // Reassign event type to our piggyback event type if defined
  662. if ( eventHook.bind ) {
  663. type = eventHook.bind;
  664. }
  665. // Reassign handler if defined
  666. if ( eventHook.handler ) {
  667. tmp = fn;
  668. fn = function wrapper( event ) {
  669. eventHook.handler.call( self, event, tmp );
  670. };
  671. }
  672. // assume the piggy back event is registered
  673. hasEvents = true;
  674. // Setup event registry entry
  675. if ( !this.data.events[ type ] ) {
  676. this.data.events[ type ] = {};
  677. // Toggle if the previous assumption was untrue
  678. hasEvents = false;
  679. }
  680. }
  681. // Register event and handler
  682. this.data.events[ type ][ fn.name || ( fn.toString() + Popcorn.guid() ) ] = fn;
  683. // only attach one event of any type
  684. if ( !hasEvents && Popcorn.events.all.indexOf( type ) > -1 ) {
  685. this.media.addEventListener( type, function( event ) {
  686. Popcorn.forEach( self.data.events[ type ], function( obj, key ) {
  687. if ( typeof obj === "function" ) {
  688. obj.call( self, event );
  689. }
  690. });
  691. }, false);
  692. }
  693. return this;
  694. },
  695. unlisten: function( type, fn ) {
  696. if ( this.data.events[ type ] && this.data.events[ type ][ fn ] ) {
  697. delete this.data.events[ type ][ fn ];
  698. return this;
  699. }
  700. this.data.events[ type ] = null;
  701. return this;
  702. }
  703. },
  704. hooks: {
  705. canplayall: {
  706. bind: "canplaythrough",
  707. add: function( event, callback ) {
  708. var state = false;
  709. if ( this.media.readyState ) {
  710. callback.call( this, event );
  711. state = true;
  712. }
  713. this.data.hooks.canplayall = {
  714. fired: state
  715. };
  716. },
  717. // declare special handling instructions
  718. handler: function canplayall( event, callback ) {
  719. if ( !this.data.hooks.canplayall.fired ) {
  720. // trigger original user callback once
  721. callback.call( this, event );
  722. this.data.hooks.canplayall.fired = true;
  723. }
  724. }
  725. }
  726. }
  727. };
  728. // Extend Popcorn.events.fns (listen, unlisten, trigger) to all Popcorn instances
  729. // Extend aliases (on, off, emit)
  730. Popcorn.forEach( [ [ "trigger", "emit" ], [ "listen", "on" ], [ "unlisten", "off" ] ], function( key ) {
  731. Popcorn.p[ key[ 0 ] ] = Popcorn.p[ key[ 1 ] ] = Popcorn.events.fn[ key[ 0 ] ];
  732. });
  733. // Internal Only - Adds track events to the instance object
  734. Popcorn.addTrackEvent = function( obj, track ) {
  735. var trackEvent, isUpdate, eventType;
  736. // Do a lookup for existing trackevents with this id
  737. if ( track.id ) {
  738. trackEvent = obj.getTrackEvent( track.id );
  739. }
  740. // If a track event by this id currently exists, modify it
  741. if ( trackEvent ) {
  742. isUpdate = true;
  743. // Create a new object with the existing trackEvent
  744. // Extend with new track properties
  745. track = Popcorn.extend( {}, trackEvent, track );
  746. // Remove the existing track from the instance
  747. obj.removeTrackEvent( track.id );
  748. }
  749. // Determine if this track has default options set for it
  750. // If so, apply them to the track object
  751. if ( track && track._natives && track._natives.type &&
  752. ( obj.options.defaults && obj.options.defaults[ track._natives.type ] ) ) {
  753. track = Popcorn.extend( {}, obj.options.defaults[ track._natives.type ], track );
  754. }
  755. if ( track._natives ) {
  756. // Supports user defined track event id
  757. track._id = track.id || track._id || Popcorn.guid( track._natives.type );
  758. // Push track event ids into the history
  759. obj.data.history.push( track._id );
  760. }
  761. track.start = Popcorn.util.toSeconds( track.start, obj.options.framerate );
  762. track.end = Popcorn.util.toSeconds( track.end, obj.options.framerate );
  763. // Store this definition in an array sorted by times
  764. var byStart = obj.data.trackEvents.byStart,
  765. byEnd = obj.data.trackEvents.byEnd,
  766. startIndex, endIndex;
  767. for ( startIndex = byStart.length - 1; startIndex >= 0; startIndex-- ) {
  768. if ( track.start >= byStart[ startIndex ].start ) {
  769. byStart.splice( startIndex + 1, 0, track );
  770. break;
  771. }
  772. }
  773. for ( endIndex = byEnd.length - 1; endIndex >= 0; endIndex-- ) {
  774. if ( track.end > byEnd[ endIndex ].end ) {
  775. byEnd.splice( endIndex + 1, 0, track );
  776. break;
  777. }
  778. }
  779. // Display track event immediately if it's enabled and current
  780. if ( track.end > obj.media.currentTime &&
  781. track.start <= obj.media.currentTime ) {
  782. track._running = true;
  783. obj.data.running[ track._natives.type ].push( track );
  784. if ( !obj.data.disabled[ track._natives.type ] ) {
  785. track._natives.start.call( obj, null, track );
  786. }
  787. }
  788. // update startIndex and endIndex
  789. if ( startIndex <= obj.data.trackEvents.startIndex &&
  790. track.start <= obj.data.trackEvents.previousUpdateTime ) {
  791. obj.data.trackEvents.startIndex++;
  792. }
  793. if ( endIndex <= obj.data.trackEvents.endIndex &&
  794. track.end < obj.data.trackEvents.previousUpdateTime ) {
  795. obj.data.trackEvents.endIndex++;
  796. }
  797. this.timeUpdate( obj, null, true );
  798. // Store references to user added trackevents in ref table
  799. if ( track._id ) {
  800. Popcorn.addTrackEvent.ref( obj, track );
  801. }
  802. // If the call to addTrackEvent was an update/modify call, fire an event
  803. if ( isUpdate ) {
  804. // Determine appropriate event type to trigger
  805. // they are identical in function, but the naming
  806. // adds some level of intuition for the end developer
  807. // to rely on
  808. if ( track._natives.type === "cue" ) {
  809. eventType = "cuechange";
  810. } else {
  811. eventType = "trackchange";
  812. }
  813. // Fire an event with change information
  814. obj.emit( eventType, {
  815. id: track.id,
  816. previousValue: {
  817. time: trackEvent.start,
  818. fn: trackEvent._natives.start
  819. },
  820. currentValue: {
  821. time: track.start,
  822. fn: track._natives.start
  823. }
  824. });
  825. }
  826. };
  827. // Internal Only - Adds track event references to the instance object's trackRefs hash table
  828. Popcorn.addTrackEvent.ref = function( obj, track ) {
  829. obj.data.trackRefs[ track._id ] = track;
  830. return obj;
  831. };
  832. Popcorn.removeTrackEvent = function( obj, removeId ) {
  833. var start, end, animate,
  834. historyLen = obj.data.history.length,
  835. length = obj.data.trackEvents.byStart.length,
  836. index = 0,
  837. indexWasAt = 0,
  838. byStart = [],
  839. byEnd = [],
  840. animating = [],
  841. history = [];
  842. while ( --length > -1 ) {
  843. start = obj.data.trackEvents.byStart[ index ];
  844. end = obj.data.trackEvents.byEnd[ index ];
  845. // Padding events will not have _id properties.
  846. // These should be safely pushed onto the front and back of the
  847. // track event array
  848. if ( !start._id ) {
  849. byStart.push( start );
  850. byEnd.push( end );
  851. }
  852. // Filter for user track events (vs system track events)
  853. if ( start._id ) {
  854. // If not a matching start event for removal
  855. if ( start._id !== removeId ) {
  856. byStart.push( start );
  857. }
  858. // If not a matching end event for removal
  859. if ( end._id !== removeId ) {
  860. byEnd.push( end );
  861. }
  862. // If the _id is matched, capture the current index
  863. if ( start._id === removeId ) {
  864. indexWasAt = index;
  865. // If a _teardown function was defined,
  866. // enforce for track event removals
  867. if ( start._natives._teardown ) {
  868. start._natives._teardown.call( obj, start );
  869. }
  870. }
  871. }
  872. // Increment the track index
  873. index++;
  874. }
  875. // Reset length to be used by the condition below to determine
  876. // if animating track events should also be filtered for removal.
  877. // Reset index below to be used by the reverse while as an
  878. // incrementing counter
  879. length = obj.data.trackEvents.animating.length;
  880. index = 0;
  881. if ( length ) {
  882. while ( --length > -1 ) {
  883. animate = obj.data.trackEvents.animating[ index ];
  884. // Padding events will not have _id properties.
  885. // These should be safely pushed onto the front and back of the
  886. // track event array
  887. if ( !animate._id ) {
  888. animating.push( animate );
  889. }
  890. // If not a matching animate event for removal
  891. if ( animate._id && animate._id !== removeId ) {
  892. animating.push( animate );
  893. }
  894. // Increment the track index
  895. index++;
  896. }
  897. }
  898. // Update
  899. if ( indexWasAt <= obj.data.trackEvents.startIndex ) {
  900. obj.data.trackEvents.startIndex--;
  901. }
  902. if ( indexWasAt <= obj.data.trackEvents.endIndex ) {
  903. obj.data.trackEvents.endIndex--;
  904. }
  905. obj.data.trackEvents.byStart = byStart;
  906. obj.data.trackEvents.byEnd = byEnd;
  907. obj.data.trackEvents.animating = animating;
  908. for ( var i = 0; i < historyLen; i++ ) {
  909. if ( obj.data.history[ i ] !== removeId ) {
  910. history.push( obj.data.history[ i ] );
  911. }
  912. }
  913. // Update ordered history array
  914. obj.data.history = history;
  915. // Update track event references
  916. Popcorn.removeTrackEvent.ref( obj, removeId );
  917. };
  918. // Internal Only - Removes track event references from instance object's trackRefs hash table
  919. Popcorn.removeTrackEvent.ref = function( obj, removeId ) {
  920. delete obj.data.trackRefs[ removeId ];
  921. return obj;
  922. };
  923. // Return an array of track events bound to this instance object
  924. Popcorn.getTrackEvents = function( obj ) {
  925. var trackevents = [],
  926. refs = obj.data.trackEvents.byStart,
  927. length = refs.length,
  928. idx = 0,
  929. ref;
  930. for ( ; idx < length; idx++ ) {
  931. ref = refs[ idx ];
  932. // Return only user attributed track event references
  933. if ( ref._id ) {
  934. trackevents.push( ref );
  935. }
  936. }
  937. return trackevents;
  938. };
  939. // Internal Only - Returns an instance object's trackRefs hash table
  940. Popcorn.getTrackEvents.ref = function( obj ) {
  941. return obj.data.trackRefs;
  942. };
  943. // Return a single track event bound to this instance object
  944. Popcorn.getTrackEvent = function( obj, trackId ) {
  945. return obj.data.trackRefs[ trackId ];
  946. };
  947. // Internal Only - Returns an instance object's track reference by track id
  948. Popcorn.getTrackEvent.ref = function( obj, trackId ) {
  949. return obj.data.trackRefs[ trackId ];
  950. };
  951. Popcorn.getLastTrackEventId = function( obj ) {
  952. return obj.data.history[ obj.data.history.length - 1 ];
  953. };
  954. Popcorn.timeUpdate = function( obj, event ) {
  955. var currentTime = obj.media.currentTime,
  956. previousTime = obj.data.trackEvents.previousUpdateTime,
  957. tracks = obj.data.trackEvents,
  958. end = tracks.endIndex,
  959. start = tracks.startIndex,
  960. byStartLen = tracks.byStart.length,
  961. byEndLen = tracks.byEnd.length,
  962. registryByName = Popcorn.registryByName,
  963. trackstart = "trackstart",
  964. trackend = "trackend",
  965. byEnd, byStart, byAnimate, natives, type, runningPlugins;
  966. // Playbar advancing
  967. if ( previousTime <= currentTime ) {
  968. while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end <= currentTime ) {
  969. byEnd = tracks.byEnd[ end ];
  970. natives = byEnd._natives;
  971. type = natives && natives.type;
  972. // If plugin does not exist on this instance, remove it
  973. if ( !natives ||
  974. ( !!registryByName[ type ] ||
  975. !!obj[ type ] ) ) {
  976. if ( byEnd._running === true ) {
  977. byEnd._running = false;
  978. runningPlugins = obj.data.running[ type ];
  979. runningPlugins.splice( runningPlugins.indexOf( byEnd ), 1 );
  980. if ( !obj.data.disabled[ type ] ) {
  981. natives.end.call( obj, event, byEnd );
  982. obj.emit( trackend,
  983. Popcorn.extend({}, byEnd, {
  984. plugin: type,
  985. type: trackend
  986. })
  987. );
  988. }
  989. }
  990. end++;
  991. } else {
  992. // remove track event
  993. Popcorn.removeTrackEvent( obj, byEnd._id );
  994. return;
  995. }
  996. }
  997. while ( tracks.byStart[ start ] && tracks.byStart[ start ].start <= currentTime ) {
  998. byStart = tracks.byStart[ start ];
  999. natives = byStart._natives;
  1000. type = natives && natives.type;
  1001. // If plugin does not exist on this instance, remove it
  1002. if ( !natives ||
  1003. ( !!registryByName[ type ] ||
  1004. !!obj[ type ] ) ) {
  1005. if ( byStart.end > currentTime &&
  1006. byStart._running === false ) {
  1007. byStart._running = true;
  1008. obj.data.running[ type ].push( byStart );
  1009. if ( !obj.data.disabled[ type ] ) {
  1010. natives.start.call( obj, event, byStart );
  1011. obj.emit( trackstart,
  1012. Popcorn.extend({}, byStart, {
  1013. plugin: type,
  1014. type: trackstart
  1015. })
  1016. );
  1017. }
  1018. }
  1019. start++;
  1020. } else {
  1021. // remove track event
  1022. Popcorn.removeTrackEvent( obj, byStart._id );
  1023. return;
  1024. }
  1025. }
  1026. // Playbar receding
  1027. } else if ( previousTime > currentTime ) {
  1028. while ( tracks.byStart[ start ] && tracks.byStart[ start ].start > currentTime ) {
  1029. byStart = tracks.byStart[ start ];
  1030. natives = byStart._natives;
  1031. type = natives && natives.type;
  1032. // if plugin does not exist on this instance, remove it
  1033. if ( !natives ||
  1034. ( !!registryByName[ type ] ||
  1035. !!obj[ type ] ) ) {
  1036. if ( byStart._running === true ) {
  1037. byStart._running = false;
  1038. runningPlugins = obj.data.running[ type ];
  1039. runningPlugins.splice( runningPlugins.indexOf( byStart ), 1 );
  1040. if ( !obj.data.disabled[ type ] ) {
  1041. natives.end.call( obj, event, byStart );
  1042. obj.emit( trackend,
  1043. Popcorn.extend({}, byStart, {
  1044. plugin: type,
  1045. type: trackend
  1046. })
  1047. );
  1048. }
  1049. }
  1050. start--;
  1051. } else {
  1052. // remove track event
  1053. Popcorn.removeTrackEvent( obj, byStart._id );
  1054. return;
  1055. }
  1056. }
  1057. while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end > currentTime ) {
  1058. byEnd = tracks.byEnd[ end ];
  1059. natives = byEnd._natives;
  1060. type = natives && natives.type;
  1061. // if plugin does not exist on this instance, remove it
  1062. if ( !natives ||
  1063. ( !!registryByName[ type ] ||
  1064. !!obj[ type ] ) ) {
  1065. if ( byEnd.start <= currentTime &&
  1066. byEnd._running === false ) {
  1067. byEnd._running = true;
  1068. obj.data.running[ type ].push( byEnd );
  1069. if ( !obj.data.disabled[ type ] ) {
  1070. natives.start.call( obj, event, byEnd );
  1071. obj.emit( trackstart,
  1072. Popcorn.extend({}, byEnd, {
  1073. plugin: type,
  1074. type: trackstart
  1075. })
  1076. );
  1077. }
  1078. }
  1079. end--;
  1080. } else {
  1081. // remove track event
  1082. Popcorn.removeTrackEvent( obj, byEnd._id );
  1083. return;
  1084. }
  1085. }
  1086. }
  1087. tracks.endIndex = end;
  1088. tracks.startIndex = start;
  1089. tracks.previousUpdateTime = currentTime;
  1090. //enforce index integrity if trackRemoved
  1091. tracks.byStart.length < byStartLen && tracks.startIndex--;
  1092. tracks.byEnd.length < byEndLen && tracks.endIndex--;
  1093. };
  1094. // Map and Extend TrackEvent functions to all Popcorn instances
  1095. Popcorn.extend( Popcorn.p, {
  1096. getTrackEvents: function() {
  1097. return Popcorn.getTrackEvents.call( null, this );
  1098. },
  1099. getTrackEvent: function( id ) {
  1100. return Popcorn.getTrackEvent.call( null, this, id );
  1101. },
  1102. getLastTrackEventId: function() {
  1103. return Popcorn.getLastTrackEventId.call( null, this );
  1104. },
  1105. removeTrackEvent: function( id ) {
  1106. Popcorn.removeTrackEvent.call( null, this, id );
  1107. return this;
  1108. },
  1109. removePlugin: function( name ) {
  1110. Popcorn.removePlugin.call( null, this, name );
  1111. return this;
  1112. },
  1113. timeUpdate: function( event ) {
  1114. Popcorn.timeUpdate.call( null, this, event );
  1115. return this;
  1116. },
  1117. destroy: function() {
  1118. Popcorn.destroy.call( null, this );
  1119. return this;
  1120. }
  1121. });
  1122. // Plugin manifests
  1123. Popcorn.manifest = {};
  1124. // Plugins are registered
  1125. Popcorn.registry = [];
  1126. Popcorn.registryByName = {};
  1127. // An interface for extending Popcorn
  1128. // with plugin functionality
  1129. Popcorn.plugin = function( name, definition, manifest ) {
  1130. if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
  1131. Popcorn.error( "'" + name + "' is a protected function name" );
  1132. return;
  1133. }
  1134. // Provides some sugar, but ultimately extends
  1135. // the definition into Popcorn.p
  1136. var reserved = [ "start", "end" ],
  1137. plugin = {},
  1138. setup,
  1139. isfn = typeof definition === "function",
  1140. methods = [ "_setup", "_teardown", "start", "end", "frame" ];
  1141. // combines calls of two function calls into one
  1142. var combineFn = function( first, second ) {
  1143. first = first || Popcorn.nop;
  1144. second = second || Popcorn.nop;
  1145. return function() {
  1146. first.apply( this, arguments );
  1147. second.apply( this, arguments );
  1148. };
  1149. };
  1150. // If `manifest` arg is undefined, check for manifest within the `definition` object
  1151. // If no `definition.manifest`, an empty object is a sufficient fallback
  1152. Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {};
  1153. // apply safe, and empty default functions
  1154. methods.forEach(function( method ) {
  1155. definition[ method ] = safeTry( definition[ method ] || Popcorn.nop, name );
  1156. });
  1157. var pluginFn = function( setup, options ) {
  1158. if ( !options ) {
  1159. return this;
  1160. }
  1161. // When the "ranges" property is set and its value is an array, short-circuit
  1162. // the pluginFn definition to recall itself with an options object generated from
  1163. // each range object in the ranges array. (eg. { start: 15, end: 16 } )
  1164. if ( options.ranges && Popcorn.isArray(options.ranges) ) {
  1165. Popcorn.forEach( options.ranges, function( range ) {
  1166. // Create a fresh object, extend with current options
  1167. // and start/end range object's properties
  1168. // Works with in/out as well.
  1169. var opts = Popcorn.extend( {}, options, range );
  1170. // Remove the ranges property to prevent infinitely
  1171. // entering this condition
  1172. delete opts.ranges;
  1173. // Call the plugin with the newly created opts object
  1174. this[ name ]( opts );
  1175. }, this);
  1176. // Return the Popcorn instance to avoid creating an empty track event
  1177. return this;
  1178. }
  1179. // Storing the plugin natives
  1180. var natives = options._natives = {},
  1181. compose = "",
  1182. originalOpts, manifestOpts;
  1183. Popcorn.extend( natives, setup );
  1184. options._natives.type = name;
  1185. options._running = false;
  1186. natives.start = natives.start || natives[ "in" ];
  1187. natives.end = natives.end || natives[ "out" ];
  1188. if ( options.once ) {
  1189. natives.end = combineFn( natives.end, function() {
  1190. this.removeTrackEvent( options._id );
  1191. });
  1192. }
  1193. // extend teardown to always call end if running
  1194. natives._teardown = combineFn(function() {
  1195. var args = slice.call( arguments ),
  1196. runningPlugins = this.data.running[ natives.type ];
  1197. // end function signature is not the same as teardown,
  1198. // put null on the front of arguments for the event parameter
  1199. args.unshift( null );
  1200. // only call end if event is running
  1201. args[ 1 ]._running &&
  1202. runningPlugins.splice( runningPlugins.indexOf( options ), 1 ) &&
  1203. natives.end.apply( this, args );
  1204. }, natives._teardown );
  1205. // default to an empty string if no effect exists
  1206. // split string into an array of effects
  1207. options.compose = options.compose && options.compose.split( " " ) || [];
  1208. options.effect = options.effect && options.effect.split( " " ) || [];
  1209. // join the two arrays together
  1210. options.compose = options.compose.concat( options.effect );
  1211. options.compose.forEach(function( composeOption ) {
  1212. // if the requested compose is garbage, throw it away
  1213. compose = Popcorn.compositions[ composeOption ] || {};
  1214. // extends previous functions with compose function
  1215. methods.forEach(function( method ) {
  1216. natives[ method ] = combineFn( natives[ method ], compose[ method ] );
  1217. });
  1218. });
  1219. // Ensure a manifest object, an empty object is a sufficient fallback
  1220. options._natives.manifest = manifest;
  1221. // Checks for expected properties
  1222. if ( !( "start" in options ) ) {
  1223. options.start = options[ "in" ] || 0;
  1224. }
  1225. if ( !options.end && options.end !== 0 ) {
  1226. options.end = options[ "out" ] || Number.MAX_VALUE;
  1227. }
  1228. // Use hasOwn to detect non-inherited toString, since all
  1229. // objects will receive a toString - its otherwise undetectable
  1230. if ( !hasOwn.call( options, "toString" ) ) {
  1231. options.toString = function() {
  1232. var props = [
  1233. "start: " + options.start,
  1234. "end: " + options.end,
  1235. "id: " + (options.id || options._id)
  1236. ];
  1237. // Matches null and undefined, allows: false, 0, "" and truthy
  1238. if ( options.target != null ) {
  1239. props.push( "target: " + options.target );
  1240. }
  1241. return name + " ( " + props.join(", ") + " )";
  1242. };
  1243. }
  1244. // Resolves 239, 241, 242
  1245. if ( !options.target ) {
  1246. // Sometimes the manifest may be missing entirely
  1247. // or it has an options object that doesn't have a `target` property
  1248. manifestOpts = "options" in manifest && manifest.options;
  1249. options.target = manifestOpts && "target" in manifestOpts && manifestOpts.target;
  1250. }
  1251. if ( options._natives ) {
  1252. // ensure an initial id is there before setup is called
  1253. options._id = Popcorn.guid( options._natives.type );
  1254. }
  1255. // Trigger _setup method if exists
  1256. options._natives._setup && options._natives._setup.call( this, options );
  1257. // Create new track event for this instance
  1258. Popcorn.addTrackEvent( this, options );
  1259. // Future support for plugin event definitions
  1260. // for all of the native events
  1261. Popcorn.forEach( setup, function( callback, type ) {
  1262. if ( type !== "type" ) {
  1263. if ( reserved.indexOf( type ) === -1 ) {
  1264. this.on( type, callback );
  1265. }
  1266. }
  1267. }, this );
  1268. return this;
  1269. };
  1270. // Extend Popcorn.p with new named definition
  1271. // Assign new named definition
  1272. Popcorn.p[ name ] = plugin[ name ] = function( id, options ) {
  1273. var length = arguments.length,
  1274. trackEvent, defaults, mergedSetupOpts;
  1275. // Shift arguments based on use case
  1276. //
  1277. // Back compat for:
  1278. // p.plugin( options );
  1279. if ( id && !options ) {
  1280. options = id;
  1281. id = null;
  1282. } else {
  1283. // Get the trackEvent that matches the given id.
  1284. trackEvent = this.getTrackEvent( id );
  1285. // If the track event does not exist, ensure that the options
  1286. // object has a proper id
  1287. if ( !trackEvent ) {
  1288. options.id = id;
  1289. // If the track event does exist, merge the updated properties
  1290. } else {
  1291. options = Popcorn.extend( {}, trackEvent, options );
  1292. Popcorn.addTrackEvent( this, options );
  1293. return this;
  1294. }
  1295. }
  1296. this.data.running[ name ] = this.data.running[ name ] || [];
  1297. // Merge with defaults if they exist, make sure per call is prioritized
  1298. defaults = ( this.options.defaults && this.options.defaults[ name ] ) || {};
  1299. mergedSetupOpts = Popcorn.extend( {}, defaults, options );
  1300. return pluginFn.call( this, isfn ? definition.call( this, mergedSetupOpts ) : definition,
  1301. mergedSetupOpts );
  1302. };
  1303. // if the manifest parameter exists we should extend it onto the definition object
  1304. // so that it shows up when calling Popcorn.registry and Popcorn.registryByName
  1305. if ( manifest ) {
  1306. Popcorn.extend( definition, {
  1307. manifest: manifest
  1308. });
  1309. }
  1310. // Push into the registry
  1311. var entry = {
  1312. fn: plugin[ name ],
  1313. definition: definition,
  1314. base: definition,
  1315. parents: [],
  1316. name: name
  1317. };
  1318. Popcorn.registry.push(
  1319. Popcorn.extend( plugin, entry, {
  1320. type: name
  1321. })
  1322. );
  1323. Popcorn.registryByName[ name ] = entry;
  1324. return plugin;
  1325. };
  1326. // Storage for plugin function errors
  1327. Popcorn.plugin.errors = [];
  1328. // Returns wrapped plugin function
  1329. function safeTry( fn, pluginName ) {
  1330. return function() {
  1331. // When Popcorn.plugin.debug is true, do not suppress errors
  1332. if ( Popcorn.plugin.debug ) {
  1333. return fn.apply( this, arguments );
  1334. }
  1335. try {
  1336. return fn.apply( this, arguments );
  1337. } catch ( ex ) {
  1338. // Push plugin function errors into logging queue
  1339. Popcorn.plugin.errors.push({
  1340. plugin: pluginName,
  1341. thrown: ex,
  1342. source: fn.toString()
  1343. });
  1344. // Trigger an error that the instance can listen for
  1345. // and react to
  1346. this.emit( "pluginerror", Popcorn.plugin.errors );
  1347. }
  1348. };
  1349. }
  1350. // Debug-mode flag for plugin development
  1351. // True for Popcorn development versions, false for stable/tagged versions
  1352. Popcorn.plugin.debug = ( Popcorn.version === "@" + "VERSION" );
  1353. // removePlugin( type ) removes all tracks of that from all instances of popcorn
  1354. // removePlugin( obj, type ) removes all tracks of type from obj, where obj is a single instance of popcorn
  1355. Popcorn.removePlugin = function( obj, name ) {
  1356. // Check if we are removing plugin from an instance or from all of Popcorn
  1357. if ( !name ) {
  1358. // Fix the order
  1359. name = obj;
  1360. obj = Popcorn.p;
  1361. if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
  1362. Popcorn.error( "'" + name + "' is a protected function name" );
  1363. return;
  1364. }
  1365. var registryLen = Popcorn.registry.length,
  1366. registryIdx;
  1367. // remove plugin reference from registry
  1368. for ( registryIdx = 0; registryIdx < registryLen; registryIdx++ ) {
  1369. if ( Popcorn.registry[ registryIdx ].name === name ) {
  1370. Popcorn.registry.splice( registryIdx, 1 );
  1371. delete Popcorn.registryByName[ name ];
  1372. delete Popcorn.manifest[ name ];
  1373. // delete the plugin
  1374. delete obj[ name ];
  1375. // plugin found and removed, stop checking, we are done
  1376. return;
  1377. }
  1378. }
  1379. }
  1380. var byStart = obj.data.trackEvents.byStart,
  1381. byEnd = obj.data.trackEvents.byEnd,
  1382. animating = obj.data.trackEvents.animating,
  1383. idx, sl;
  1384. // remove all trackEvents
  1385. for ( idx = 0, sl = byStart.length; idx < sl; idx++ ) {
  1386. if ( byStart[ idx ] && byStart[ idx ]._natives && byStart[ idx ]._natives.type === name ) {
  1387. byStart[ idx ]._natives._teardown && byStart[ idx ]._natives._teardown.call( obj, byStart[ idx ] );
  1388. byStart.splice( idx, 1 );
  1389. // update for loop if something removed, but keep checking
  1390. idx--; sl--;
  1391. if ( obj.data.trackEvents.startIndex <= idx ) {
  1392. obj.data.trackEvents.startIndex--;
  1393. obj.data.trackEvents.endIndex--;
  1394. }
  1395. }
  1396. // clean any remaining references in the end index
  1397. // we do this seperate from the above check because they might not be in the same order
  1398. if ( byEnd[ idx ] && byEnd[ idx ]._natives && byEnd[ idx ]._natives.type === name ) {
  1399. byEnd.splice( idx, 1 );
  1400. }
  1401. }
  1402. //remove all animating events
  1403. for ( idx = 0, sl = animating.length; idx < sl; idx++ ) {
  1404. if ( animating[ idx ] && animating[ idx ]._natives && animating[ idx ]._natives.type === name ) {
  1405. animating.splice( idx, 1 );
  1406. // update for loop if something removed, but keep checking
  1407. idx--; sl--;
  1408. }
  1409. }
  1410. };
  1411. Popcorn.compositions = {};
  1412. // Plugin inheritance
  1413. Popcorn.compose = function( name, definition, manifest ) {
  1414. // If `manifest` arg is undefined, check for manifest within the `definition` object
  1415. // If no `definition.manifest`, an empty object is a sufficient fallback
  1416. Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {};
  1417. // register the effect by name
  1418. Popcorn.compositions[ name ] = definition;
  1419. };
  1420. Popcorn.plugin.effect = Popcorn.effect = Popcorn.compose;
  1421. var rnaiveExpr = /^(?:\.|#|\[)/;
  1422. // Basic DOM utilities and helpers API. See #1037
  1423. Popcorn.dom = {
  1424. debug: false,
  1425. // Popcorn.dom.find( selector, context )
  1426. //
  1427. // Returns the first element that matches the specified selector
  1428. // Optionally provide a context element, defaults to `document`
  1429. //
  1430. // eg.
  1431. // Popcorn.dom.find("video") returns the first video element
  1432. // Popcorn.dom.find("#foo") returns the first element with `id="foo"`
  1433. // Popcorn.dom.find("foo") returns the first element with `id="foo"`
  1434. // Note: Popcorn.dom.find("foo") is the only allowed deviation
  1435. // from valid querySelector selector syntax
  1436. //
  1437. // Popcorn.dom.find(".baz") returns the first element with `class="baz"`
  1438. // Popcorn.dom.find("[preload]") returns the first element with `preload="..."`
  1439. // ...
  1440. // See https://developer.mozilla.org/En/DOM/Document.querySelector
  1441. //
  1442. //
  1443. find: function( selector, context ) {
  1444. var node = null;
  1445. // Trim leading/trailing whitespace to avoid false negatives
  1446. selector = selector.trim();
  1447. // Default context is the `document`
  1448. context = context || document;
  1449. if ( selector ) {
  1450. // If the selector does not begin with "#", "." or "[",
  1451. // it could be either a nodeName or ID w/o "#"
  1452. if ( !rnaiveExpr.test( selector ) ) {
  1453. // Try finding an element that matches by ID first
  1454. node = document.getElementById( selector );
  1455. // If a match was found by ID, return the element
  1456. if ( node !== null ) {
  1457. return node;
  1458. }
  1459. }
  1460. // Assume no elements have been found yet
  1461. // Catch any invalid selector syntax errors and bury them.
  1462. try {
  1463. node = context.querySelector( selector );
  1464. } catch ( e ) {
  1465. if ( Popcorn.dom.debug ) {
  1466. throw new Error(e);
  1467. }
  1468. }
  1469. }
  1470. return node;
  1471. }
  1472. };
  1473. // Cache references to reused RegExps
  1474. var rparams = /\?/,
  1475. // XHR Setup object
  1476. setup = {
  1477. url: "",
  1478. data: "",
  1479. dataType: "",
  1480. success: Popcorn.nop,
  1481. type: "GET",
  1482. async: true,
  1483. xhr: function() {
  1484. return new global.XMLHttpRequest();
  1485. }
  1486. };
  1487. Popcorn.xhr = function( options ) {
  1488. options.dataType = options.dataType && options.dataType.toLowerCase() || null;
  1489. if ( options.dataType &&
  1490. ( options.dataType === "jsonp" || options.dataType === "script" ) ) {
  1491. Popcorn.xhr.getJSONP(
  1492. options.url,
  1493. options.success,
  1494. options.dataType === "script"
  1495. );
  1496. return;
  1497. }
  1498. var settings = Popcorn.extend( {}, setup, options );
  1499. // Create new XMLHttpRequest object
  1500. settings.ajax = settings.xhr();
  1501. if ( settings.ajax ) {
  1502. if ( settings.type === "GET" && settings.data ) {
  1503. // append query string
  1504. settings.url += ( rparams.test( settings.url ) ? "&" : "?" ) + settings.data;
  1505. // Garbage collect and reset settings.data
  1506. settings.data = null;
  1507. }
  1508. settings.ajax.open( settings.type, settings.url, settings.async );
  1509. settings.ajax.send( settings.data || null );
  1510. return Popcorn.xhr.httpData( settings );
  1511. }
  1512. };
  1513. Popcorn.xhr.httpData = function( settings ) {
  1514. var data, json = null,
  1515. parser, xml = null;
  1516. settings.ajax.onreadystatechange = function() {
  1517. if ( settings.ajax.readyState === 4 ) {
  1518. try {
  1519. json = JSON.parse( settings.ajax.responseText );
  1520. } catch( e ) {
  1521. //suppress
  1522. }
  1523. data = {
  1524. xml: settings.ajax.responseXML,
  1525. text: settings.ajax.responseText,
  1526. json: json
  1527. };
  1528. // Normalize: data.xml is non-null in IE9 regardless of if response is valid xml
  1529. if ( !data.xml || !data.xml.documentElement ) {
  1530. data.xml = null;
  1531. try {
  1532. parser = new DOMParser();
  1533. xml = parser.parseFromString( settings.ajax.responseText, "text/xml" );
  1534. if ( !xml.getElementsByTagName( "parsererror" ).length ) {
  1535. data.xml = xml;
  1536. }
  1537. } catch ( e ) {
  1538. // data.xml remains null
  1539. }
  1540. }
  1541. // If a dataType was specified, return that type of data
  1542. if ( settings.dataType ) {
  1543. data = data[ settings.dataType ];
  1544. }
  1545. settings.success.call( settings.ajax, data );
  1546. }
  1547. };
  1548. return data;
  1549. };
  1550. Popcorn.xhr.getJSONP = function( url, success, isScript ) {
  1551. var head = document.head || document.getElementsByTagName( "head" )[ 0 ] || document.documentElement,
  1552. script = document.createElement( "script" ),
  1553. isFired = false,
  1554. params = [],
  1555. rjsonp = /(=)\?(?=&|$)|\?\?/,
  1556. replaceInUrl, prefix, paramStr, callback, callparam;
  1557. if ( !isScript ) {
  1558. // is there a calback already in the url
  1559. callparam = url.match( /(callback=[^&]*)/ );
  1560. if ( callparam !== null && callparam.length ) {
  1561. prefix = callparam[ 1 ].split( "=" )[ 1 ];
  1562. // Since we need to support developer specified callbacks
  1563. // and placeholders in harmony, make sure matches to "callback="
  1564. // aren't just placeholders.
  1565. // We coded ourselves into a corner here.
  1566. // JSONP callbacks should never have been
  1567. // allowed to have developer specified callbacks
  1568. if ( prefix === "?" ) {
  1569. prefix = "jsonp";
  1570. }
  1571. // get the callback name
  1572. callback = Popcorn.guid( prefix );
  1573. // replace existing callback name with unique callback name
  1574. url = url.replace( /(callback=[^&]*)/, "callback=" + callback );
  1575. } else {
  1576. callback = Popcorn.guid( "jsonp" );
  1577. if ( rjsonp.test( url ) ) {
  1578. url = url.replace( rjsonp, "$1" + callback );
  1579. }
  1580. // split on first question mark,
  1581. // this is to capture the query string
  1582. params = url.split( /\?(.+)?/ );
  1583. // rebuild url with callback
  1584. url = params[ 0 ] + "?";
  1585. if ( params[ 1 ] ) {
  1586. url += params[ 1 ] + "&";
  1587. }
  1588. url += "callback=" + callback;
  1589. }
  1590. // Define the JSONP success callback globally
  1591. window[ callback ] = function( data ) {
  1592. // Fire success callbacks
  1593. success && success( data );
  1594. isFired = true;
  1595. };
  1596. }
  1597. script.addEventListener( "load", function() {
  1598. // Handling remote script loading callbacks
  1599. if ( isScript ) {
  1600. // getScript
  1601. success && success();
  1602. }
  1603. // Executing for JSONP requests
  1604. if ( isFired ) {
  1605. // Garbage collect the callback
  1606. delete window[ callback ];
  1607. }
  1608. // Garbage collect the script resource
  1609. head.removeChild( script );
  1610. }, false );
  1611. script.src = url;
  1612. head.insertBefore( script, head.firstChild );
  1613. return;
  1614. };
  1615. Popcorn.getJSONP = Popcorn.xhr.getJSONP;
  1616. Popcorn.getScript = Popcorn.xhr.getScript = function( url, success ) {
  1617. return Popcorn.xhr.getJSONP( url, success, true );
  1618. };
  1619. Popcorn.util = {
  1620. // Simple function to parse a timestamp into seconds
  1621. // Acceptable formats are:
  1622. // HH:MM:SS.MMM
  1623. // HH:MM:SS;FF
  1624. // Hours and minutes are optional. They default to 0
  1625. toSeconds: function( timeStr, framerate ) {
  1626. // Hours and minutes are optional
  1627. // Seconds must be specified
  1628. // Seconds can be followed by milliseconds OR by the frame information
  1629. var validTimeFormat = /^([0-9]+:){0,2}[0-9]+([.;][0-9]+)?$/,
  1630. errorMessage = "Invalid time format",
  1631. digitPairs, lastIndex, lastPair, firstPair,
  1632. frameInfo, frameTime;
  1633. if ( typeof timeStr === "number" ) {
  1634. return timeStr;
  1635. }
  1636. if ( typeof timeStr === "string" &&
  1637. !validTimeFormat.test( timeStr ) ) {
  1638. Popcorn.error( errorMessage );
  1639. }
  1640. digitPairs = timeStr.split( ":" );
  1641. lastIndex = digitPairs.length - 1;
  1642. lastPair = digitPairs[ lastIndex ];
  1643. // Fix last element:
  1644. if ( lastPair.indexOf( ";" ) > -1 ) {
  1645. frameInfo = lastPair.split( ";" );
  1646. frameTime = 0;
  1647. if ( framerate && ( typeof framerate === "number" ) ) {
  1648. frameTime = parseFloat( frameInfo[ 1 ], 10 ) / framerate;
  1649. }
  1650. digitPairs[ lastIndex ] = parseInt( frameInfo[ 0 ], 10 ) + frameTime;
  1651. }
  1652. firstPair = digitPairs[ 0 ];
  1653. return {
  1654. 1: parseFloat( firstPair, 10 ),
  1655. 2: ( parseInt( firstPair, 10 ) * 60 ) +
  1656. parseFloat( digitPairs[ 1 ], 10 ),
  1657. 3: ( parseInt( firstPair, 10 ) * 3600 ) +
  1658. ( parseInt( digitPairs[ 1 ], 10 ) * 60 ) +
  1659. parseFloat( digitPairs[ 2 ], 10 )
  1660. }[ digitPairs.length || 1 ];
  1661. }
  1662. };
  1663. // alias for exec function
  1664. Popcorn.p.cue = Popcorn.p.exec;
  1665. // Protected API methods
  1666. Popcorn.protect = {
  1667. natives: getKeys( Popcorn.p ).map(function( val ) {
  1668. return val.toLowerCase();
  1669. })
  1670. };
  1671. // Setup logging for deprecated methods
  1672. Popcorn.forEach({
  1673. // Deprecated: Recommended
  1674. "listen": "on",
  1675. "unlisten": "off",
  1676. "trigger": "emit",
  1677. "exec": "cue"
  1678. }, function( recommend, api ) {
  1679. var original = Popcorn.p[ api ];
  1680. // Override the deprecated api method with a method of the same name
  1681. // that logs a warning and defers to the new recommended method
  1682. Popcorn.p[ api ] = function() {
  1683. if ( typeof console !== "undefined" && console.warn ) {
  1684. console.warn(
  1685. "Deprecated method '" + api + "', " +
  1686. (recommend == null ? "do not use." : "use '" + recommend + "' instead." )
  1687. );
  1688. // Restore api after first warning
  1689. Popcorn.p[ api ] = original;
  1690. }
  1691. return Popcorn.p[ recommend ].apply( this, [].slice.call( arguments ) );
  1692. };
  1693. });
  1694. // Exposes Popcorn to global context
  1695. global.Popcorn = Popcorn;
  1696. })(window, window.document);