PageRenderTime 49ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 1ms

/static/popcorn/popcorn-complete.js

https://github.com/CodeCosmos/codecosmos.github.io
JavaScript | 9167 lines | 5793 code | 1632 blank | 1742 comment | 948 complexity | 8233cb13ba5c8f9a4a1b7108c1a76223 MD5 | raw file
Possible License(s): BSD-3-Clause, BSD-2-Clause, Apache-2.0, MIT, GPL-3.0
  1. /*
  2. * popcorn.js version 1.3
  3. * http://popcornjs.org
  4. *
  5. * Copyright 2011, Mozilla Foundation
  6. * Licensed under the MIT license
  7. */
  8. (function(global, document) {
  9. // Popcorn.js does not support archaic browsers
  10. if ( !document.addEventListener ) {
  11. global.Popcorn = {
  12. isSupported: false
  13. };
  14. var methods = ( "byId forEach extend effects error guid sizeOf isArray nop position disable enable destroy" +
  15. "addTrackEvent removeTrackEvent getTrackEvents getTrackEvent getLastTrackEventId " +
  16. "timeUpdate plugin removePlugin compose effect xhr getJSONP getScript" ).split(/\s+/);
  17. while ( methods.length ) {
  18. global.Popcorn[ methods.shift() ] = function() {};
  19. }
  20. return;
  21. }
  22. var
  23. AP = Array.prototype,
  24. OP = Object.prototype,
  25. forEach = AP.forEach,
  26. slice = AP.slice,
  27. hasOwn = OP.hasOwnProperty,
  28. toString = OP.toString,
  29. // Copy global Popcorn (may not exist)
  30. _Popcorn = global.Popcorn,
  31. // Ready fn cache
  32. readyStack = [],
  33. readyBound = false,
  34. readyFired = false,
  35. // Non-public internal data object
  36. internal = {
  37. events: {
  38. hash: {},
  39. apis: {}
  40. }
  41. },
  42. // Non-public `requestAnimFrame`
  43. // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
  44. requestAnimFrame = (function(){
  45. return global.requestAnimationFrame ||
  46. global.webkitRequestAnimationFrame ||
  47. global.mozRequestAnimationFrame ||
  48. global.oRequestAnimationFrame ||
  49. global.msRequestAnimationFrame ||
  50. function( callback, element ) {
  51. global.setTimeout( callback, 16 );
  52. };
  53. }()),
  54. // Non-public `getKeys`, return an object's keys as an array
  55. getKeys = function( obj ) {
  56. return Object.keys ? Object.keys( obj ) : (function( obj ) {
  57. var item,
  58. list = [];
  59. for ( item in obj ) {
  60. if ( hasOwn.call( obj, item ) ) {
  61. list.push( item );
  62. }
  63. }
  64. return list;
  65. })( obj );
  66. },
  67. // Declare constructor
  68. // Returns an instance object.
  69. Popcorn = function( entity, options ) {
  70. // Return new Popcorn object
  71. return new Popcorn.p.init( entity, options || null );
  72. };
  73. // Popcorn API version, automatically inserted via build system.
  74. Popcorn.version = "1.3";
  75. // Boolean flag allowing a client to determine if Popcorn can be supported
  76. Popcorn.isSupported = true;
  77. // Instance caching
  78. Popcorn.instances = [];
  79. // Declare a shortcut (Popcorn.p) to and a definition of
  80. // the new prototype for our Popcorn constructor
  81. Popcorn.p = Popcorn.prototype = {
  82. init: function( entity, options ) {
  83. var matches, nodeName,
  84. self = this;
  85. // Supports Popcorn(function () { /../ })
  86. // Originally proposed by Daniel Brooks
  87. if ( typeof entity === "function" ) {
  88. // If document ready has already fired
  89. if ( document.readyState === "complete" ) {
  90. entity( document, Popcorn );
  91. return;
  92. }
  93. // Add `entity` fn to ready stack
  94. readyStack.push( entity );
  95. // This process should happen once per page load
  96. if ( !readyBound ) {
  97. // set readyBound flag
  98. readyBound = true;
  99. var DOMContentLoaded = function() {
  100. readyFired = true;
  101. // Remove global DOM ready listener
  102. document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
  103. // Execute all ready function in the stack
  104. for ( var i = 0, readyStackLength = readyStack.length; i < readyStackLength; i++ ) {
  105. readyStack[ i ].call( document, Popcorn );
  106. }
  107. // GC readyStack
  108. readyStack = null;
  109. };
  110. // Register global DOM ready listener
  111. document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
  112. }
  113. return;
  114. }
  115. if ( typeof entity === "string" ) {
  116. try {
  117. matches = document.querySelector( entity );
  118. } catch( e ) {
  119. throw new Error( "Popcorn.js Error: Invalid media element selector: " + entity );
  120. }
  121. }
  122. // Get media element by id or object reference
  123. this.media = matches || entity;
  124. // inner reference to this media element's nodeName string value
  125. nodeName = ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video";
  126. // Create an audio or video element property reference
  127. this[ nodeName ] = this.media;
  128. this.options = options || {};
  129. // Resolve custom ID or default prefixed ID
  130. this.id = this.options.id || Popcorn.guid( nodeName );
  131. // Throw if an attempt is made to use an ID that already exists
  132. if ( Popcorn.byId( this.id ) ) {
  133. throw new Error( "Popcorn.js Error: Cannot use duplicate ID (" + this.id + ")" );
  134. }
  135. this.isDestroyed = false;
  136. this.data = {
  137. // data structure of all
  138. running: {
  139. cue: []
  140. },
  141. // Executed by either timeupdate event or in rAF loop
  142. timeUpdate: Popcorn.nop,
  143. // Allows disabling a plugin per instance
  144. disabled: {},
  145. // Stores DOM event queues by type
  146. events: {},
  147. // Stores Special event hooks data
  148. hooks: {},
  149. // Store track event history data
  150. history: [],
  151. // Stores ad-hoc state related data]
  152. state: {
  153. volume: this.media.volume
  154. },
  155. // Store track event object references by trackId
  156. trackRefs: {},
  157. // Playback track event queues
  158. trackEvents: {
  159. byStart: [{
  160. start: -1,
  161. end: -1
  162. }],
  163. byEnd: [{
  164. start: -1,
  165. end: -1
  166. }],
  167. animating: [],
  168. startIndex: 0,
  169. endIndex: 0,
  170. previousUpdateTime: -1
  171. }
  172. };
  173. // Register new instance
  174. Popcorn.instances.push( this );
  175. // function to fire when video is ready
  176. var isReady = function() {
  177. // chrome bug: http://code.google.com/p/chromium/issues/detail?id=119598
  178. // it is possible the video's time is less than 0
  179. // this has the potential to call track events more than once, when they should not
  180. // start: 0, end: 1 will start, end, start again, when it should just start
  181. // just setting it to 0 if it is below 0 fixes this issue
  182. if ( self.media.currentTime < 0 ) {
  183. self.media.currentTime = 0;
  184. }
  185. self.media.removeEventListener( "loadeddata", isReady, false );
  186. var duration, videoDurationPlus,
  187. runningPlugins, runningPlugin, rpLength, rpNatives;
  188. // Adding padding to the front and end of the arrays
  189. // this is so we do not fall off either end
  190. duration = self.media.duration;
  191. // Check for no duration info (NaN)
  192. videoDurationPlus = duration != duration ? Number.MAX_VALUE : duration + 1;
  193. Popcorn.addTrackEvent( self, {
  194. start: videoDurationPlus,
  195. end: videoDurationPlus
  196. });
  197. if ( self.options.frameAnimation ) {
  198. // if Popcorn is created with frameAnimation option set to true,
  199. // requestAnimFrame is used instead of "timeupdate" media event.
  200. // This is for greater frame time accuracy, theoretically up to
  201. // 60 frames per second as opposed to ~4 ( ~every 15-250ms)
  202. self.data.timeUpdate = function () {
  203. Popcorn.timeUpdate( self, {} );
  204. // fire frame for each enabled active plugin of every type
  205. Popcorn.forEach( Popcorn.manifest, function( key, val ) {
  206. runningPlugins = self.data.running[ val ];
  207. // ensure there are running plugins on this type on this instance
  208. if ( runningPlugins ) {
  209. rpLength = runningPlugins.length;
  210. for ( var i = 0; i < rpLength; i++ ) {
  211. runningPlugin = runningPlugins[ i ];
  212. rpNatives = runningPlugin._natives;
  213. rpNatives && rpNatives.frame &&
  214. rpNatives.frame.call( self, {}, runningPlugin, self.currentTime() );
  215. }
  216. }
  217. });
  218. self.emit( "timeupdate" );
  219. !self.isDestroyed && requestAnimFrame( self.data.timeUpdate );
  220. };
  221. !self.isDestroyed && requestAnimFrame( self.data.timeUpdate );
  222. } else {
  223. self.data.timeUpdate = function( event ) {
  224. Popcorn.timeUpdate( self, event );
  225. };
  226. if ( !self.isDestroyed ) {
  227. self.media.addEventListener( "timeupdate", self.data.timeUpdate, false );
  228. }
  229. }
  230. };
  231. Object.defineProperty( this, "error", {
  232. get: function() {
  233. return self.media.error;
  234. }
  235. });
  236. if ( self.media.readyState >= 2 ) {
  237. isReady();
  238. } else {
  239. self.media.addEventListener( "loadeddata", isReady, false );
  240. }
  241. return this;
  242. }
  243. };
  244. // Extend constructor prototype to instance prototype
  245. // Allows chaining methods to instances
  246. Popcorn.p.init.prototype = Popcorn.p;
  247. Popcorn.byId = function( str ) {
  248. var instances = Popcorn.instances,
  249. length = instances.length,
  250. i = 0;
  251. for ( ; i < length; i++ ) {
  252. if ( instances[ i ].id === str ) {
  253. return instances[ i ];
  254. }
  255. }
  256. return null;
  257. };
  258. Popcorn.forEach = function( obj, fn, context ) {
  259. if ( !obj || !fn ) {
  260. return {};
  261. }
  262. context = context || this;
  263. var key, len;
  264. // Use native whenever possible
  265. if ( forEach && obj.forEach === forEach ) {
  266. return obj.forEach( fn, context );
  267. }
  268. if ( toString.call( obj ) === "[object NodeList]" ) {
  269. for ( key = 0, len = obj.length; key < len; key++ ) {
  270. fn.call( context, obj[ key ], key, obj );
  271. }
  272. return obj;
  273. }
  274. for ( key in obj ) {
  275. if ( hasOwn.call( obj, key ) ) {
  276. fn.call( context, obj[ key ], key, obj );
  277. }
  278. }
  279. return obj;
  280. };
  281. Popcorn.extend = function( obj ) {
  282. var dest = obj, src = slice.call( arguments, 1 );
  283. Popcorn.forEach( src, function( copy ) {
  284. for ( var prop in copy ) {
  285. dest[ prop ] = copy[ prop ];
  286. }
  287. });
  288. return dest;
  289. };
  290. // A Few reusable utils, memoized onto Popcorn
  291. Popcorn.extend( Popcorn, {
  292. noConflict: function( deep ) {
  293. if ( deep ) {
  294. global.Popcorn = _Popcorn;
  295. }
  296. return Popcorn;
  297. },
  298. error: function( msg ) {
  299. throw new Error( msg );
  300. },
  301. guid: function( prefix ) {
  302. Popcorn.guid.counter++;
  303. return ( prefix ? prefix : "" ) + ( +new Date() + Popcorn.guid.counter );
  304. },
  305. sizeOf: function( obj ) {
  306. var size = 0;
  307. for ( var prop in obj ) {
  308. size++;
  309. }
  310. return size;
  311. },
  312. isArray: Array.isArray || function( array ) {
  313. return toString.call( array ) === "[object Array]";
  314. },
  315. nop: function() {},
  316. position: function( elem ) {
  317. var clientRect = elem.getBoundingClientRect(),
  318. bounds = {},
  319. doc = elem.ownerDocument,
  320. docElem = document.documentElement,
  321. body = document.body,
  322. clientTop, clientLeft, scrollTop, scrollLeft, top, left;
  323. // Determine correct clientTop/Left
  324. clientTop = docElem.clientTop || body.clientTop || 0;
  325. clientLeft = docElem.clientLeft || body.clientLeft || 0;
  326. // Determine correct scrollTop/Left
  327. scrollTop = ( global.pageYOffset && docElem.scrollTop || body.scrollTop );
  328. scrollLeft = ( global.pageXOffset && docElem.scrollLeft || body.scrollLeft );
  329. // Temp top/left
  330. top = Math.ceil( clientRect.top + scrollTop - clientTop );
  331. left = Math.ceil( clientRect.left + scrollLeft - clientLeft );
  332. for ( var p in clientRect ) {
  333. bounds[ p ] = Math.round( clientRect[ p ] );
  334. }
  335. return Popcorn.extend({}, bounds, { top: top, left: left });
  336. },
  337. disable: function( instance, plugin ) {
  338. if ( !instance.data.disabled[ plugin ] ) {
  339. instance.data.disabled[ plugin ] = true;
  340. for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) {
  341. event = instance.data.running[ plugin ][ i ];
  342. event._natives.end.call( instance, null, event );
  343. }
  344. }
  345. return instance;
  346. },
  347. enable: function( instance, plugin ) {
  348. if ( instance.data.disabled[ plugin ] ) {
  349. instance.data.disabled[ plugin ] = false;
  350. for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) {
  351. event = instance.data.running[ plugin ][ i ];
  352. event._natives.start.call( instance, null, event );
  353. }
  354. }
  355. return instance;
  356. },
  357. destroy: function( instance ) {
  358. var events = instance.data.events,
  359. trackEvents = instance.data.trackEvents,
  360. singleEvent, item, fn, plugin;
  361. // Iterate through all events and remove them
  362. for ( item in events ) {
  363. singleEvent = events[ item ];
  364. for ( fn in singleEvent ) {
  365. delete singleEvent[ fn ];
  366. }
  367. events[ item ] = null;
  368. }
  369. // remove all plugins off the given instance
  370. for ( plugin in Popcorn.registryByName ) {
  371. Popcorn.removePlugin( instance, plugin );
  372. }
  373. // Remove all data.trackEvents #1178
  374. trackEvents.byStart.length = 0;
  375. trackEvents.byEnd.length = 0;
  376. if ( !instance.isDestroyed ) {
  377. instance.data.timeUpdate && instance.media.removeEventListener( "timeupdate", instance.data.timeUpdate, false );
  378. instance.isDestroyed = true;
  379. }
  380. }
  381. });
  382. // Memoized GUID Counter
  383. Popcorn.guid.counter = 1;
  384. // Factory to implement getters, setters and controllers
  385. // as Popcorn instance methods. The IIFE will create and return
  386. // an object with defined methods
  387. Popcorn.extend(Popcorn.p, (function() {
  388. var methods = "load play pause currentTime playbackRate volume duration preload playbackRate " +
  389. "autoplay loop controls muted buffered readyState seeking paused played seekable ended",
  390. ret = {};
  391. // Build methods, store in object that is returned and passed to extend
  392. Popcorn.forEach( methods.split( /\s+/g ), function( name ) {
  393. ret[ name ] = function( arg ) {
  394. var previous;
  395. if ( typeof this.media[ name ] === "function" ) {
  396. // Support for shorthanded play(n)/pause(n) jump to currentTime
  397. // If arg is not null or undefined and called by one of the
  398. // allowed shorthandable methods, then set the currentTime
  399. // Supports time as seconds or SMPTE
  400. if ( arg != null && /play|pause/.test( name ) ) {
  401. this.media.currentTime = Popcorn.util.toSeconds( arg );
  402. }
  403. this.media[ name ]();
  404. return this;
  405. }
  406. if ( arg != null ) {
  407. // Capture the current value of the attribute property
  408. previous = this.media[ name ];
  409. // Set the attribute property with the new value
  410. this.media[ name ] = arg;
  411. // If the new value is not the same as the old value
  412. // emit an "attrchanged event"
  413. if ( previous !== arg ) {
  414. this.emit( "attrchange", {
  415. attribute: name,
  416. previousValue: previous,
  417. currentValue: arg
  418. });
  419. }
  420. return this;
  421. }
  422. return this.media[ name ];
  423. };
  424. });
  425. return ret;
  426. })()
  427. );
  428. Popcorn.forEach( "enable disable".split(" "), function( method ) {
  429. Popcorn.p[ method ] = function( plugin ) {
  430. return Popcorn[ method ]( this, plugin );
  431. };
  432. });
  433. Popcorn.extend(Popcorn.p, {
  434. // Rounded currentTime
  435. roundTime: function() {
  436. return Math.round( this.media.currentTime );
  437. },
  438. // Attach an event to a single point in time
  439. exec: function( id, time, fn ) {
  440. var length = arguments.length,
  441. trackEvent, sec;
  442. // Check if first could possibly be a SMPTE string
  443. // p.cue( "smpte string", fn );
  444. // try/catch avoid awful throw in Popcorn.util.toSeconds
  445. // TODO: Get rid of that, replace with NaN return?
  446. try {
  447. sec = Popcorn.util.toSeconds( id );
  448. } catch ( e ) {}
  449. // If it can be converted into a number then
  450. // it's safe to assume that the string was SMPTE
  451. if ( typeof sec === "number" ) {
  452. id = sec;
  453. }
  454. // Shift arguments based on use case
  455. //
  456. // Back compat for:
  457. // p.cue( time, fn );
  458. if ( typeof id === "number" && length === 2 ) {
  459. fn = time;
  460. time = id;
  461. id = Popcorn.guid( "cue" );
  462. } else {
  463. // Support for new forms
  464. // p.cue( "empty-cue" );
  465. if ( length === 1 ) {
  466. // Set a time for an empty cue. It's not important what
  467. // the time actually is, because the cue is a no-op
  468. time = -1;
  469. } else {
  470. // Get the trackEvent that matches the given id.
  471. trackEvent = this.getTrackEvent( id );
  472. if ( trackEvent ) {
  473. // p.cue( "my-id", 12 );
  474. // p.cue( "my-id", function() { ... });
  475. if ( typeof id === "string" && length === 2 ) {
  476. // p.cue( "my-id", 12 );
  477. // The path will update the cue time.
  478. if ( typeof time === "number" ) {
  479. // Re-use existing trackEvent start callback
  480. fn = trackEvent._natives.start;
  481. }
  482. // p.cue( "my-id", function() { ... });
  483. // The path will update the cue function
  484. if ( typeof time === "function" ) {
  485. fn = time;
  486. // Re-use existing trackEvent start time
  487. time = trackEvent.start;
  488. }
  489. }
  490. } else {
  491. if ( length >= 2 ) {
  492. // p.cue( "a", "00:00:00");
  493. if ( typeof time === "string" ) {
  494. try {
  495. sec = Popcorn.util.toSeconds( time );
  496. } catch ( e ) {}
  497. time = sec;
  498. }
  499. // p.cue( "b", 11 );
  500. if ( typeof time === "number" ) {
  501. fn = Popcorn.nop();
  502. }
  503. // p.cue( "c", function() {});
  504. if ( typeof time === "function" ) {
  505. fn = time;
  506. time = -1;
  507. }
  508. }
  509. }
  510. }
  511. }
  512. // Creating a one second track event with an empty end
  513. // Or update an existing track event with new values
  514. Popcorn.addTrackEvent( this, {
  515. id: id,
  516. start: time,
  517. end: time + 1,
  518. _running: false,
  519. _natives: {
  520. start: fn || Popcorn.nop,
  521. end: Popcorn.nop,
  522. type: "cue"
  523. }
  524. });
  525. return this;
  526. },
  527. // Mute the calling media, optionally toggle
  528. mute: function( toggle ) {
  529. var event = toggle == null || toggle === true ? "muted" : "unmuted";
  530. // If `toggle` is explicitly `false`,
  531. // unmute the media and restore the volume level
  532. if ( event === "unmuted" ) {
  533. this.media.muted = false;
  534. this.media.volume = this.data.state.volume;
  535. }
  536. // If `toggle` is either null or undefined,
  537. // save the current volume and mute the media element
  538. if ( event === "muted" ) {
  539. this.data.state.volume = this.media.volume;
  540. this.media.muted = true;
  541. }
  542. // Trigger either muted|unmuted event
  543. this.emit( event );
  544. return this;
  545. },
  546. // Convenience method, unmute the calling media
  547. unmute: function( toggle ) {
  548. return this.mute( toggle == null ? false : !toggle );
  549. },
  550. // Get the client bounding box of an instance element
  551. position: function() {
  552. return Popcorn.position( this.media );
  553. },
  554. // Toggle a plugin's playback behaviour (on or off) per instance
  555. toggle: function( plugin ) {
  556. return Popcorn[ this.data.disabled[ plugin ] ? "enable" : "disable" ]( this, plugin );
  557. },
  558. // Set default values for plugin options objects per instance
  559. defaults: function( plugin, defaults ) {
  560. // If an array of default configurations is provided,
  561. // iterate and apply each to this instance
  562. if ( Popcorn.isArray( plugin ) ) {
  563. Popcorn.forEach( plugin, function( obj ) {
  564. for ( var name in obj ) {
  565. this.defaults( name, obj[ name ] );
  566. }
  567. }, this );
  568. return this;
  569. }
  570. if ( !this.options.defaults ) {
  571. this.options.defaults = {};
  572. }
  573. if ( !this.options.defaults[ plugin ] ) {
  574. this.options.defaults[ plugin ] = {};
  575. }
  576. Popcorn.extend( this.options.defaults[ plugin ], defaults );
  577. return this;
  578. }
  579. });
  580. Popcorn.Events = {
  581. UIEvents: "blur focus focusin focusout load resize scroll unload",
  582. MouseEvents: "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave click dblclick",
  583. Events: "loadstart progress suspend emptied stalled play pause error " +
  584. "loadedmetadata loadeddata waiting playing canplay canplaythrough " +
  585. "seeking seeked timeupdate ended ratechange durationchange volumechange"
  586. };
  587. Popcorn.Events.Natives = Popcorn.Events.UIEvents + " " +
  588. Popcorn.Events.MouseEvents + " " +
  589. Popcorn.Events.Events;
  590. internal.events.apiTypes = [ "UIEvents", "MouseEvents", "Events" ];
  591. // Privately compile events table at load time
  592. (function( events, data ) {
  593. var apis = internal.events.apiTypes,
  594. eventsList = events.Natives.split( /\s+/g ),
  595. idx = 0, len = eventsList.length, prop;
  596. for( ; idx < len; idx++ ) {
  597. data.hash[ eventsList[idx] ] = true;
  598. }
  599. apis.forEach(function( val, idx ) {
  600. data.apis[ val ] = {};
  601. var apiEvents = events[ val ].split( /\s+/g ),
  602. len = apiEvents.length,
  603. k = 0;
  604. for ( ; k < len; k++ ) {
  605. data.apis[ val ][ apiEvents[ k ] ] = true;
  606. }
  607. });
  608. })( Popcorn.Events, internal.events );
  609. Popcorn.events = {
  610. isNative: function( type ) {
  611. return !!internal.events.hash[ type ];
  612. },
  613. getInterface: function( type ) {
  614. if ( !Popcorn.events.isNative( type ) ) {
  615. return false;
  616. }
  617. var eventApi = internal.events,
  618. apis = eventApi.apiTypes,
  619. apihash = eventApi.apis,
  620. idx = 0, len = apis.length, api, tmp;
  621. for ( ; idx < len; idx++ ) {
  622. tmp = apis[ idx ];
  623. if ( apihash[ tmp ][ type ] ) {
  624. api = tmp;
  625. break;
  626. }
  627. }
  628. return api;
  629. },
  630. // Compile all native events to single array
  631. all: Popcorn.Events.Natives.split( /\s+/g ),
  632. // Defines all Event handling static functions
  633. fn: {
  634. trigger: function( type, data ) {
  635. var eventInterface, evt;
  636. // setup checks for custom event system
  637. if ( this.data.events[ type ] && Popcorn.sizeOf( this.data.events[ type ] ) ) {
  638. eventInterface = Popcorn.events.getInterface( type );
  639. if ( eventInterface ) {
  640. evt = document.createEvent( eventInterface );
  641. evt.initEvent( type, true, true, global, 1 );
  642. this.media.dispatchEvent( evt );
  643. return this;
  644. }
  645. // Custom events
  646. Popcorn.forEach( this.data.events[ type ], function( obj, key ) {
  647. obj.call( this, data );
  648. }, this );
  649. }
  650. return this;
  651. },
  652. listen: function( type, fn ) {
  653. var self = this,
  654. hasEvents = true,
  655. eventHook = Popcorn.events.hooks[ type ],
  656. origType = type,
  657. tmp;
  658. if ( !this.data.events[ type ] ) {
  659. this.data.events[ type ] = {};
  660. hasEvents = false;
  661. }
  662. // Check and setup event hooks
  663. if ( eventHook ) {
  664. // Execute hook add method if defined
  665. if ( eventHook.add ) {
  666. eventHook.add.call( this, {}, fn );
  667. }
  668. // Reassign event type to our piggyback event type if defined
  669. if ( eventHook.bind ) {
  670. type = eventHook.bind;
  671. }
  672. // Reassign handler if defined
  673. if ( eventHook.handler ) {
  674. tmp = fn;
  675. fn = function wrapper( event ) {
  676. eventHook.handler.call( self, event, tmp );
  677. };
  678. }
  679. // assume the piggy back event is registered
  680. hasEvents = true;
  681. // Setup event registry entry
  682. if ( !this.data.events[ type ] ) {
  683. this.data.events[ type ] = {};
  684. // Toggle if the previous assumption was untrue
  685. hasEvents = false;
  686. }
  687. }
  688. // Register event and handler
  689. this.data.events[ type ][ fn.name || ( fn.toString() + Popcorn.guid() ) ] = fn;
  690. // only attach one event of any type
  691. if ( !hasEvents && Popcorn.events.all.indexOf( type ) > -1 ) {
  692. this.media.addEventListener( type, function( event ) {
  693. Popcorn.forEach( self.data.events[ type ], function( obj, key ) {
  694. if ( typeof obj === "function" ) {
  695. obj.call( self, event );
  696. }
  697. });
  698. }, false);
  699. }
  700. return this;
  701. },
  702. unlisten: function( type, fn ) {
  703. if ( this.data.events[ type ] && this.data.events[ type ][ fn ] ) {
  704. delete this.data.events[ type ][ fn ];
  705. return this;
  706. }
  707. this.data.events[ type ] = null;
  708. return this;
  709. }
  710. },
  711. hooks: {
  712. canplayall: {
  713. bind: "canplaythrough",
  714. add: function( event, callback ) {
  715. var state = false;
  716. if ( this.media.readyState ) {
  717. callback.call( this, event );
  718. state = true;
  719. }
  720. this.data.hooks.canplayall = {
  721. fired: state
  722. };
  723. },
  724. // declare special handling instructions
  725. handler: function canplayall( event, callback ) {
  726. if ( !this.data.hooks.canplayall.fired ) {
  727. // trigger original user callback once
  728. callback.call( this, event );
  729. this.data.hooks.canplayall.fired = true;
  730. }
  731. }
  732. }
  733. }
  734. };
  735. // Extend Popcorn.events.fns (listen, unlisten, trigger) to all Popcorn instances
  736. // Extend aliases (on, off, emit)
  737. Popcorn.forEach( [ [ "trigger", "emit" ], [ "listen", "on" ], [ "unlisten", "off" ] ], function( key ) {
  738. Popcorn.p[ key[ 0 ] ] = Popcorn.p[ key[ 1 ] ] = Popcorn.events.fn[ key[ 0 ] ];
  739. });
  740. // Internal Only - Adds track events to the instance object
  741. Popcorn.addTrackEvent = function( obj, track ) {
  742. var trackEvent, isUpdate, eventType;
  743. // Do a lookup for existing trackevents with this id
  744. if ( track.id ) {
  745. trackEvent = obj.getTrackEvent( track.id );
  746. }
  747. // If a track event by this id currently exists, modify it
  748. if ( trackEvent ) {
  749. isUpdate = true;
  750. // Create a new object with the existing trackEvent
  751. // Extend with new track properties
  752. track = Popcorn.extend( {}, trackEvent, track );
  753. // Remove the existing track from the instance
  754. obj.removeTrackEvent( track.id );
  755. }
  756. // Determine if this track has default options set for it
  757. // If so, apply them to the track object
  758. if ( track && track._natives && track._natives.type &&
  759. ( obj.options.defaults && obj.options.defaults[ track._natives.type ] ) ) {
  760. track = Popcorn.extend( {}, obj.options.defaults[ track._natives.type ], track );
  761. }
  762. if ( track._natives ) {
  763. // Supports user defined track event id
  764. track._id = track.id || track._id || Popcorn.guid( track._natives.type );
  765. // Push track event ids into the history
  766. obj.data.history.push( track._id );
  767. }
  768. track.start = Popcorn.util.toSeconds( track.start, obj.options.framerate );
  769. track.end = Popcorn.util.toSeconds( track.end, obj.options.framerate );
  770. // Store this definition in an array sorted by times
  771. var byStart = obj.data.trackEvents.byStart,
  772. byEnd = obj.data.trackEvents.byEnd,
  773. startIndex, endIndex;
  774. for ( startIndex = byStart.length - 1; startIndex >= 0; startIndex-- ) {
  775. if ( track.start >= byStart[ startIndex ].start ) {
  776. byStart.splice( startIndex + 1, 0, track );
  777. break;
  778. }
  779. }
  780. for ( endIndex = byEnd.length - 1; endIndex >= 0; endIndex-- ) {
  781. if ( track.end > byEnd[ endIndex ].end ) {
  782. byEnd.splice( endIndex + 1, 0, track );
  783. break;
  784. }
  785. }
  786. // Display track event immediately if it's enabled and current
  787. if ( track.end > obj.media.currentTime &&
  788. track.start <= obj.media.currentTime ) {
  789. track._running = true;
  790. obj.data.running[ track._natives.type ].push( track );
  791. if ( !obj.data.disabled[ track._natives.type ] ) {
  792. track._natives.start.call( obj, null, track );
  793. }
  794. }
  795. // update startIndex and endIndex
  796. if ( startIndex <= obj.data.trackEvents.startIndex &&
  797. track.start <= obj.data.trackEvents.previousUpdateTime ) {
  798. obj.data.trackEvents.startIndex++;
  799. }
  800. if ( endIndex <= obj.data.trackEvents.endIndex &&
  801. track.end < obj.data.trackEvents.previousUpdateTime ) {
  802. obj.data.trackEvents.endIndex++;
  803. }
  804. this.timeUpdate( obj, null, true );
  805. // Store references to user added trackevents in ref table
  806. if ( track._id ) {
  807. Popcorn.addTrackEvent.ref( obj, track );
  808. }
  809. // If the call to addTrackEvent was an update/modify call, fire an event
  810. if ( isUpdate ) {
  811. // Determine appropriate event type to trigger
  812. // they are identical in function, but the naming
  813. // adds some level of intuition for the end developer
  814. // to rely on
  815. if ( track._natives.type === "cue" ) {
  816. eventType = "cuechange";
  817. } else {
  818. eventType = "trackchange";
  819. }
  820. // Fire an event with change information
  821. obj.emit( eventType, {
  822. id: track.id,
  823. previousValue: {
  824. time: trackEvent.start,
  825. fn: trackEvent._natives.start
  826. },
  827. currentValue: {
  828. time: track.start,
  829. fn: track._natives.start
  830. }
  831. });
  832. }
  833. };
  834. // Internal Only - Adds track event references to the instance object's trackRefs hash table
  835. Popcorn.addTrackEvent.ref = function( obj, track ) {
  836. obj.data.trackRefs[ track._id ] = track;
  837. return obj;
  838. };
  839. Popcorn.removeTrackEvent = function( obj, removeId ) {
  840. var start, end, animate,
  841. historyLen = obj.data.history.length,
  842. length = obj.data.trackEvents.byStart.length,
  843. index = 0,
  844. indexWasAt = 0,
  845. byStart = [],
  846. byEnd = [],
  847. animating = [],
  848. history = [];
  849. while ( --length > -1 ) {
  850. start = obj.data.trackEvents.byStart[ index ];
  851. end = obj.data.trackEvents.byEnd[ index ];
  852. // Padding events will not have _id properties.
  853. // These should be safely pushed onto the front and back of the
  854. // track event array
  855. if ( !start._id ) {
  856. byStart.push( start );
  857. byEnd.push( end );
  858. }
  859. // Filter for user track events (vs system track events)
  860. if ( start._id ) {
  861. // If not a matching start event for removal
  862. if ( start._id !== removeId ) {
  863. byStart.push( start );
  864. }
  865. // If not a matching end event for removal
  866. if ( end._id !== removeId ) {
  867. byEnd.push( end );
  868. }
  869. // If the _id is matched, capture the current index
  870. if ( start._id === removeId ) {
  871. indexWasAt = index;
  872. // If a _teardown function was defined,
  873. // enforce for track event removals
  874. if ( start._natives._teardown ) {
  875. start._natives._teardown.call( obj, start );
  876. }
  877. }
  878. }
  879. // Increment the track index
  880. index++;
  881. }
  882. // Reset length to be used by the condition below to determine
  883. // if animating track events should also be filtered for removal.
  884. // Reset index below to be used by the reverse while as an
  885. // incrementing counter
  886. length = obj.data.trackEvents.animating.length;
  887. index = 0;
  888. if ( length ) {
  889. while ( --length > -1 ) {
  890. animate = obj.data.trackEvents.animating[ index ];
  891. // Padding events will not have _id properties.
  892. // These should be safely pushed onto the front and back of the
  893. // track event array
  894. if ( !animate._id ) {
  895. animating.push( animate );
  896. }
  897. // If not a matching animate event for removal
  898. if ( animate._id && animate._id !== removeId ) {
  899. animating.push( animate );
  900. }
  901. // Increment the track index
  902. index++;
  903. }
  904. }
  905. // Update
  906. if ( indexWasAt <= obj.data.trackEvents.startIndex ) {
  907. obj.data.trackEvents.startIndex--;
  908. }
  909. if ( indexWasAt <= obj.data.trackEvents.endIndex ) {
  910. obj.data.trackEvents.endIndex--;
  911. }
  912. obj.data.trackEvents.byStart = byStart;
  913. obj.data.trackEvents.byEnd = byEnd;
  914. obj.data.trackEvents.animating = animating;
  915. for ( var i = 0; i < historyLen; i++ ) {
  916. if ( obj.data.history[ i ] !== removeId ) {
  917. history.push( obj.data.history[ i ] );
  918. }
  919. }
  920. // Update ordered history array
  921. obj.data.history = history;
  922. // Update track event references
  923. Popcorn.removeTrackEvent.ref( obj, removeId );
  924. };
  925. // Internal Only - Removes track event references from instance object's trackRefs hash table
  926. Popcorn.removeTrackEvent.ref = function( obj, removeId ) {
  927. delete obj.data.trackRefs[ removeId ];
  928. return obj;
  929. };
  930. // Return an array of track events bound to this instance object
  931. Popcorn.getTrackEvents = function( obj ) {
  932. var trackevents = [],
  933. refs = obj.data.trackEvents.byStart,
  934. length = refs.length,
  935. idx = 0,
  936. ref;
  937. for ( ; idx < length; idx++ ) {
  938. ref = refs[ idx ];
  939. // Return only user attributed track event references
  940. if ( ref._id ) {
  941. trackevents.push( ref );
  942. }
  943. }
  944. return trackevents;
  945. };
  946. // Internal Only - Returns an instance object's trackRefs hash table
  947. Popcorn.getTrackEvents.ref = function( obj ) {
  948. return obj.data.trackRefs;
  949. };
  950. // Return a single track event bound to this instance object
  951. Popcorn.getTrackEvent = function( obj, trackId ) {
  952. return obj.data.trackRefs[ trackId ];
  953. };
  954. // Internal Only - Returns an instance object's track reference by track id
  955. Popcorn.getTrackEvent.ref = function( obj, trackId ) {
  956. return obj.data.trackRefs[ trackId ];
  957. };
  958. Popcorn.getLastTrackEventId = function( obj ) {
  959. return obj.data.history[ obj.data.history.length - 1 ];
  960. };
  961. Popcorn.timeUpdate = function( obj, event ) {
  962. var currentTime = obj.media.currentTime,
  963. previousTime = obj.data.trackEvents.previousUpdateTime,
  964. tracks = obj.data.trackEvents,
  965. end = tracks.endIndex,
  966. start = tracks.startIndex,
  967. byStartLen = tracks.byStart.length,
  968. byEndLen = tracks.byEnd.length,
  969. registryByName = Popcorn.registryByName,
  970. trackstart = "trackstart",
  971. trackend = "trackend",
  972. byEnd, byStart, byAnimate, natives, type, runningPlugins;
  973. // Playbar advancing
  974. if ( previousTime <= currentTime ) {
  975. while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end <= currentTime ) {
  976. byEnd = tracks.byEnd[ end ];
  977. natives = byEnd._natives;
  978. type = natives && natives.type;
  979. // If plugin does not exist on this instance, remove it
  980. if ( !natives ||
  981. ( !!registryByName[ type ] ||
  982. !!obj[ type ] ) ) {
  983. if ( byEnd._running === true ) {
  984. byEnd._running = false;
  985. runningPlugins = obj.data.running[ type ];
  986. runningPlugins.splice( runningPlugins.indexOf( byEnd ), 1 );
  987. if ( !obj.data.disabled[ type ] ) {
  988. natives.end.call( obj, event, byEnd );
  989. obj.emit( trackend,
  990. Popcorn.extend({}, byEnd, {
  991. plugin: type,
  992. type: trackend
  993. })
  994. );
  995. }
  996. }
  997. end++;
  998. } else {
  999. // remove track event
  1000. Popcorn.removeTrackEvent( obj, byEnd._id );
  1001. return;
  1002. }
  1003. }
  1004. while ( tracks.byStart[ start ] && tracks.byStart[ start ].start <= currentTime ) {
  1005. byStart = tracks.byStart[ start ];
  1006. natives = byStart._natives;
  1007. type = natives && natives.type;
  1008. // If plugin does not exist on this instance, remove it
  1009. if ( !natives ||
  1010. ( !!registryByName[ type ] ||
  1011. !!obj[ type ] ) ) {
  1012. if ( byStart.end > currentTime &&
  1013. byStart._running === false ) {
  1014. byStart._running = true;
  1015. obj.data.running[ type ].push( byStart );
  1016. if ( !obj.data.disabled[ type ] ) {
  1017. natives.start.call( obj, event, byStart );
  1018. obj.emit( trackstart,
  1019. Popcorn.extend({}, byStart, {
  1020. plugin: type,
  1021. type: trackstart
  1022. })
  1023. );
  1024. }
  1025. }
  1026. start++;
  1027. } else {
  1028. // remove track event
  1029. Popcorn.removeTrackEvent( obj, byStart._id );
  1030. return;
  1031. }
  1032. }
  1033. // Playbar receding
  1034. } else if ( previousTime > currentTime ) {
  1035. while ( tracks.byStart[ start ] && tracks.byStart[ start ].start > currentTime ) {
  1036. byStart = tracks.byStart[ start ];
  1037. natives = byStart._natives;
  1038. type = natives && natives.type;
  1039. // if plugin does not exist on this instance, remove it
  1040. if ( !natives ||
  1041. ( !!registryByName[ type ] ||
  1042. !!obj[ type ] ) ) {
  1043. if ( byStart._running === true ) {
  1044. byStart._running = false;
  1045. runningPlugins = obj.data.running[ type ];
  1046. runningPlugins.splice( runningPlugins.indexOf( byStart ), 1 );
  1047. if ( !obj.data.disabled[ type ] ) {
  1048. natives.end.call( obj, event, byStart );
  1049. obj.emit( trackend,
  1050. Popcorn.extend({}, byStart, {
  1051. plugin: type,
  1052. type: trackend
  1053. })
  1054. );
  1055. }
  1056. }
  1057. start--;
  1058. } else {
  1059. // remove track event
  1060. Popcorn.removeTrackEvent( obj, byStart._id );
  1061. return;
  1062. }
  1063. }
  1064. while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end > currentTime ) {
  1065. byEnd = tracks.byEnd[ end ];
  1066. natives = byEnd._natives;
  1067. type = natives && natives.type;
  1068. // if plugin does not exist on this instance, remove it
  1069. if ( !natives ||
  1070. ( !!registryByName[ type ] ||
  1071. !!obj[ type ] ) ) {
  1072. if ( byEnd.start <= currentTime &&
  1073. byEnd._running === false ) {
  1074. byEnd._running = true;
  1075. obj.data.running[ type ].push( byEnd );
  1076. if ( !obj.data.disabled[ type ] ) {
  1077. natives.start.call( obj, event, byEnd );
  1078. obj.emit( trackstart,
  1079. Popcorn.extend({}, byEnd, {
  1080. plugin: type,
  1081. type: trackstart
  1082. })
  1083. );
  1084. }
  1085. }
  1086. end--;
  1087. } else {
  1088. // remove track event
  1089. Popcorn.removeTrackEvent( obj, byEnd._id );
  1090. return;
  1091. }
  1092. }
  1093. }
  1094. tracks.endIndex = end;
  1095. tracks.startIndex = start;
  1096. tracks.previousUpdateTime = currentTime;
  1097. //enforce index integrity if trackRemoved
  1098. tracks.byStart.length < byStartLen && tracks.startIndex--;
  1099. tracks.byEnd.length < byEndLen && tracks.endIndex--;
  1100. };
  1101. // Map and Extend TrackEvent functions to all Popcorn instances
  1102. Popcorn.extend( Popcorn.p, {
  1103. getTrackEvents: function() {
  1104. return Popcorn.getTrackEvents.call( null, this );
  1105. },
  1106. getTrackEvent: function( id ) {
  1107. return Popcorn.getTrackEvent.call( null, this, id );
  1108. },
  1109. getLastTrackEventId: function() {
  1110. return Popcorn.getLastTrackEventId.call( null, this );
  1111. },
  1112. removeTrackEvent: function( id ) {
  1113. Popcorn.removeTrackEvent.call( null, this, id );
  1114. return this;
  1115. },
  1116. removePlugin: function( name ) {
  1117. Popcorn.removePlugin.call( null, this, name );
  1118. return this;
  1119. },
  1120. timeUpdate: function( event ) {
  1121. Popcorn.timeUpdate.call( null, this, event );
  1122. return this;
  1123. },
  1124. destroy: function() {
  1125. Popcorn.destroy.call( null, this );
  1126. return this;
  1127. }
  1128. });
  1129. // Plugin manifests
  1130. Popcorn.manifest = {};
  1131. // Plugins are registered
  1132. Popcorn.registry = [];
  1133. Popcorn.registryByName = {};
  1134. // An interface for extending Popcorn
  1135. // with plugin functionality
  1136. Popcorn.plugin = function( name, definition, manifest ) {
  1137. if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
  1138. Popcorn.error( "'" + name + "' is a protected function name" );
  1139. return;
  1140. }
  1141. // Provides some sugar, but ultimately extends
  1142. // the definition into Popcorn.p
  1143. var reserved = [ "start", "end" ],
  1144. plugin = {},
  1145. setup,
  1146. isfn = typeof definition === "function",
  1147. methods = [ "_setup", "_teardown", "start", "end", "frame" ];
  1148. // combines calls of two function calls into one
  1149. var combineFn = function( first, second ) {
  1150. first = first || Popcorn.nop;
  1151. second = second || Popcorn.nop;
  1152. return function() {
  1153. first.apply( this, arguments );
  1154. second.apply( this, arguments );
  1155. };
  1156. };
  1157. // If `manifest` arg is undefined, check for manifest within the `definition` object
  1158. // If no `definition.manifest`, an empty object is a sufficient fallback
  1159. Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {};
  1160. // apply safe, and empty default functions
  1161. methods.forEach(function( method ) {
  1162. definition[ method ] = safeTry( definition[ method ] || Popcorn.nop, name );
  1163. });
  1164. var pluginFn = function( setup, options ) {
  1165. if ( !options ) {
  1166. return this;
  1167. }
  1168. // When the "ranges" property is set and its value is an array, short-circuit
  1169. // the pluginFn definition to recall itself with an options object generated from
  1170. // each range object in the ranges array. (eg. { start: 15, end: 16 } )
  1171. if ( options.ranges && Popcorn.isArray(options.ranges) ) {
  1172. Popcorn.forEach( options.ranges, function( range ) {
  1173. // Create a fresh object, extend with current options
  1174. // and start/end range object's properties
  1175. // Works with in/out as well.
  1176. var opts = Popcorn.extend( {}, options, range );
  1177. // Remove the ranges property to prevent infinitely
  1178. // entering this condition
  1179. delete opts.ranges;
  1180. // Call the plugin with the newly created opts object
  1181. this[ name ]( opts );
  1182. }, this);
  1183. // Return the Popcorn instance to avoid creating an empty track event
  1184. return this;
  1185. }
  1186. // Storing the plugin natives
  1187. var natives = options._natives = {},
  1188. compose = "",
  1189. originalOpts, manifestOpts;
  1190. Popcorn.extend( natives, setup );
  1191. options._natives.type = name;
  1192. options._running = false;
  1193. natives.start = natives.start || natives[ "in" ];
  1194. natives.end = natives.end || natives[ "out" ];
  1195. if ( options.once ) {
  1196. natives.end = combineFn( natives.end, function() {
  1197. this.removeTrackEvent( options._id );
  1198. });
  1199. }
  1200. // extend teardown to always call end if running
  1201. natives._teardown = combineFn(function() {
  1202. var args = slice.call( arguments ),
  1203. runningPlugins = this.data.running[ natives.type ];
  1204. // end function signature is not the same as teardown,
  1205. // put null on the front of arguments for the event parameter
  1206. args.unshift( null );
  1207. // only call end if event is running
  1208. args[ 1 ]._running &&
  1209. runningPlugins.splice( runningPlugins.indexOf( options ), 1 ) &&
  1210. natives.end.apply( this, args );
  1211. }, natives._teardown );
  1212. // default to an empty string if no effect exists
  1213. // split string into an array of effects
  1214. options.compose = options.compose && options.compose.split( " " ) || [];
  1215. options.effect = options.effect && options.effect.split( " " ) || [];
  1216. // join the two arrays together
  1217. options.compose = options.compose.concat( options.effect );
  1218. options.compose.forEach(function( composeOption ) {
  1219. // if the requested compose is garbage, throw it away
  1220. compose = Popcorn.compositions[ composeOption ] || {};
  1221. // extends previous functions with compose function
  1222. methods.forEach(function( method ) {
  1223. natives[ method ] = combineFn( natives[ method ], compose[ method ] );
  1224. });
  1225. });
  1226. // Ensure a manifest object, an empty object is a sufficient fallback
  1227. options._natives.manifest = manifest;
  1228. // Checks for expected properties
  1229. if ( !( "start" in options ) ) {
  1230. options.start = options[ "in" ] || 0;
  1231. }
  1232. if ( !options.end && options.end !== 0 ) {
  1233. options.end = options[ "out" ] || Number.MAX_VALUE;
  1234. }
  1235. // Use hasOwn to detect non-inherited toString, since all
  1236. // objects will receive a toString - its otherwise undetectable
  1237. if ( !hasOwn.call( options, "toString" ) ) {
  1238. options.toString = function() {
  1239. var props = [
  1240. "start: " + options.start,
  1241. "end: " + options.end,
  1242. "id: " + (options.id || options._id)
  1243. ];
  1244. // Matches null and undefined, allows: false, 0, "" and truthy
  1245. if ( options.target != null ) {
  1246. props.push( "target: " + options.target );
  1247. }
  1248. return name + " ( " + props.join(", ") + " )";
  1249. };
  1250. }
  1251. // Resolves 239, 241, 242
  1252. if ( !options.target ) {
  1253. // Sometimes the manifest may be missing entirely
  1254. // or it has an options object that doesn't have a `target` property
  1255. manifestOpts = "options" in manifest && manifest.options;
  1256. options.target = manifestOpts && "target" in manifestOpts && manifestOpts.target;
  1257. }
  1258. if ( options._natives ) {
  1259. // ensure an initial id is there before setup is called
  1260. options._id = Popcorn.guid( options._natives.type );
  1261. }
  1262. // Trigger _setup method if exists
  1263. options._natives._setup && options._natives._setup.call( this, options );
  1264. // Create new track event for this instance
  1265. Popcorn.addTrackEvent( this, options );
  1266. // Future support for plugin event definitions
  1267. // for all of the native events
  1268. Popcorn.forEach( setup, function( callback, type ) {
  1269. if ( type !== "type" ) {
  1270. if ( reserved.indexOf( type ) === -1 ) {
  1271. this.on( type, callback );
  1272. }
  1273. }
  1274. }, this );
  1275. return this;
  1276. };
  1277. // Extend Popcorn.p with new named definition
  1278. // Assign new named definition
  1279. Popcorn.p[ name ] = plugin[ name ] = function( id, options ) {
  1280. var length = arguments.length,
  1281. trackEvent, defaults, mergedSetupOpts;
  1282. // Shift arguments based on use case
  1283. //
  1284. // Back compat for:
  1285. // p.plugin( options );
  1286. if ( id && !options ) {
  1287. options = id;
  1288. id = null;
  1289. } else {
  1290. // Get the trackEvent that matches the given id.
  1291. trackEvent = this.getTrackEvent( id );
  1292. // If the track event does not exist, ensure that the options
  1293. // object has a proper id
  1294. if ( !trackEvent ) {
  1295. options.id = id;
  1296. // If the track event does exist, merge the updated properties
  1297. } else {
  1298. options = Popcorn.extend( {}, trackEvent, options );
  1299. Popcorn.addTrackEvent( this, options );
  1300. return this;
  1301. }
  1302. }
  1303. this.data.running[ name ] = this.data.running[ name ] || [];
  1304. // Merge with defaults if they exist, make sure per call is prioritized
  1305. defaults = ( this.options.defaults && this.options.defaults[ name ] ) || {};
  1306. mergedSetupOpts = Popcorn.extend( {}, defaults, options );
  1307. return pluginFn.call( this, isfn ? definition.call( this, mergedSetupOpts ) : definition,
  1308. mergedSetupOpts );
  1309. };
  1310. // if the manifest parameter exists we should extend it onto the definition object
  1311. // so that it shows up when calling Popcorn.registry and Popcorn.registryByName
  1312. if ( manifest ) {
  1313. Popcorn.extend( definition, {
  1314. manifest: manifest
  1315. });
  1316. }
  1317. // Push into the registry
  1318. var entry = {
  1319. fn: plugin[ name ],
  1320. definition: definition,
  1321. base: definition,
  1322. parents: [],
  1323. name: name
  1324. };
  1325. Popcorn.registry.push(
  1326. Popcorn.extend( plugin, entry, {
  1327. type: name
  1328. })
  1329. );
  1330. Popcorn.registryByName[ name ] = entry;
  1331. return plugin;
  1332. };
  1333. // Storage for plugin function errors
  1334. Popcorn.plugin.errors = [];
  1335. // Returns wrapped plugin function
  1336. function safeTry( fn, pluginName ) {
  1337. return function() {
  1338. // When Popcorn.plugin.debug is true, do not suppress errors
  1339. if ( Popcorn.plugin.debug ) {
  1340. return fn.apply( this, arguments );
  1341. }
  1342. try {
  1343. return fn.apply( this, arguments );
  1344. } catch ( ex ) {
  1345. // Push plugin function errors into logging queue
  1346. Popcorn.plugin.errors.push({
  1347. plugin: pluginName,
  1348. thrown: ex,
  1349. source: fn.toString()
  1350. });
  1351. // Trigger an error that the instance can listen for
  1352. // and react to
  1353. this.emit( "pluginerror", Popcorn.plugin.errors );
  1354. }
  1355. };
  1356. }
  1357. // Debug-mode flag for plugin development
  1358. // True for Popcorn development versions, false for stable/tagged versions
  1359. Popcorn.plugin.debug = ( Popcorn.version === "@" + "VERSION" );
  1360. // removePlugin( type ) removes all tracks of that from all instances of popcorn
  1361. // removePlugin( obj, type ) removes all tracks of type from obj, where obj is a single instance of popcorn
  1362. Popcorn.removePlugin = function( obj, name ) {
  1363. // Check if we are removing plugin from an instance or from all of Popcorn
  1364. if ( !name ) {
  1365. // Fix the order
  1366. name = obj;
  1367. obj = Popcorn.p;
  1368. if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
  1369. Popcorn.error( "'" + name + "' is a protected function name" );
  1370. return;
  1371. }
  1372. var registryLen = Popcorn.registry.length,
  1373. registryIdx;
  1374. // remove plugin reference from registry
  1375. for ( registryIdx = 0; registryIdx < registryLen; registryIdx++ ) {
  1376. if ( Popcorn.registry[ registryIdx ].name === name ) {
  1377. Popcorn.registry.splice( registryIdx, 1 );
  1378. delete Popcorn.registryByName[ name ];
  1379. delete Popcorn.manifest[ name ];
  1380. // delete the plugin
  1381. delete obj[ name ];
  1382. // plugin found and removed, stop checking, we are done
  1383. return;
  1384. }
  1385. }
  1386. }
  1387. var byStart = obj.data.trackEvents.byStart,
  1388. byEnd = obj.data.trackEvents.byEnd,
  1389. animating = obj.data.trackEvents.animating,
  1390. idx, sl;
  1391. // remove all trackEvents
  1392. for ( idx = 0, sl = byStart.length; idx < sl; idx++ ) {
  1393. if ( byStart[ idx ] && byStart[ idx ]._natives && byStart[ idx ]._natives.type === name ) {
  1394. byStart[ idx ]._natives._teardown && byStart[ idx ]._natives._teardown.call( obj, byStart[ idx ] );
  1395. byStart.splice( idx, 1 );
  1396. // update for loop if something removed, but keep checking
  1397. idx--; sl--;
  1398. if ( obj.data.trackEvents.startIndex <= idx ) {
  1399. obj.data.trackEvents.startIndex--;
  1400. obj.data.trackEvents.endIndex--;
  1401. }
  1402. }
  1403. // clean any remaining references in the end index
  1404. // we do this seperate from the above check because they might not be in the same order
  1405. if ( byEnd[ idx ] && byEnd[ idx ]._natives && byEnd[ idx ]._natives.type === name ) {
  1406. byEnd.splice( idx, 1 );
  1407. }
  1408. }
  1409. //remove all animating events
  1410. for ( idx = 0, sl = animating.length; idx < sl; idx++ ) {
  1411. if ( animating[ idx ] && animating[ idx ]._natives && animating[ idx ]._natives.type === name ) {
  1412. animating.splice( idx, 1 );
  1413. // update for loop if something removed, but keep checking
  1414. idx--; sl--;
  1415. }
  1416. }
  1417. };
  1418. Popcorn.compositions = {};
  1419. // Plugin inheritance
  1420. Popcorn.compose = function( name, definition, manifest ) {
  1421. // If `manifest` arg is undefined, check for manifest within the `definition` object
  1422. // If no `definition.manifest`, an empty object is a sufficient fallback
  1423. Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {};
  1424. // register the effect by name
  1425. Popcorn.compositions[ name ] = definition;
  1426. };
  1427. Popcorn.plugin.effect = Popcorn.effect = Popcorn.compose;
  1428. var rnaiveExpr = /^(?:\.|#|\[)/;
  1429. // Basic DOM utilities and helpers API. See #1037
  1430. Popcorn.dom = {
  1431. debug: false,
  1432. // Popcorn.dom.find( selector, context )
  1433. //
  1434. // Returns the first element that matches the specified selector
  1435. // Optionally provide a context element, defaults to `document`
  1436. //
  1437. // eg.
  1438. // Popcorn.dom.find("video") returns the first video element
  1439. // Popcorn.dom.find("#foo") returns the first element with `id="foo"`
  1440. // Popcorn.dom.find("foo") returns the first element with `id="foo"`
  1441. // Note: Popcorn.dom.find("foo") is the only allowed deviation
  1442. // from valid querySelector selector syntax
  1443. //
  1444. // Popcorn.dom.find(".baz") returns the first element with `class="baz"`
  1445. // Popcorn.dom.find("[preload]") returns the first element with `preload="..."`
  1446. // ...
  1447. // See https://developer.mozilla.org/En/DOM/Document.querySelector
  1448. //
  1449. //
  1450. find: function( selector, context ) {
  1451. var node = null;
  1452. // Trim leading/trailing whitespace to avoid false negatives
  1453. selector = selector.trim();
  1454. // Default context is the `document`
  1455. context = context || document;
  1456. if ( selector ) {
  1457. // If the selector does not begin with "#", "." or "[",
  1458. // it could be either a nodeName or ID w/o "#"
  1459. if ( !rnaiveExpr.test( selector ) ) {
  1460. // Try finding an element that matches by ID first
  1461. node = document.getElementById( selector );
  1462. // If a match was found by ID, return the element
  1463. if ( node !== null ) {
  1464. return node;
  1465. }
  1466. }
  1467. // Assume no elements have been found yet
  1468. // Catch any invalid selector syntax errors and bury them.
  1469. try {
  1470. node = context.querySelector( selector );
  1471. } catch ( e ) {
  1472. if ( Popcorn.dom.debug ) {
  1473. throw new Error(e);
  1474. }
  1475. }
  1476. }
  1477. return node;
  1478. }
  1479. };
  1480. // Cache references to reused RegExps
  1481. var rparams = /\?/,
  1482. // XHR Setup object
  1483. setup = {
  1484. url: "",
  1485. data: "",
  1486. dataType: "",
  1487. success: Popcorn.nop,
  1488. type: "GET",
  1489. async: true,
  1490. xhr: function() {
  1491. return new global.XMLHttpRequest();
  1492. }
  1493. };
  1494. Popcorn.xhr = function( options ) {
  1495. options.dataType = options.dataType && options.dataType.toLowerCase() || null;
  1496. if ( options.dataType &&
  1497. ( options.dataType === "jsonp" || options.dataType === "script" ) ) {
  1498. Popcorn.xhr.getJSONP(
  1499. options.url,
  1500. options.success,
  1501. options.dataType === "script"
  1502. );
  1503. return;
  1504. }
  1505. var settings = Popcorn.extend( {}, setup, options );
  1506. // Create new XMLHttpRequest object
  1507. settings.ajax = settings.xhr();
  1508. if ( settings.ajax ) {
  1509. if ( settings.type === "GET" && settings.data ) {
  1510. // append query string
  1511. settings.url += ( rparams.test( settings.url ) ? "&" : "?" ) + settings.data;
  1512. // Garbage collect and reset settings.data
  1513. settings.data = null;
  1514. }
  1515. settings.ajax.open( settings.type, settings.url, settings.async );
  1516. settings.ajax.send( settings.data || null );
  1517. return Popcorn.xhr.httpData( settings );
  1518. }
  1519. };
  1520. Popcorn.xhr.httpData = function( settings ) {
  1521. var data, json = null,
  1522. parser, xml = null;
  1523. settings.ajax.onreadystatechange = function() {
  1524. if ( settings.ajax.readyState === 4 ) {
  1525. try {
  1526. json = JSON.parse( settings.ajax.responseText );
  1527. } catch( e ) {
  1528. //suppress
  1529. }
  1530. data = {
  1531. xml: settings.ajax.responseXML,
  1532. text: settings.ajax.responseText,
  1533. json: json
  1534. };
  1535. // Normalize: data.xml is non-null in IE9 regardless of if response is valid xml
  1536. if ( !data.xml || !data.xml.documentElement ) {
  1537. data.xml = null;
  1538. try {
  1539. parser = new DOMParser();
  1540. xml = parser.parseFromString( settings.ajax.responseText, "text/xml" );
  1541. if ( !xml.getElementsByTagName( "parsererror" ).length ) {
  1542. data.xml = xml;
  1543. }
  1544. } catch ( e ) {
  1545. // data.xml remains null
  1546. }
  1547. }
  1548. // If a dataType was specified, return that type of data
  1549. if ( settings.dataType ) {
  1550. data = data[ settings.dataType ];
  1551. }
  1552. settings.success.call( settings.ajax, data );
  1553. }
  1554. };
  1555. return data;
  1556. };
  1557. Popcorn.xhr.getJSONP = function( url, success, isScript ) {
  1558. var head = document.head || document.getElementsByTagName( "head" )[ 0 ] || document.documentElement,
  1559. script = document.createElement( "script" ),
  1560. isFired = false,
  1561. params = [],
  1562. rjsonp = /(=)\?(?=&|$)|\?\?/,
  1563. replaceInUrl, prefix, paramStr, callback, callparam;
  1564. if ( !isScript ) {
  1565. // is there a calback already in the url
  1566. callparam = url.match( /(callback=[^&]*)/ );
  1567. if ( callparam !== null && callparam.length ) {
  1568. prefix = callparam[ 1 ].split( "=" )[ 1 ];
  1569. // Since we need to support developer specified callbacks
  1570. // and placeholders in harmony, make sure matches to "callback="
  1571. // aren't just placeholders.
  1572. // We coded ourselves into a corner here.
  1573. // JSONP callbacks should never have been
  1574. // allowed to have developer specified callbacks
  1575. if ( prefix === "?" ) {
  1576. prefix = "jsonp";
  1577. }
  1578. // get the callback name
  1579. callback = Popcorn.guid( prefix );
  1580. // replace existing callback name with unique callback name
  1581. url = url.replace( /(callback=[^&]*)/, "callback=" + callback );
  1582. } else {
  1583. callback = Popcorn.guid( "jsonp" );
  1584. if ( rjsonp.test( url ) ) {
  1585. url = url.replace( rjsonp, "$1" + callback );
  1586. }
  1587. // split on first question mark,
  1588. // this is to capture the query string
  1589. params = url.split( /\?(.+)?/ );
  1590. // rebuild url with callback
  1591. url = params[ 0 ] + "?";
  1592. if ( params[ 1 ] ) {
  1593. url += params[ 1 ] + "&";
  1594. }
  1595. url += "callback=" + callback;
  1596. }
  1597. // Define the JSONP success callback globally
  1598. window[ callback ] = function( data ) {
  1599. // Fire success callbacks
  1600. success && success( data );
  1601. isFired = true;
  1602. };
  1603. }
  1604. script.addEventListener( "load", function() {
  1605. // Handling remote script loading callbacks
  1606. if ( isScript ) {
  1607. // getScript
  1608. success && success();
  1609. }
  1610. // Executing for JSONP requests
  1611. if ( isFired ) {
  1612. // Garbage collect the callback
  1613. delete window[ callback ];
  1614. }
  1615. // Garbage collect the script resource
  1616. head.removeChild( script );
  1617. }, false );
  1618. script.src = url;
  1619. head.insertBefore( script, head.firstChild );
  1620. return;
  1621. };
  1622. Popcorn.getJSONP = Popcorn.xhr.getJSONP;
  1623. Popcorn.getScript = Popcorn.xhr.getScript = function( url, success ) {
  1624. return Popcorn.xhr.getJSONP( url, success, true );
  1625. };
  1626. Popcorn.util = {
  1627. // Simple function to parse a timestamp into seconds
  1628. // Acceptable formats are:
  1629. // HH:MM:SS.MMM
  1630. // HH:MM:SS;FF
  1631. // Hours and minutes are optional. They default to 0
  1632. toSeconds: function( timeStr, framerate ) {
  1633. // Hours and minutes are optional
  1634. // Seconds must be specified
  1635. // Seconds can be followed by milliseconds OR by the frame information
  1636. var validTimeFormat = /^([0-9]+:){0,2}[0-9]+([.;][0-9]+)?$/,
  1637. errorMessage = "Invalid time format",
  1638. digitPairs, lastIndex, lastPair, firstPair,
  1639. frameInfo, frameTime;
  1640. if ( typeof timeStr === "number" ) {
  1641. return timeStr;
  1642. }
  1643. if ( typeof timeStr === "string" &&
  1644. !validTimeFormat.test( timeStr ) ) {
  1645. Popcorn.error( errorMessage );
  1646. }
  1647. digitPairs = timeStr.split( ":" );
  1648. lastIndex = digitPairs.length - 1;
  1649. lastPair = digitPairs[ lastIndex ];
  1650. // Fix last element:
  1651. if ( lastPair.indexOf( ";" ) > -1 ) {
  1652. frameInfo = lastPair.split( ";" );
  1653. frameTime = 0;
  1654. if ( framerate && ( typeof framerate === "number" ) ) {
  1655. frameTime = parseFloat( frameInfo[ 1 ], 10 ) / framerate;
  1656. }
  1657. digitPairs[ lastIndex ] = parseInt( frameInfo[ 0 ], 10 ) + frameTime;
  1658. }
  1659. firstPair = digitPairs[ 0 ];
  1660. return {
  1661. 1: parseFloat( firstPair, 10 ),
  1662. 2: ( parseInt( firstPair, 10 ) * 60 ) +
  1663. parseFloat( digitPairs[ 1 ], 10 ),
  1664. 3: ( parseInt( firstPair, 10 ) * 3600 ) +
  1665. ( parseInt( digitPairs[ 1 ], 10 ) * 60 ) +
  1666. parseFloat( digitPairs[ 2 ], 10 )
  1667. }[ digitPairs.length || 1 ];
  1668. }
  1669. };
  1670. // alias for exec function
  1671. Popcorn.p.cue = Popcorn.p.exec;
  1672. // Protected API methods
  1673. Popcorn.protect = {
  1674. natives: getKeys( Popcorn.p ).map(function( val ) {
  1675. return val.toLowerCase();
  1676. })
  1677. };
  1678. // Setup logging for deprecated methods
  1679. Popcorn.forEach({
  1680. // Deprecated: Recommended
  1681. "listen": "on",
  1682. "unlisten": "off",
  1683. "trigger": "emit",
  1684. "exec": "cue"
  1685. }, function( recommend, api ) {
  1686. var original = Popcorn.p[ api ];
  1687. // Override the deprecated api method with a method of the same name
  1688. // that logs a warning and defers to the new recommended method
  1689. Popcorn.p[ api ] = function() {
  1690. if ( typeof console !== "undefined" && console.warn ) {
  1691. console.warn(
  1692. "Deprecated method '" + api + "', " +
  1693. (recommend == null ? "do not use." : "use '" + recommend + "' instead." )
  1694. );
  1695. // Restore api after first warning
  1696. Popcorn.p[ api ] = original;
  1697. }
  1698. return Popcorn.p[ recommend ].apply( this, [].slice.call( arguments ) );
  1699. };
  1700. });
  1701. // Exposes Popcorn to global context
  1702. global.Popcorn = Popcorn;
  1703. })(window, window.document);
  1704. /*!
  1705. * Popcorn.sequence
  1706. *
  1707. * Copyright 2011, Rick Waldron
  1708. * Licensed under MIT license.
  1709. *
  1710. */
  1711. /* jslint forin: true, maxerr: 50, indent: 4, es5: true */
  1712. /* global Popcorn: true */
  1713. // Requires Popcorn.js
  1714. (function( global, Popcorn ) {
  1715. // TODO: as support increases, migrate to element.dataset
  1716. var doc = global.document,
  1717. location = global.location,
  1718. rprotocol = /:\/\//,
  1719. // TODO: better solution to this sucky stop-gap
  1720. lochref = location.href.replace( location.href.split("/").slice(-1)[0], "" ),
  1721. // privately held
  1722. range = function(start, stop, step) {
  1723. start = start || 0;
  1724. stop = ( stop || start || 0 ) + 1;
  1725. step = step || 1;
  1726. var len = Math.ceil((stop - start) / step) || 0,
  1727. idx = 0,
  1728. range = [];
  1729. range.length = len;
  1730. while (idx < len) {
  1731. range[idx++] = start;
  1732. start += step;
  1733. }
  1734. return range;
  1735. };
  1736. Popcorn.sequence = function( parent, list ) {
  1737. return new Popcorn.sequence.init( parent, list );
  1738. };
  1739. Popcorn.sequence.init = function( parent, list ) {
  1740. // Video element
  1741. this.parent = doc.getElementById( parent );
  1742. // Store ref to a special ID
  1743. this.seqId = Popcorn.guid( "__sequenced" );
  1744. // List of HTMLVideoElements
  1745. this.queue = [];
  1746. // List of Popcorn objects
  1747. this.playlist = [];
  1748. // Lists of in/out points
  1749. this.inOuts = {
  1750. // Stores the video in/out times for each video in sequence
  1751. ofVideos: [],
  1752. // Stores the clip in/out times for each clip in sequences
  1753. ofClips: []
  1754. };
  1755. // Store first video dimensions
  1756. this.dims = {
  1757. width: 0, //this.video.videoWidth,
  1758. height: 0 //this.video.videoHeight
  1759. };
  1760. this.active = 0;
  1761. this.cycling = false;
  1762. this.playing = false;
  1763. this.times = {
  1764. last: 0
  1765. };
  1766. // Store event pointers and queues
  1767. this.events = {
  1768. };
  1769. var self = this,
  1770. clipOffset = 0;
  1771. // Create `video` elements
  1772. Popcorn.forEach( list, function( media, idx ) {
  1773. var video = doc.createElement( "video" );
  1774. video.preload = "auto";
  1775. // Setup newly created video element
  1776. video.controls = true;
  1777. // If the first, show it, if the after, hide it
  1778. video.style.display = ( idx && "none" ) || "" ;
  1779. // Seta registered sequence id
  1780. video.id = self.seqId + "-" + idx ;
  1781. // Push this video into the sequence queue
  1782. self.queue.push( video );
  1783. var //satisfy lint
  1784. mIn = media["in"],
  1785. mOut = media["out"];
  1786. // Push the in/out points into sequence ioVideos
  1787. self.inOuts.ofVideos.push({
  1788. "in": ( mIn !== undefined && mIn ) || 1,
  1789. "out": ( mOut !== undefined && mOut ) || 0
  1790. });
  1791. self.inOuts.ofVideos[ idx ]["out"] = self.inOuts.ofVideos[ idx ]["out"] || self.inOuts.ofVideos[ idx ]["in"] + 2;
  1792. // Set the sources
  1793. video.src = !rprotocol.test( media.src ) ? lochref + media.src : media.src;
  1794. // Set some squence specific data vars
  1795. video.setAttribute("data-sequence-owner", parent );
  1796. video.setAttribute("data-sequence-guid", self.seqId );
  1797. video.setAttribute("data-sequence-id", idx );
  1798. video.setAttribute("data-sequence-clip", [ self.inOuts.ofVideos[ idx ]["in"], self.inOuts.ofVideos[ idx ]["out"] ].join(":") );
  1799. // Append the video to the parent element
  1800. self.parent.appendChild( video );
  1801. self.playlist.push( Popcorn("#" + video.id ) );
  1802. });
  1803. self.inOuts.ofVideos.forEach(function( obj ) {
  1804. var clipDuration = obj["out"] - obj["in"],
  1805. offs = {
  1806. "in": clipOffset,
  1807. "out": clipOffset + clipDuration
  1808. };
  1809. self.inOuts.ofClips.push( offs );
  1810. clipOffset = offs["out"] + 1;
  1811. });
  1812. Popcorn.forEach( this.queue, function( media, idx ) {
  1813. function canPlayThrough( event ) {
  1814. // If this is idx zero, use it as dimension for all
  1815. if ( !idx ) {
  1816. self.dims.width = media.videoWidth;
  1817. self.dims.height = media.videoHeight;
  1818. }
  1819. media.currentTime = self.inOuts.ofVideos[ idx ]["in"] - 0.5;
  1820. media.removeEventListener( "canplaythrough", canPlayThrough, false );
  1821. return true;
  1822. }
  1823. // Hook up event listeners for managing special playback
  1824. media.addEventListener( "canplaythrough", canPlayThrough, false );
  1825. // TODO: consolidate & DRY
  1826. media.addEventListener( "play", function( event ) {
  1827. self.playing = true;
  1828. }, false );
  1829. media.addEventListener( "pause", function( event ) {
  1830. self.playing = false;
  1831. }, false );
  1832. media.addEventListener( "timeupdate", function( event ) {
  1833. var target = event.srcElement || event.target,
  1834. seqIdx = +( (target.dataset && target.dataset.sequenceId) || target.getAttribute("data-sequence-id") ),
  1835. floor = Math.floor( media.currentTime );
  1836. if ( self.times.last !== floor &&
  1837. seqIdx === self.active ) {
  1838. self.times.last = floor;
  1839. if ( floor === self.inOuts.ofVideos[ seqIdx ]["out"] ) {
  1840. Popcorn.sequence.cycle.call( self, seqIdx );
  1841. }
  1842. }
  1843. }, false );
  1844. });
  1845. return this;
  1846. };
  1847. Popcorn.sequence.init.prototype = Popcorn.sequence.prototype;
  1848. //
  1849. Popcorn.sequence.cycle = function( idx ) {
  1850. if ( !this.queue ) {
  1851. Popcorn.error("Popcorn.sequence.cycle is not a public method");
  1852. }
  1853. var // Localize references
  1854. queue = this.queue,
  1855. ioVideos = this.inOuts.ofVideos,
  1856. current = queue[ idx ],
  1857. nextIdx = 0,
  1858. next, clip;
  1859. var // Popcorn instances
  1860. $popnext,
  1861. $popprev;
  1862. if ( queue[ idx + 1 ] ) {
  1863. nextIdx = idx + 1;
  1864. }
  1865. // Reset queue
  1866. if ( !queue[ idx + 1 ] ) {
  1867. nextIdx = 0;
  1868. this.playlist[ idx ].pause();
  1869. } else {
  1870. next = queue[ nextIdx ];
  1871. clip = ioVideos[ nextIdx ];
  1872. // Constrain dimentions
  1873. Popcorn.extend( next, {
  1874. width: this.dims.width,
  1875. height: this.dims.height
  1876. });
  1877. $popnext = this.playlist[ nextIdx ];
  1878. $popprev = this.playlist[ idx ];
  1879. // When not resetting to 0
  1880. current.pause();
  1881. this.active = nextIdx;
  1882. this.times.last = clip["in"] - 1;
  1883. // Play the next video in the sequence
  1884. $popnext.currentTime( clip["in"] );
  1885. $popnext[ nextIdx ? "play" : "pause" ]();
  1886. // Trigger custom cycling event hook
  1887. this.trigger( "cycle", {
  1888. position: {
  1889. previous: idx,
  1890. current: nextIdx
  1891. }
  1892. });
  1893. // Set the previous back to it's beginning time
  1894. // $popprev.currentTime( ioVideos[ idx ].in );
  1895. if ( nextIdx ) {
  1896. // Hide the currently ending video
  1897. current.style.display = "none";
  1898. // Show the next video in the sequence
  1899. next.style.display = "";
  1900. }
  1901. this.cycling = false;
  1902. }
  1903. return this;
  1904. };
  1905. var excludes = [ "timeupdate", "play", "pause" ];
  1906. // Sequence object prototype
  1907. Popcorn.extend( Popcorn.sequence.prototype, {
  1908. // Returns Popcorn object from sequence at index
  1909. eq: function( idx ) {
  1910. return this.playlist[ idx ];
  1911. },
  1912. // Remove a sequence from it's playback display container
  1913. remove: function() {
  1914. this.parent.innerHTML = null;
  1915. },
  1916. // Returns Clip object from sequence at index
  1917. clip: function( idx ) {
  1918. return this.inOuts.ofVideos[ idx ];
  1919. },
  1920. // Returns sum duration for all videos in sequence
  1921. duration: function() {
  1922. var ret = 0,
  1923. seq = this.inOuts.ofClips,
  1924. idx = 0;
  1925. for ( ; idx < seq.length; idx++ ) {
  1926. ret += seq[ idx ]["out"] - seq[ idx ]["in"] + 1;
  1927. }
  1928. return ret - 1;
  1929. },
  1930. play: function() {
  1931. this.playlist[ this.active ].play();
  1932. return this;
  1933. },
  1934. // Attach an event to a single point in time
  1935. exec: function ( time, fn ) {
  1936. var index = this.active;
  1937. this.inOuts.ofClips.forEach(function( off, idx ) {
  1938. if ( time >= off["in"] && time <= off["out"] ) {
  1939. index = idx;
  1940. }
  1941. });
  1942. //offsetBy = time - self.inOuts.ofVideos[ index ].in;
  1943. time += this.inOuts.ofVideos[ index ]["in"] - this.inOuts.ofClips[ index ]["in"];
  1944. // Creating a one second track event with an empty end
  1945. Popcorn.addTrackEvent( this.playlist[ index ], {
  1946. start: time - 1,
  1947. end: time,
  1948. _running: false,
  1949. _natives: {
  1950. start: fn || Popcorn.nop,
  1951. end: Popcorn.nop,
  1952. type: "exec"
  1953. }
  1954. });
  1955. return this;
  1956. },
  1957. // Binds event handlers that fire only when all
  1958. // videos in sequence have heard the event
  1959. listen: function( type, callback ) {
  1960. var self = this,
  1961. seq = this.playlist,
  1962. total = seq.length,
  1963. count = 0,
  1964. fnName;
  1965. if ( !callback ) {
  1966. callback = Popcorn.nop;
  1967. }
  1968. // Handling for DOM and Media events
  1969. if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) {
  1970. Popcorn.forEach( seq, function( video ) {
  1971. video.listen( type, function( event ) {
  1972. event.active = self;
  1973. if ( excludes.indexOf( type ) > -1 ) {
  1974. callback.call( video, event );
  1975. } else {
  1976. if ( ++count === total ) {
  1977. callback.call( video, event );
  1978. }
  1979. }
  1980. });
  1981. });
  1982. } else {
  1983. // If no events registered with this name, create a cache
  1984. if ( !this.events[ type ] ) {
  1985. this.events[ type ] = {};
  1986. }
  1987. // Normalize a callback name key
  1988. fnName = callback.name || Popcorn.guid( "__" + type );
  1989. // Store in event cache
  1990. this.events[ type ][ fnName ] = callback;
  1991. }
  1992. // Return the sequence object
  1993. return this;
  1994. },
  1995. unlisten: function( type, name ) {
  1996. // TODO: finish implementation
  1997. },
  1998. trigger: function( type, data ) {
  1999. var self = this;
  2000. // Handling for DOM and Media events
  2001. if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) {
  2002. // find the active video and trigger api events on that video.
  2003. return;
  2004. } else {
  2005. // Only proceed if there are events of this type
  2006. // currently registered on the sequence
  2007. if ( this.events[ type ] ) {
  2008. Popcorn.forEach( this.events[ type ], function( callback, name ) {
  2009. callback.call( self, { type: type }, data );
  2010. });
  2011. }
  2012. }
  2013. return this;
  2014. }
  2015. });
  2016. Popcorn.forEach( Popcorn.manifest, function( obj, plugin ) {
  2017. // Implement passthrough methods to plugins
  2018. Popcorn.sequence.prototype[ plugin ] = function( options ) {
  2019. // console.log( this, options );
  2020. var videos = {}, assignTo = [],
  2021. idx, off, inOuts, inIdx, outIdx, keys, clip, clipInOut, clipRange;
  2022. for ( idx = 0; idx < this.inOuts.ofClips.length; idx++ ) {
  2023. // store reference
  2024. off = this.inOuts.ofClips[ idx ];
  2025. // array to test against
  2026. inOuts = range( off["in"], off["out"] );
  2027. inIdx = inOuts.indexOf( options.start );
  2028. outIdx = inOuts.indexOf( options.end );
  2029. if ( inIdx > -1 ) {
  2030. videos[ idx ] = Popcorn.extend( {}, off, {
  2031. start: inOuts[ inIdx ],
  2032. clipIdx: inIdx
  2033. });
  2034. }
  2035. if ( outIdx > -1 ) {
  2036. videos[ idx ] = Popcorn.extend( {}, off, {
  2037. end: inOuts[ outIdx ],
  2038. clipIdx: outIdx
  2039. });
  2040. }
  2041. }
  2042. keys = Object.keys( videos ).map(function( val ) {
  2043. return +val;
  2044. });
  2045. assignTo = range( keys[ 0 ], keys[ 1 ] );
  2046. //console.log( "PLUGIN CALL MAPS: ", videos, keys, assignTo );
  2047. for ( idx = 0; idx < assignTo.length; idx++ ) {
  2048. var compile = {},
  2049. play = assignTo[ idx ],
  2050. vClip = videos[ play ];
  2051. if ( vClip ) {
  2052. // has instructions
  2053. clip = this.inOuts.ofVideos[ play ];
  2054. clipInOut = vClip.clipIdx;
  2055. clipRange = range( clip["in"], clip["out"] );
  2056. if ( vClip.start ) {
  2057. compile.start = clipRange[ clipInOut ];
  2058. compile.end = clipRange[ clipRange.length - 1 ];
  2059. }
  2060. if ( vClip.end ) {
  2061. compile.start = clipRange[ 0 ];
  2062. compile.end = clipRange[ clipInOut ];
  2063. }
  2064. //compile.start += 0.1;
  2065. //compile.end += 0.9;
  2066. } else {
  2067. compile.start = this.inOuts.ofVideos[ play ]["in"];
  2068. compile.end = this.inOuts.ofVideos[ play ]["out"];
  2069. //compile.start += 0.1;
  2070. //compile.end += 0.9;
  2071. }
  2072. // Handling full clip persistance
  2073. //if ( compile.start === compile.end ) {
  2074. //compile.start -= 0.1;
  2075. //compile.end += 0.9;
  2076. //}
  2077. // Call the plugin on the appropriate Popcorn object in the playlist
  2078. // Merge original options object & compiled (start/end) object into
  2079. // a new fresh object
  2080. this.playlist[ play ][ plugin ](
  2081. Popcorn.extend( {}, options, compile )
  2082. );
  2083. }
  2084. // Return the sequence object
  2085. return this;
  2086. };
  2087. });
  2088. })( this, Popcorn );
  2089. (function( Popcorn ) {
  2090. document.addEventListener( "DOMContentLoaded", function() {
  2091. // Supports non-specific elements
  2092. var dataAttr = "data-timeline-sources",
  2093. medias = document.querySelectorAll( "[" + dataAttr + "]" );
  2094. Popcorn.forEach( medias, function( idx, key ) {
  2095. var media = medias[ key ],
  2096. hasDataSources = false,
  2097. dataSources, data, popcornMedia;
  2098. // Ensure that the DOM has an id
  2099. if ( !media.id ) {
  2100. media.id = Popcorn.guid( "__popcorn" );
  2101. }
  2102. // Ensure we're looking at a dom node
  2103. if ( media.nodeType && media.nodeType === 1 ) {
  2104. popcornMedia = Popcorn( "#" + media.id );
  2105. dataSources = ( media.getAttribute( dataAttr ) || "" ).split( "," );
  2106. if ( dataSources[ 0 ] ) {
  2107. Popcorn.forEach( dataSources, function( source ) {
  2108. // split the parser and data as parser!file
  2109. data = source.split( "!" );
  2110. // if no parser is defined for the file, assume "parse" + file extension
  2111. if ( data.length === 1 ) {
  2112. // parse a relative URL for the filename, split to get extension
  2113. data = source.match( /(.*)[\/\\]([^\/\\]+\.\w+)$/ )[ 2 ].split( "." );
  2114. data[ 0 ] = "parse" + data[ 1 ].toUpperCase();
  2115. data[ 1 ] = source;
  2116. }
  2117. // If the media has data sources and the correct parser is registered, continue to load
  2118. if ( dataSources[ 0 ] && popcornMedia[ data[ 0 ] ] ) {
  2119. // Set up the media and load in the datasources
  2120. popcornMedia[ data[ 0 ] ]( data[ 1 ] );
  2121. }
  2122. });
  2123. }
  2124. // Only play the media if it was specified to do so
  2125. if ( !!popcornMedia.autoplay() ) {
  2126. popcornMedia.play();
  2127. }
  2128. }
  2129. });
  2130. }, false );
  2131. })( Popcorn );(function( global, Popcorn ) {
  2132. var navigator = global.navigator;
  2133. // Initialize locale data
  2134. // Based on http://en.wikipedia.org/wiki/Language_localisation#Language_tags_and_codes
  2135. function initLocale( arg ) {
  2136. var locale = typeof arg === "string" ? arg : [ arg.language, arg.region ].join( "-" ),
  2137. parts = locale.split( "-" );
  2138. // Setup locale data table
  2139. return {
  2140. iso6391: locale,
  2141. language: parts[ 0 ] || "",
  2142. region: parts[ 1 ] || ""
  2143. };
  2144. }
  2145. // Declare locale data table
  2146. var localeData = initLocale( navigator.userLanguage || navigator.language );
  2147. Popcorn.locale = {
  2148. // Popcorn.locale.get()
  2149. // returns reference to privately
  2150. // defined localeData
  2151. get: function() {
  2152. return localeData;
  2153. },
  2154. // Popcorn.locale.set( string|object );
  2155. set: function( arg ) {
  2156. localeData = initLocale( arg );
  2157. Popcorn.locale.broadcast();
  2158. return localeData;
  2159. },
  2160. // Popcorn.locale.broadcast( type )
  2161. // Sends events to all popcorn media instances that are
  2162. // listening for locale events
  2163. broadcast: function( type ) {
  2164. var instances = Popcorn.instances,
  2165. length = instances.length,
  2166. idx = 0,
  2167. instance;
  2168. type = type || "locale:changed";
  2169. // Iterate all current instances
  2170. for ( ; idx < length; idx++ ) {
  2171. instance = instances[ idx ];
  2172. // For those instances with locale event listeners,
  2173. // trigger a locale change event
  2174. if ( type in instance.data.events ) {
  2175. instance.trigger( type );
  2176. }
  2177. }
  2178. }
  2179. };
  2180. })( this, this.Popcorn );(function( Popcorn ) {
  2181. var
  2182. AP = Array.prototype,
  2183. OP = Object.prototype,
  2184. forEach = AP.forEach,
  2185. slice = AP.slice,
  2186. hasOwn = OP.hasOwnProperty,
  2187. toString = OP.toString;
  2188. // stores parsers keyed on filetype
  2189. Popcorn.parsers = {};
  2190. // An interface for extending Popcorn
  2191. // with parser functionality
  2192. Popcorn.parser = function( name, type, definition ) {
  2193. if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) {
  2194. Popcorn.error( "'" + name + "' is a protected function name" );
  2195. return;
  2196. }
  2197. // fixes parameters for overloaded function call
  2198. if ( typeof type === "function" && !definition ) {
  2199. definition = type;
  2200. type = "";
  2201. }
  2202. if ( typeof definition !== "function" || typeof type !== "string" ) {
  2203. return;
  2204. }
  2205. // Provides some sugar, but ultimately extends
  2206. // the definition into Popcorn.p
  2207. var natives = Popcorn.events.all,
  2208. parseFn,
  2209. parser = {};
  2210. parseFn = function( filename, callback ) {
  2211. if ( !filename ) {
  2212. return this;
  2213. }
  2214. var that = this;
  2215. Popcorn.xhr({
  2216. url: filename,
  2217. dataType: type,
  2218. success: function( data ) {
  2219. var tracksObject = definition( data ),
  2220. tracksData,
  2221. tracksDataLen,
  2222. tracksDef,
  2223. idx = 0;
  2224. tracksData = tracksObject.data || [];
  2225. tracksDataLen = tracksData.length;
  2226. tracksDef = null;
  2227. // If no tracks to process, return immediately
  2228. if ( !tracksDataLen ) {
  2229. return;
  2230. }
  2231. // Create tracks out of parsed object
  2232. for ( ; idx < tracksDataLen; idx++ ) {
  2233. tracksDef = tracksData[ idx ];
  2234. for ( var key in tracksDef ) {
  2235. if ( hasOwn.call( tracksDef, key ) && !!that[ key ] ) {
  2236. that[ key ]( tracksDef[ key ] );
  2237. }
  2238. }
  2239. }
  2240. if ( callback ) {
  2241. callback();
  2242. }
  2243. }
  2244. });
  2245. return this;
  2246. };
  2247. // Assign new named definition
  2248. parser[ name ] = parseFn;
  2249. // Extend Popcorn.p with new named definition
  2250. Popcorn.extend( Popcorn.p, parser );
  2251. // keys the function name by filetype extension
  2252. //Popcorn.parsers[ name ] = true;
  2253. return parser;
  2254. };
  2255. })( Popcorn );(function( Popcorn ) {
  2256. // combines calls of two function calls into one
  2257. var combineFn = function( first, second ) {
  2258. first = first || Popcorn.nop;
  2259. second = second || Popcorn.nop;
  2260. return function() {
  2261. first.apply( this, arguments );
  2262. second.apply( this, arguments );
  2263. };
  2264. };
  2265. // ID string matching
  2266. var rIdExp = /^(#([\w\-\_\.]+))$/;
  2267. var audioExtensions = "ogg|oga|aac|mp3|wav",
  2268. videoExtensions = "ogg|ogv|mp4|webm",
  2269. mediaExtensions = audioExtensions + "|" + videoExtensions;
  2270. var audioExtensionRegexp = new RegExp( "^.*\\.(" + audioExtensions + ")($|\\?)" ),
  2271. mediaExtensionRegexp = new RegExp( "^.*\\.(" + mediaExtensions + ")($|\\?)" );
  2272. Popcorn.player = function( name, player ) {
  2273. // return early if a player already exists under this name
  2274. if ( Popcorn[ name ] ) {
  2275. return;
  2276. }
  2277. player = player || {};
  2278. var playerFn = function( target, src, options ) {
  2279. options = options || {};
  2280. // List of events
  2281. var date = new Date() / 1000,
  2282. baselineTime = date,
  2283. currentTime = 0,
  2284. readyState = 0,
  2285. volume = 1,
  2286. muted = false,
  2287. events = {},
  2288. // The container div of the resource
  2289. container = typeof target === "string" ? Popcorn.dom.find( target ) : target,
  2290. basePlayer = {},
  2291. timeout,
  2292. popcorn;
  2293. if ( !Object.prototype.__defineGetter__ ) {
  2294. basePlayer = container || document.createElement( "div" );
  2295. }
  2296. // copies a div into the media object
  2297. for( var val in container ) {
  2298. // don't copy properties if using container as baseplayer
  2299. if ( val in basePlayer ) {
  2300. continue;
  2301. }
  2302. if ( typeof container[ val ] === "object" ) {
  2303. basePlayer[ val ] = container[ val ];
  2304. } else if ( typeof container[ val ] === "function" ) {
  2305. basePlayer[ val ] = (function( value ) {
  2306. // this is a stupid ugly kludgy hack in honour of Safari
  2307. // in Safari a NodeList is a function, not an object
  2308. if ( "length" in container[ value ] && !container[ value ].call ) {
  2309. return container[ value ];
  2310. } else {
  2311. return function() {
  2312. return container[ value ].apply( container, arguments );
  2313. };
  2314. }
  2315. }( val ));
  2316. } else {
  2317. Popcorn.player.defineProperty( basePlayer, val, {
  2318. get: (function( value ) {
  2319. return function() {
  2320. return container[ value ];
  2321. };
  2322. }( val )),
  2323. set: Popcorn.nop,
  2324. configurable: true
  2325. });
  2326. }
  2327. }
  2328. var timeupdate = function() {
  2329. date = new Date() / 1000;
  2330. if ( !basePlayer.paused ) {
  2331. basePlayer.currentTime = basePlayer.currentTime + ( date - baselineTime );
  2332. basePlayer.dispatchEvent( "timeupdate" );
  2333. timeout = setTimeout( timeupdate, 10 );
  2334. }
  2335. baselineTime = date;
  2336. };
  2337. basePlayer.play = function() {
  2338. this.paused = false;
  2339. if ( basePlayer.readyState >= 4 ) {
  2340. baselineTime = new Date() / 1000;
  2341. basePlayer.dispatchEvent( "play" );
  2342. timeupdate();
  2343. }
  2344. };
  2345. basePlayer.pause = function() {
  2346. this.paused = true;
  2347. basePlayer.dispatchEvent( "pause" );
  2348. };
  2349. Popcorn.player.defineProperty( basePlayer, "currentTime", {
  2350. get: function() {
  2351. return currentTime;
  2352. },
  2353. set: function( val ) {
  2354. // make sure val is a number
  2355. currentTime = +val;
  2356. basePlayer.dispatchEvent( "timeupdate" );
  2357. return currentTime;
  2358. },
  2359. configurable: true
  2360. });
  2361. Popcorn.player.defineProperty( basePlayer, "volume", {
  2362. get: function() {
  2363. return volume;
  2364. },
  2365. set: function( val ) {
  2366. // make sure val is a number
  2367. volume = +val;
  2368. basePlayer.dispatchEvent( "volumechange" );
  2369. return volume;
  2370. },
  2371. configurable: true
  2372. });
  2373. Popcorn.player.defineProperty( basePlayer, "muted", {
  2374. get: function() {
  2375. return muted;
  2376. },
  2377. set: function( val ) {
  2378. // make sure val is a number
  2379. muted = +val;
  2380. basePlayer.dispatchEvent( "volumechange" );
  2381. return muted;
  2382. },
  2383. configurable: true
  2384. });
  2385. Popcorn.player.defineProperty( basePlayer, "readyState", {
  2386. get: function() {
  2387. return readyState;
  2388. },
  2389. set: function( val ) {
  2390. readyState = val;
  2391. return readyState;
  2392. },
  2393. configurable: true
  2394. });
  2395. // Adds an event listener to the object
  2396. basePlayer.addEventListener = function( evtName, fn ) {
  2397. if ( !events[ evtName ] ) {
  2398. events[ evtName ] = [];
  2399. }
  2400. events[ evtName ].push( fn );
  2401. return fn;
  2402. };
  2403. // Removes an event listener from the object
  2404. basePlayer.removeEventListener = function( evtName, fn ) {
  2405. var i,
  2406. listeners = events[ evtName ];
  2407. if ( !listeners ){
  2408. return;
  2409. }
  2410. // walk backwards so we can safely splice
  2411. for ( i = events[ evtName ].length - 1; i >= 0; i-- ) {
  2412. if( fn === listeners[ i ] ) {
  2413. listeners.splice(i, 1);
  2414. }
  2415. }
  2416. return fn;
  2417. };
  2418. // Can take event object or simple string
  2419. basePlayer.dispatchEvent = function( oEvent ) {
  2420. var evt,
  2421. self = this,
  2422. eventInterface,
  2423. eventName = oEvent.type;
  2424. // A string was passed, create event object
  2425. if ( !eventName ) {
  2426. eventName = oEvent;
  2427. eventInterface = Popcorn.events.getInterface( eventName );
  2428. if ( eventInterface ) {
  2429. evt = document.createEvent( eventInterface );
  2430. evt.initEvent( eventName, true, true, window, 1 );
  2431. }
  2432. }
  2433. if ( events[ eventName ] ) {
  2434. for ( var i = events[ eventName ].length - 1; i >= 0; i-- ) {
  2435. events[ eventName ][ i ].call( self, evt, self );
  2436. }
  2437. }
  2438. };
  2439. // Attempt to get src from playerFn parameter
  2440. basePlayer.src = src || "";
  2441. basePlayer.duration = 0;
  2442. basePlayer.paused = true;
  2443. basePlayer.ended = 0;
  2444. options && options.events && Popcorn.forEach( options.events, function( val, key ) {
  2445. basePlayer.addEventListener( key, val, false );
  2446. });
  2447. // true and undefined returns on canPlayType means we should attempt to use it,
  2448. // false means we cannot play this type
  2449. if ( player._canPlayType( container.nodeName, src ) !== false ) {
  2450. if ( player._setup ) {
  2451. player._setup.call( basePlayer, options );
  2452. } else {
  2453. // there is no setup, which means there is nothing to load
  2454. basePlayer.readyState = 4;
  2455. basePlayer.dispatchEvent( "loadedmetadata" );
  2456. basePlayer.dispatchEvent( "loadeddata" );
  2457. basePlayer.dispatchEvent( "canplaythrough" );
  2458. }
  2459. } else {
  2460. // Asynchronous so that users can catch this event
  2461. setTimeout( function() {
  2462. basePlayer.dispatchEvent( "error" );
  2463. }, 0 );
  2464. }
  2465. popcorn = new Popcorn.p.init( basePlayer, options );
  2466. if ( player._teardown ) {
  2467. popcorn.destroy = combineFn( popcorn.destroy, function() {
  2468. player._teardown.call( basePlayer, options );
  2469. });
  2470. }
  2471. return popcorn;
  2472. };
  2473. playerFn.canPlayType = player._canPlayType = player._canPlayType || Popcorn.nop;
  2474. Popcorn[ name ] = Popcorn.player.registry[ name ] = playerFn;
  2475. };
  2476. Popcorn.player.registry = {};
  2477. Popcorn.player.defineProperty = Object.defineProperty || function( object, description, options ) {
  2478. object.__defineGetter__( description, options.get || Popcorn.nop );
  2479. object.__defineSetter__( description, options.set || Popcorn.nop );
  2480. };
  2481. // player queue is to help players queue things like play and pause
  2482. // HTML5 video's play and pause are asynch, but do fire in sequence
  2483. // play() should really mean "requestPlay()" or "queuePlay()" and
  2484. // stash a callback that will play the media resource when it's ready to be played
  2485. Popcorn.player.playerQueue = function() {
  2486. var _queue = [],
  2487. _running = false;
  2488. return {
  2489. next: function() {
  2490. _running = false;
  2491. _queue.shift();
  2492. _queue[ 0 ] && _queue[ 0 ]();
  2493. },
  2494. add: function( callback ) {
  2495. _queue.push(function() {
  2496. _running = true;
  2497. callback && callback();
  2498. });
  2499. // if there is only one item on the queue, start it
  2500. !_running && _queue[ 0 ]();
  2501. }
  2502. };
  2503. };
  2504. // smart will attempt to find you a match, if it does not find a match,
  2505. // it will attempt to create a video element with the source,
  2506. // if that failed, it will throw.
  2507. Popcorn.smart = function( target, src, options ) {
  2508. var playerType,
  2509. elementTypes = [ "AUDIO", "VIDEO" ],
  2510. sourceNode,
  2511. firstSrc,
  2512. node = Popcorn.dom.find( target ),
  2513. i, srcResult,
  2514. canPlayTypeTester = document.createElement( "video" ),
  2515. canPlayTypes = {
  2516. "ogg": "video/ogg",
  2517. "ogv": "video/ogg",
  2518. "oga": "audio/ogg",
  2519. "webm": "video/webm",
  2520. "mp4": "video/mp4",
  2521. "mp3": "audio/mp3"
  2522. };
  2523. var canPlayType = function( type ) {
  2524. return canPlayTypeTester.canPlayType( canPlayTypes[ type ] );
  2525. };
  2526. var canPlaySrc = function( src ) {
  2527. srcResult = mediaExtensionRegexp.exec( src );
  2528. if ( !srcResult || !srcResult[ 1 ] ) {
  2529. return false;
  2530. }
  2531. return canPlayType( srcResult[ 1 ] );
  2532. };
  2533. if ( !node ) {
  2534. Popcorn.error( "Specified target " + target + " was not found." );
  2535. return;
  2536. }
  2537. // For when no src is defined.
  2538. // Usually this is a video element with a src already on it.
  2539. if ( elementTypes.indexOf( node.nodeName ) > -1 && !src ) {
  2540. if ( typeof src === "object" ) {
  2541. options = src;
  2542. src = undefined;
  2543. }
  2544. return Popcorn( node, options );
  2545. }
  2546. // if our src is not an array, create an array of one.
  2547. if ( typeof( src ) === "string" ) {
  2548. src = [ src ];
  2549. }
  2550. // go through each src, and find the first playable.
  2551. // this only covers player sources popcorn knows of,
  2552. // and not things like a youtube src that is private.
  2553. // it will still consider a private youtube video to be playable.
  2554. for ( i = 0, srcLength = src.length; i < srcLength; i++ ) {
  2555. // src is a playable HTML5 video, we don't need to check custom players.
  2556. if ( canPlaySrc( src[ i ] ) ) {
  2557. src = src[ i ];
  2558. break;
  2559. }
  2560. // for now we loop through and use the first valid player we find.
  2561. for ( var key in Popcorn.player.registry ) {
  2562. if ( Popcorn.player.registry.hasOwnProperty( key ) ) {
  2563. if ( Popcorn.player.registry[ key ].canPlayType( node.nodeName, src[ i ] ) ) {
  2564. // Popcorn.smart( player, src, /* options */ )
  2565. return Popcorn[ key ]( node, src[ i ], options );
  2566. }
  2567. }
  2568. }
  2569. }
  2570. // Popcorn.smart( div, src, /* options */ )
  2571. // attempting to create a video in a container
  2572. if ( elementTypes.indexOf( node.nodeName ) === -1 ) {
  2573. firstSrc = typeof( src ) === "string" ? src : src.length ? src[ 0 ] : src;
  2574. target = document.createElement( !!audioExtensionRegexp.exec( firstSrc ) ? elementTypes[ 0 ] : elementTypes[ 1 ] );
  2575. // Controls are defaulted to being present
  2576. target.controls = true;
  2577. node.appendChild( target );
  2578. node = target;
  2579. }
  2580. options && options.events && options.events.error && node.addEventListener( "error", options.events.error, false );
  2581. node.src = src;
  2582. return Popcorn( node, options );
  2583. };
  2584. })( Popcorn );
  2585. // PLUGIN: mediaspawner
  2586. /**
  2587. * mediaspawner Popcorn Plugin.
  2588. * Adds Video/Audio to the page using Popcorns players
  2589. * Start is the time that you want this plug-in to execute
  2590. * End is the time that you want this plug-in to stop executing
  2591. *
  2592. * @param {HTML} options
  2593. *
  2594. * Example:
  2595. var p = Popcorn('#video')
  2596. .mediaspawner( {
  2597. source: "http://www.youtube.com/watch?v=bUB1L3zGVvc",
  2598. target: "mediaspawnerdiv",
  2599. start: 1,
  2600. end: 10,
  2601. caption: "This is a test. We are assuming conrol. We are assuming control."
  2602. })
  2603. *
  2604. */
  2605. (function ( Popcorn, global ) {
  2606. var PLAYER_URL = "http://popcornjs.org/code/modules/player/popcorn.player.js",
  2607. urlRegex = /(?:http:\/\/www\.|http:\/\/|www\.|\.|^)(youtu|vimeo|soundcloud|baseplayer)/,
  2608. forEachPlayer,
  2609. playerTypeLoading = {},
  2610. playerTypesLoaded = {
  2611. "vimeo": false,
  2612. "youtube": false,
  2613. "soundcloud": false,
  2614. "module": false
  2615. };
  2616. Object.defineProperty( playerTypeLoading, forEachPlayer, {
  2617. get: function() {
  2618. return playerTypesLoaded[ forEachPlayer ];
  2619. },
  2620. set: function( val ) {
  2621. playerTypesLoaded[ forEachPlayer ] = val;
  2622. }
  2623. });
  2624. Popcorn.plugin( "mediaspawner", {
  2625. manifest: {
  2626. about: {
  2627. name: "Popcorn Media Spawner Plugin",
  2628. version: "0.1",
  2629. author: "Matthew Schranz, @mjschranz",
  2630. website: "mschranz.wordpress.com"
  2631. },
  2632. options: {
  2633. source: {
  2634. elem: "input",
  2635. type: "text",
  2636. label: "Media Source",
  2637. "default": "http://www.youtube.com/watch?v=CXDstfD9eJ0"
  2638. },
  2639. caption: {
  2640. elem: "input",
  2641. type: "text",
  2642. label: "Media Caption",
  2643. "default": "Popcorn Popping",
  2644. optional: true
  2645. },
  2646. target: "mediaspawner-container",
  2647. start: {
  2648. elem: "input",
  2649. type: "number",
  2650. label: "Start"
  2651. },
  2652. end: {
  2653. elem: "input",
  2654. type: "number",
  2655. label: "End"
  2656. },
  2657. autoplay: {
  2658. elem: "input",
  2659. type: "checkbox",
  2660. label: "Autoplay Video",
  2661. optional: true
  2662. },
  2663. width: {
  2664. elem: "input",
  2665. type: "number",
  2666. label: "Media Width",
  2667. "default": 400,
  2668. units: "px",
  2669. optional: true
  2670. },
  2671. height: {
  2672. elem: "input",
  2673. type: "number",
  2674. label: "Media Height",
  2675. "default": 200,
  2676. units: "px",
  2677. optional: true
  2678. }
  2679. }
  2680. },
  2681. _setup: function( options ) {
  2682. var target = document.getElementById( options.target ) || {},
  2683. mediaType,
  2684. container,
  2685. capContainer,
  2686. regexResult;
  2687. regexResult = urlRegex.exec( options.source );
  2688. if ( regexResult ) {
  2689. mediaType = regexResult[ 1 ];
  2690. // our regex only handles youtu ( incase the url looks something like youtu.be )
  2691. if ( mediaType === "youtu" ) {
  2692. mediaType = "youtube";
  2693. }
  2694. }
  2695. else {
  2696. // if the regex didn't return anything we know it's an HTML5 source
  2697. mediaType = "HTML5";
  2698. }
  2699. // Store Reference to Type for use in end
  2700. options._type = mediaType;
  2701. // Create separate container for plugin
  2702. options._container = document.createElement( "div" );
  2703. container = options._container;
  2704. container.id = "mediaSpawnerdiv-" + Popcorn.guid();
  2705. // Default width and height of media
  2706. options.width = options.width || 400;
  2707. options.height = options.height || 200;
  2708. // Captions now need to be in their own container, due to the problem with flash players
  2709. // described in start/end
  2710. if ( options.caption ) {
  2711. capContainer = document.createElement( "div" );
  2712. capContainer.innerHTML = options.caption;
  2713. capContainer.style.display = "none";
  2714. options._capCont = capContainer;
  2715. container.appendChild( capContainer );
  2716. }
  2717. target && target.appendChild( container );
  2718. function constructMedia(){
  2719. function checkPlayerTypeLoaded() {
  2720. if ( mediaType !== "HTML5" && !window.Popcorn[ mediaType ] ) {
  2721. setTimeout( function() {
  2722. checkPlayerTypeLoaded();
  2723. }, 300 );
  2724. } else {
  2725. options.id = options._container.id;
  2726. // Set the width/height of the container before calling Popcorn.smart
  2727. // Allows youtube to pickup on the specified height an create the player
  2728. // with specified dimensions
  2729. options._container.style.width = options.width + "px";
  2730. options._container.style.height = options.height + "px";
  2731. options.popcorn = Popcorn.smart( "#" + options.id, options.source );
  2732. if ( mediaType === "HTML5" ) {
  2733. options.popcorn.controls( true );
  2734. }
  2735. // Set them to 0 now so it is hidden
  2736. options._container.style.width = "0px";
  2737. options._container.style.height = "0px";
  2738. options._container.style.visibility = "hidden";
  2739. options._container.style.overflow = "hidden";
  2740. }
  2741. }
  2742. if ( mediaType !== "HTML5" && !window.Popcorn[ mediaType ] && !playerTypeLoading[ mediaType ] ) {
  2743. playerTypeLoading[ mediaType ] = true;
  2744. Popcorn.getScript( "http://popcornjs.org/code/players/" + mediaType + "/popcorn." + mediaType + ".js", function() {
  2745. checkPlayerTypeLoaded();
  2746. });
  2747. }
  2748. else {
  2749. checkPlayerTypeLoaded();
  2750. }
  2751. }
  2752. // If Player script needed to be loaded, keep checking until it is and then fire readycallback
  2753. function isPlayerReady() {
  2754. if ( !window.Popcorn.player ) {
  2755. setTimeout( function () {
  2756. isPlayerReady();
  2757. }, 300 );
  2758. } else {
  2759. constructMedia();
  2760. }
  2761. }
  2762. // If player script isn't present, retrieve script
  2763. if ( !window.Popcorn.player && !playerTypeLoading.module ) {
  2764. playerTypeLoading.module = true;
  2765. Popcorn.getScript( PLAYER_URL, isPlayerReady );
  2766. } else {
  2767. isPlayerReady();
  2768. }
  2769. },
  2770. start: function( event, options ) {
  2771. if( options._capCont ) {
  2772. options._capCont.style.display = "";
  2773. }
  2774. /* Using this style for Start/End is required because of the flash players
  2775. * Without it on end an internal cleanup is called, causing the flash players
  2776. * to be out of sync with Popcorn, as they are then rebuilt.
  2777. */
  2778. options._container.style.width = options.width + "px";
  2779. options._container.style.height = options.height + "px";
  2780. options._container.style.visibility = "visible";
  2781. options._container.style.overflow = "visible";
  2782. if ( options.autoplay ) {
  2783. options.popcorn.play();
  2784. }
  2785. },
  2786. end: function( event, options ) {
  2787. if( options._capCont ) {
  2788. options._capCont.style.display = "none";
  2789. }
  2790. /* Using this style for Start/End is required because of the flash players
  2791. * Without it on end an internal cleanup is called, causing the flash players
  2792. * to be out of sync with Popcorn, as they are then rebuilt.
  2793. */
  2794. options._container.style.width = "0px";
  2795. options._container.style.height = "0px";
  2796. options._container.style.visibility = "hidden";
  2797. options._container.style.overflow = "hidden";
  2798. // Pause all popcorn instances on exit
  2799. options.popcorn.pause();
  2800. },
  2801. _teardown: function( options ) {
  2802. if ( options.popcorn && options.popcorn.destory ) {
  2803. options.popcorn.destroy();
  2804. }
  2805. document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( options._container );
  2806. }
  2807. });
  2808. })( Popcorn, this );
  2809. // PLUGIN: Code
  2810. (function ( Popcorn ) {
  2811. /**
  2812. * Code Popcorn Plug-in
  2813. *
  2814. * Adds the ability to run arbitrary code (JavaScript functions) according to video timing.
  2815. *
  2816. * @param {Object} options
  2817. *
  2818. * Required parameters: start, end, template, data, and target.
  2819. * Optional parameter: static.
  2820. *
  2821. * start: the time in seconds when the mustache template should be rendered
  2822. * in the target div.
  2823. *
  2824. * end: the time in seconds when the rendered mustache template should be
  2825. * removed from the target div.
  2826. *
  2827. * onStart: the function to be run when the start time is reached.
  2828. *
  2829. * onFrame: [optional] a function to be run on each paint call
  2830. * (e.g., called ~60 times per second) between the start and end times.
  2831. *
  2832. * onEnd: [optional] a function to be run when the end time is reached.
  2833. *
  2834. * Example:
  2835. var p = Popcorn('#video')
  2836. // onStart function only
  2837. .code({
  2838. start: 1,
  2839. end: 4,
  2840. onStart: function( options ) {
  2841. // called on start
  2842. }
  2843. })
  2844. // onStart + onEnd only
  2845. .code({
  2846. start: 6,
  2847. end: 8,
  2848. onStart: function( options ) {
  2849. // called on start
  2850. },
  2851. onEnd: function ( options ) {
  2852. // called on end
  2853. }
  2854. })
  2855. // onStart, onEnd, onFrame
  2856. .code({
  2857. start: 10,
  2858. end: 14,
  2859. onStart: function( options ) {
  2860. // called on start
  2861. },
  2862. onFrame: function ( options ) {
  2863. // called on every paint frame between start and end.
  2864. // uses mozRequestAnimationFrame, webkitRequestAnimationFrame,
  2865. // or setTimeout with 16ms window.
  2866. },
  2867. onEnd: function ( options ) {
  2868. // called on end
  2869. }
  2870. });
  2871. *
  2872. */
  2873. Popcorn.plugin( "code" , function( options ) {
  2874. var running = false,
  2875. instance = this;
  2876. // Setup a proper frame interval function (60fps), favouring paint events.
  2877. var step = (function() {
  2878. var buildFrameRunner = function( runner ) {
  2879. return function( f, options ) {
  2880. var _f = function() {
  2881. running && f.call( instance, options );
  2882. running && runner( _f );
  2883. };
  2884. _f();
  2885. };
  2886. };
  2887. // Figure out which level of browser support we have for this
  2888. if ( window.webkitRequestAnimationFrame ) {
  2889. return buildFrameRunner( window.webkitRequestAnimationFrame );
  2890. } else if ( window.mozRequestAnimationFrame ) {
  2891. return buildFrameRunner( window.mozRequestAnimationFrame );
  2892. } else {
  2893. return buildFrameRunner( function( f ) {
  2894. window.setTimeout( f, 16 );
  2895. });
  2896. }
  2897. })();
  2898. if ( !options.onStart || typeof options.onStart !== "function" ) {
  2899. options.onStart = Popcorn.nop;
  2900. }
  2901. if ( options.onEnd && typeof options.onEnd !== "function" ) {
  2902. options.onEnd = undefined;
  2903. }
  2904. if ( options.onFrame && typeof options.onFrame !== "function" ) {
  2905. options.onFrame = undefined;
  2906. }
  2907. return {
  2908. start: function( event, options ) {
  2909. options.onStart.call( instance, options );
  2910. if ( options.onFrame ) {
  2911. running = true;
  2912. step( options.onFrame, options );
  2913. }
  2914. },
  2915. end: function( event, options ) {
  2916. if ( options.onFrame ) {
  2917. running = false;
  2918. }
  2919. if ( options.onEnd ) {
  2920. options.onEnd.call( instance, options );
  2921. }
  2922. }
  2923. };
  2924. },
  2925. {
  2926. about: {
  2927. name: "Popcorn Code Plugin",
  2928. version: "0.1",
  2929. author: "David Humphrey (@humphd)",
  2930. website: "http://vocamus.net/dave"
  2931. },
  2932. options: {
  2933. start: {
  2934. elem: "input",
  2935. type: "number",
  2936. label: "Start"
  2937. },
  2938. end: {
  2939. elem: "input",
  2940. type: "number",
  2941. label: "End"
  2942. },
  2943. onStart: {
  2944. elem: "input",
  2945. type: "function",
  2946. label: "onStart"
  2947. },
  2948. onFrame: {
  2949. elem: "input",
  2950. type: "function",
  2951. label: "onFrame",
  2952. optional: true
  2953. },
  2954. onEnd: {
  2955. elem: "input",
  2956. type: "function",
  2957. label: "onEnd"
  2958. }
  2959. }
  2960. });
  2961. })( Popcorn );
  2962. // PLUGIN: Flickr
  2963. (function (Popcorn) {
  2964. /**
  2965. * Flickr popcorn plug-in
  2966. * Appends a users Flickr images to an element on the page.
  2967. * Options parameter will need a start, end, target and userid or username and api_key.
  2968. * Optional parameters are numberofimages, height, width, padding, and border
  2969. * Start is the time that you want this plug-in to execute (in seconds)
  2970. * End is the time that you want this plug-in to stop executing (in seconds)
  2971. * Userid is the id of who's Flickr images you wish to show
  2972. * Tags is a mutually exclusive list of image descriptor tags
  2973. * Username is the username of who's Flickr images you wish to show
  2974. * using both userid and username is redundant
  2975. * an api_key is required when using username
  2976. * Apikey is your own api key provided by Flickr
  2977. * Target is the id of the document element that the images are
  2978. * appended to, this target element must exist on the DOM
  2979. * Numberofimages specify the number of images to retreive from flickr, defaults to 4
  2980. * Height the height of the image, defaults to '50px'
  2981. * Width the width of the image, defaults to '50px'
  2982. * Padding number of pixels between images, defaults to '5px'
  2983. * Border border size in pixels around images, defaults to '0px'
  2984. *
  2985. * @param {Object} options
  2986. *
  2987. * Example:
  2988. var p = Popcorn('#video')
  2989. .flickr({
  2990. start: 5, // seconds, mandatory
  2991. end: 15, // seconds, mandatory
  2992. userid: '35034346917@N01', // optional
  2993. tags: 'dogs,cats', // optional
  2994. numberofimages: '8', // optional
  2995. height: '50px', // optional
  2996. width: '50px', // optional
  2997. padding: '5px', // optional
  2998. border: '0px', // optional
  2999. target: 'flickrdiv' // mandatory
  3000. } )
  3001. *
  3002. */
  3003. var idx = 0;
  3004. Popcorn.plugin( "flickr" , function( options ) {
  3005. var containerDiv,
  3006. target = document.getElementById( options.target ),
  3007. _userid,
  3008. _uri,
  3009. _link,
  3010. _image,
  3011. _count = options.numberofimages || 4,
  3012. _height = options.height || "50px",
  3013. _width = options.width || "50px",
  3014. _padding = options.padding || "5px",
  3015. _border = options.border || "0px";
  3016. // create a new div this way anything in the target div is left intact
  3017. // this is later populated with Flickr images
  3018. containerDiv = document.createElement( "div" );
  3019. containerDiv.id = "flickr" + idx;
  3020. containerDiv.style.width = "100%";
  3021. containerDiv.style.height = "100%";
  3022. containerDiv.style.display = "none";
  3023. idx++;
  3024. target && target.appendChild( containerDiv );
  3025. // get the userid from Flickr API by using the username and apikey
  3026. var isUserIDReady = function() {
  3027. if ( !_userid ) {
  3028. _uri = "http://api.flickr.com/services/rest/?method=flickr.people.findByUsername&";
  3029. _uri += "username=" + options.username + "&api_key=" + options.apikey + "&format=json&jsoncallback=flickr";
  3030. Popcorn.getJSONP( _uri, function( data ) {
  3031. _userid = data.user.nsid;
  3032. getFlickrData();
  3033. });
  3034. } else {
  3035. setTimeout(function () {
  3036. isUserIDReady();
  3037. }, 5 );
  3038. }
  3039. };
  3040. // get the photos from Flickr API by using the user_id and/or tags
  3041. var getFlickrData = function() {
  3042. _uri = "http://api.flickr.com/services/feeds/photos_public.gne?";
  3043. if ( _userid ) {
  3044. _uri += "id=" + _userid + "&";
  3045. }
  3046. if ( options.tags ) {
  3047. _uri += "tags=" + options.tags + "&";
  3048. }
  3049. _uri += "lang=en-us&format=json&jsoncallback=flickr";
  3050. Popcorn.xhr.getJSONP( _uri, function( data ) {
  3051. var fragment = document.createElement( "div" );
  3052. fragment.innerHTML = "<p style='padding:" + _padding + ";'>" + data.title + "<p/>";
  3053. Popcorn.forEach( data.items, function ( item, i ) {
  3054. if ( i < _count ) {
  3055. _link = document.createElement( "a" );
  3056. _link.setAttribute( "href", item.link );
  3057. _link.setAttribute( "target", "_blank" );
  3058. _image = document.createElement( "img" );
  3059. _image.setAttribute( "src", item.media.m );
  3060. _image.setAttribute( "height",_height );
  3061. _image.setAttribute( "width", _width );
  3062. _image.setAttribute( "style", "border:" + _border + ";padding:" + _padding );
  3063. _link.appendChild( _image );
  3064. fragment.appendChild( _link );
  3065. } else {
  3066. return false;
  3067. }
  3068. });
  3069. containerDiv.appendChild( fragment );
  3070. });
  3071. };
  3072. if ( options.username && options.apikey ) {
  3073. isUserIDReady();
  3074. }
  3075. else {
  3076. _userid = options.userid;
  3077. getFlickrData();
  3078. }
  3079. return {
  3080. /**
  3081. * @member flickr
  3082. * The start function will be executed when the currentTime
  3083. * of the video reaches the start time provided by the
  3084. * options variable
  3085. */
  3086. start: function( event, options ) {
  3087. containerDiv.style.display = "inline";
  3088. },
  3089. /**
  3090. * @member flickr
  3091. * The end function will be executed when the currentTime
  3092. * of the video reaches the end time provided by the
  3093. * options variable
  3094. */
  3095. end: function( event, options ) {
  3096. containerDiv.style.display = "none";
  3097. },
  3098. _teardown: function( options ) {
  3099. document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( containerDiv );
  3100. }
  3101. };
  3102. },
  3103. {
  3104. about: {
  3105. name: "Popcorn Flickr Plugin",
  3106. version: "0.2",
  3107. author: "Scott Downe, Steven Weerdenburg, Annasob",
  3108. website: "http://scottdowne.wordpress.com/"
  3109. },
  3110. options: {
  3111. start: {
  3112. elem: "input",
  3113. type: "number",
  3114. label: "Start"
  3115. },
  3116. end: {
  3117. elem: "input",
  3118. type: "number",
  3119. label: "End"
  3120. },
  3121. userid: {
  3122. elem: "input",
  3123. type: "text",
  3124. label: "User ID",
  3125. optional: true
  3126. },
  3127. tags: {
  3128. elem: "input",
  3129. type: "text",
  3130. label: "Tags"
  3131. },
  3132. username: {
  3133. elem: "input",
  3134. type: "text",
  3135. label: "Username",
  3136. optional: true
  3137. },
  3138. apikey: {
  3139. elem: "input",
  3140. type: "text",
  3141. label: "API Key",
  3142. optional: true
  3143. },
  3144. target: "flickr-container",
  3145. height: {
  3146. elem: "input",
  3147. type: "text",
  3148. label: "Height",
  3149. "default": "50px",
  3150. optional: true
  3151. },
  3152. width: {
  3153. elem: "input",
  3154. type: "text",
  3155. label: "Width",
  3156. "default": "50px",
  3157. optional: true
  3158. },
  3159. padding: {
  3160. elem: "input",
  3161. type: "text",
  3162. label: "Padding",
  3163. optional: true
  3164. },
  3165. border: {
  3166. elem: "input",
  3167. type: "text",
  3168. label: "Border",
  3169. "default": "5px",
  3170. optional: true
  3171. },
  3172. numberofimages: {
  3173. elem: "input",
  3174. type: "number",
  3175. "default": 4,
  3176. label: "Number of Images"
  3177. }
  3178. }
  3179. });
  3180. })( Popcorn );
  3181. // PLUGIN: Footnote/Text
  3182. (function ( Popcorn ) {
  3183. /**
  3184. * Footnote popcorn plug-in
  3185. * Adds text to an element on the page.
  3186. * Options parameter will need a start, end, target and text.
  3187. * Start is the time that you want this plug-in to execute
  3188. * End is the time that you want this plug-in to stop executing
  3189. * Text is the text that you want to appear in the target
  3190. * Target is the id of the document element that the text needs to be
  3191. * attached to, this target element must exist on the DOM
  3192. *
  3193. * @param {Object} options
  3194. *
  3195. * Example:
  3196. * var p = Popcorn('#video')
  3197. * .footnote({
  3198. * start: 5, // seconds
  3199. * end: 15, // seconds
  3200. * text: 'This video made exclusively for drumbeat.org',
  3201. * target: 'footnotediv'
  3202. * });
  3203. **/
  3204. Popcorn.plugin( "footnote", {
  3205. manifest: {
  3206. about: {
  3207. name: "Popcorn Footnote Plugin",
  3208. version: "0.2",
  3209. author: "@annasob, @rwaldron",
  3210. website: "annasob.wordpress.com"
  3211. },
  3212. options: {
  3213. start: {
  3214. elem: "input",
  3215. type: "number",
  3216. label: "Start"
  3217. },
  3218. end: {
  3219. elem: "input",
  3220. type: "number",
  3221. label: "End"
  3222. },
  3223. text: {
  3224. elem: "input",
  3225. type: "text",
  3226. label: "Text"
  3227. },
  3228. target: "footnote-container"
  3229. }
  3230. },
  3231. _setup: function( options ) {
  3232. var target = Popcorn.dom.find( options.target );
  3233. options._container = document.createElement( "div" );
  3234. options._container.style.display = "none";
  3235. options._container.innerHTML = options.text;
  3236. target.appendChild( options._container );
  3237. },
  3238. /**
  3239. * @member footnote
  3240. * The start function will be executed when the currentTime
  3241. * of the video reaches the start time provided by the
  3242. * options variable
  3243. */
  3244. start: function( event, options ){
  3245. options._container.style.display = "inline";
  3246. },
  3247. /**
  3248. * @member footnote
  3249. * The end function will be executed when the currentTime
  3250. * of the video reaches the end time provided by the
  3251. * options variable
  3252. */
  3253. end: function( event, options ){
  3254. options._container.style.display = "none";
  3255. },
  3256. _teardown: function( options ) {
  3257. var target = Popcorn.dom.find( options.target );
  3258. if ( target ) {
  3259. target.removeChild( options._container );
  3260. }
  3261. }
  3262. });
  3263. })( Popcorn );
  3264. // PLUGIN: Text
  3265. (function ( Popcorn ) {
  3266. /**
  3267. * Text Popcorn plug-in
  3268. *
  3269. * Places text in an element on the page. Plugin options include:
  3270. * Options parameter will need a start, end.
  3271. * Start: Is the time that you want this plug-in to execute
  3272. * End: Is the time that you want this plug-in to stop executing
  3273. * Text: Is the text that you want to appear in the target
  3274. * Escape: {true|false} Whether to escape the text (e.g., html strings)
  3275. * Multiline: {true|false} Whether newlines should be turned into <br>s
  3276. * Target: Is the ID of the element where the text should be placed. An empty target
  3277. * will be placed on top of the media element
  3278. *
  3279. * @param {Object} options
  3280. *
  3281. * Example:
  3282. * var p = Popcorn('#video')
  3283. *
  3284. * // Simple text
  3285. * .text({
  3286. * start: 5, // seconds
  3287. * end: 15, // seconds
  3288. * text: 'This video made exclusively for drumbeat.org',
  3289. * target: 'textdiv'
  3290. * })
  3291. *
  3292. * // HTML text, rendered as HTML
  3293. * .text({
  3294. * start: 15, // seconds
  3295. * end: 20, // seconds
  3296. * text: '<p>This video made <em>exclusively</em> for drumbeat.org</p>',
  3297. * target: 'textdiv'
  3298. * })
  3299. *
  3300. * // HTML text, escaped and rendered as plain text
  3301. * .text({
  3302. * start: 20, // seconds
  3303. * end: 25, // seconds
  3304. * text: 'This is an HTML p element: <p>paragraph</p>',
  3305. * escape: true,
  3306. * target: 'textdiv'
  3307. * })
  3308. *
  3309. * // Multi-Line HTML text, escaped and rendered as plain text
  3310. * .text({
  3311. * start: 25, // seconds
  3312. * end: 30, // seconds
  3313. * text: 'This is an HTML p element: <p>paragraph</p>\nThis is an HTML b element: <b>bold</b>',
  3314. * escape: true,
  3315. * multiline: true,
  3316. * target: 'textdiv'
  3317. * });
  3318. *
  3319. * // Subtitle text
  3320. * .text({
  3321. * start: 30, // seconds
  3322. * end: 40, // seconds
  3323. * text: 'This will be overlayed on the video',
  3324. * })
  3325. **/
  3326. /**
  3327. * HTML escape code from mustache.js, used under MIT Licence
  3328. * https://github.com/janl/mustache.js/blob/master/mustache.js
  3329. **/
  3330. var escapeMap = {
  3331. "&": "&amp;",
  3332. "<": "&lt;",
  3333. ">": "&gt;",
  3334. '"': '&quot;',
  3335. "'": '&#39;'
  3336. };
  3337. function escapeHTML( string, multiline ) {
  3338. return String( string ).replace( /&(?!\w+;)|[<>"']/g, function ( s ) {
  3339. return escapeMap[ s ] || s;
  3340. });
  3341. }
  3342. function newlineToBreak( string ) {
  3343. // Deal with both \r\n and \n
  3344. return string.replace( /\r?\n/gm, "<br>" );
  3345. }
  3346. // Subtitle specific functionality
  3347. function createSubtitleContainer( context, id ) {
  3348. var ctxContainer = context.container = document.createElement( "div" ),
  3349. style = ctxContainer.style,
  3350. media = context.media;
  3351. var updatePosition = function() {
  3352. var position = context.position();
  3353. // the video element must have height and width defined
  3354. style.fontSize = "18px";
  3355. style.width = media.offsetWidth + "px";
  3356. style.top = position.top + media.offsetHeight - ctxContainer.offsetHeight - 40 + "px";
  3357. style.left = position.left + "px";
  3358. setTimeout( updatePosition, 10 );
  3359. };
  3360. ctxContainer.id = id || "";
  3361. style.position = "absolute";
  3362. style.color = "white";
  3363. style.textShadow = "black 2px 2px 6px";
  3364. style.fontWeight = "bold";
  3365. style.textAlign = "center";
  3366. updatePosition();
  3367. context.media.parentNode.appendChild( ctxContainer );
  3368. return ctxContainer;
  3369. }
  3370. Popcorn.plugin( "text", {
  3371. manifest: {
  3372. about: {
  3373. name: "Popcorn Text Plugin",
  3374. version: "0.1",
  3375. author: "@humphd"
  3376. },
  3377. options: {
  3378. start: {
  3379. elem: "input",
  3380. type: "number",
  3381. label: "Start"
  3382. },
  3383. end: {
  3384. elem: "input",
  3385. type: "number",
  3386. label: "End"
  3387. },
  3388. text: {
  3389. elem: "input",
  3390. type: "text",
  3391. label: "Text",
  3392. "default": "Popcorn.js"
  3393. },
  3394. escape: {
  3395. elem: "input",
  3396. type: "checkbox",
  3397. label: "Escape"
  3398. },
  3399. multiline: {
  3400. elem: "input",
  3401. type: "checkbox",
  3402. label: "Multiline"
  3403. }
  3404. }
  3405. },
  3406. _setup: function( options ) {
  3407. var target,
  3408. text,
  3409. container = options._container = document.createElement( "div" );
  3410. container.style.display = "none";
  3411. if ( options.target ) {
  3412. // Try to use supplied target
  3413. target = Popcorn.dom.find( options.target );
  3414. if ( !target ) {
  3415. target = createSubtitleContainer( this, options.target );
  3416. }
  3417. else if ( [ "VIDEO", "AUDIO" ].indexOf( target.nodeName ) > -1 ) {
  3418. target = createSubtitleContainer( this, options.target + "-overlay" );
  3419. }
  3420. } else if ( !this.container ) {
  3421. // Creates a div for all subtitles to use
  3422. target = createSubtitleContainer( this );
  3423. } else {
  3424. // Use subtitle container
  3425. target = this.container;
  3426. }
  3427. // cache reference to actual target container
  3428. options._target = target;
  3429. // Escape HTML text if requested
  3430. text = !!options.escape ? escapeHTML( options.text ) :
  3431. options.text;
  3432. // Swap newline for <br> if requested
  3433. text = !!options.multiline ? newlineToBreak ( text ) : text;
  3434. container.innerHTML = text || "";
  3435. target.appendChild( container );
  3436. },
  3437. /**
  3438. * @member text
  3439. * The start function will be executed when the currentTime
  3440. * of the video reaches the start time provided by the
  3441. * options variable
  3442. */
  3443. start: function( event, options ) {
  3444. options._container.style.display = "inline";
  3445. },
  3446. /**
  3447. * @member text
  3448. * The end function will be executed when the currentTime
  3449. * of the video reaches the end time provided by the
  3450. * options variable
  3451. */
  3452. end: function( event, options ) {
  3453. options._container.style.display = "none";
  3454. },
  3455. _teardown: function( options ) {
  3456. var target = options._target;
  3457. if ( target ) {
  3458. target.removeChild( options._container );
  3459. }
  3460. }
  3461. });
  3462. })( Popcorn );
  3463. // PLUGIN: Google Maps
  3464. var googleCallback;
  3465. (function ( Popcorn ) {
  3466. var i = 1,
  3467. _mapFired = false,
  3468. _mapLoaded = false,
  3469. geocoder, loadMaps;
  3470. //google api callback
  3471. googleCallback = function ( data ) {
  3472. // ensure all of the maps functions needed are loaded
  3473. // before setting _maploaded to true
  3474. if ( typeof google !== "undefined" && google.maps && google.maps.Geocoder && google.maps.LatLng ) {
  3475. geocoder = new google.maps.Geocoder();
  3476. Popcorn.getScript( "//maps.stamen.com/js/tile.stamen.js", function() {
  3477. _mapLoaded = true;
  3478. });
  3479. } else {
  3480. setTimeout(function () {
  3481. googleCallback( data );
  3482. }, 1);
  3483. }
  3484. };
  3485. // function that loads the google api
  3486. loadMaps = function () {
  3487. // for some reason the Google Map API adds content to the body
  3488. if ( document.body ) {
  3489. _mapFired = true;
  3490. Popcorn.getScript( "//maps.google.com/maps/api/js?sensor=false&callback=googleCallback" );
  3491. } else {
  3492. setTimeout(function () {
  3493. loadMaps();
  3494. }, 1);
  3495. }
  3496. };
  3497. function buildMap( options, location, mapDiv ) {
  3498. var type = options.type ? options.type.toUpperCase() : "HYBRID",
  3499. layer;
  3500. // See if we need to make a custom Stamen map layer
  3501. if ( type === "STAMEN-WATERCOLOR" ||
  3502. type === "STAMEN-TERRAIN" ||
  3503. type === "STAMEN-TONER" ) {
  3504. // Stamen types are lowercase
  3505. layer = type.replace("STAMEN-", "").toLowerCase();
  3506. }
  3507. var map = new google.maps.Map( mapDiv, {
  3508. // If a custom layer was specified, use that, otherwise use type
  3509. mapTypeId: layer ? layer : google.maps.MapTypeId[ type ],
  3510. // Hide the layer selection UI
  3511. mapTypeControlOptions: { mapTypeIds: [] }
  3512. });
  3513. if ( layer ) {
  3514. map.mapTypes.set( layer, new google.maps.StamenMapType( layer ));
  3515. }
  3516. map.getDiv().style.display = "none";
  3517. return map;
  3518. }
  3519. /**
  3520. * googlemap popcorn plug-in
  3521. * Adds a map to the target div centered on the location specified by the user
  3522. * Options parameter will need a start, end, target, type, zoom, lat and lng, and location
  3523. * -Start is the time that you want this plug-in to execute
  3524. * -End is the time that you want this plug-in to stop executing
  3525. * -Target is the id of the DOM element that you want the map to appear in. This element must be in the DOM
  3526. * -Type [optional] either: HYBRID (default), ROADMAP, SATELLITE, TERRAIN, STREETVIEW, or one of the
  3527. * Stamen custom map types (http://http://maps.stamen.com): STAMEN-TONER,
  3528. * STAMEN-WATERCOLOR, or STAMEN-TERRAIN.
  3529. * -Zoom [optional] defaults to 0
  3530. * -Heading [optional] STREETVIEW orientation of camera in degrees relative to true north (0 north, 90 true east, ect)
  3531. * -Pitch [optional] STREETVIEW vertical orientation of the camera (between 1 and 3 is recommended)
  3532. * -Lat and Lng: the coordinates of the map must be present if location is not specified.
  3533. * -Height [optional] the height of the map, in "px" or "%". Defaults to "100%".
  3534. * -Width [optional] the width of the map, in "px" or "%". Defaults to "100%".
  3535. * -Location: the adress you want the map to display, must be present if lat and lng are not specified.
  3536. * Note: using location requires extra loading time, also not specifying both lat/lng and location will
  3537. * cause and error.
  3538. *
  3539. * Tweening works using the following specifications:
  3540. * -location is the start point when using an auto generated route
  3541. * -tween when used in this context is a string which specifies the end location for your route
  3542. * Note that both location and tween must be present when using an auto generated route, or the map will not tween
  3543. * -interval is the speed in which the tween will be executed, a reasonable time is 1000 ( time in milliseconds )
  3544. * Heading, Zoom, and Pitch streetview values are also used in tweening with the autogenerated route
  3545. *
  3546. * -tween is an array of objects, each containing data for one frame of a tween
  3547. * -position is an object with has two paramaters, lat and lng, both which are mandatory for a tween to work
  3548. * -pov is an object which houses heading, pitch, and zoom paramters, which are all optional, if undefined, these values default to 0
  3549. * -interval is the speed in which the tween will be executed, a reasonable time is 1000 ( time in milliseconds )
  3550. *
  3551. * @param {Object} options
  3552. *
  3553. * Example:
  3554. var p = Popcorn("#video")
  3555. .googlemap({
  3556. start: 5, // seconds
  3557. end: 15, // seconds
  3558. type: "ROADMAP",
  3559. target: "map",
  3560. lat: 43.665429,
  3561. lng: -79.403323
  3562. } )
  3563. *
  3564. */
  3565. Popcorn.plugin( "googlemap", function ( options ) {
  3566. var newdiv, map, location,
  3567. target = document.getElementById( options.target );
  3568. options.type = options.type || "ROADMAP";
  3569. options.zoom = options.zoom || 1;
  3570. options.lat = options.lat || 0;
  3571. options.lng = options.lng || 0;
  3572. // if this is the firest time running the plugins
  3573. // call the function that gets the sctipt
  3574. if ( !_mapFired ) {
  3575. loadMaps();
  3576. }
  3577. // create a new div this way anything in the target div is left intact
  3578. // this is later passed on to the maps api
  3579. newdiv = document.createElement( "div" );
  3580. newdiv.id = "actualmap" + i;
  3581. newdiv.style.width = options.width || "100%";
  3582. // height is a little more complicated than width.
  3583. if ( options.height ) {
  3584. newdiv.style.height = options.height;
  3585. } else if ( target && target.clientHeight ) {
  3586. newdiv.style.height = target.clientHeight + "px";
  3587. } else {
  3588. newdiv.style.height = "100%";
  3589. }
  3590. i++;
  3591. target && target.appendChild( newdiv );
  3592. // ensure that google maps and its functions are loaded
  3593. // before setting up the map parameters
  3594. var isMapReady = function () {
  3595. if ( _mapLoaded ) {
  3596. if ( newdiv ) {
  3597. if ( options.location ) {
  3598. // calls an anonymous google function called on separate thread
  3599. geocoder.geocode({
  3600. "address": options.location
  3601. }, function ( results, status ) {
  3602. // second check for newdiv since it could have disappeared before
  3603. // this callback is actual run
  3604. if ( newdiv && status === google.maps.GeocoderStatus.OK ) {
  3605. options.lat = results[ 0 ].geometry.location.lat();
  3606. options.lng = results[ 0 ].geometry.location.lng();
  3607. location = new google.maps.LatLng( options.lat, options.lng );
  3608. map = buildMap( options, location, newdiv );
  3609. }
  3610. });
  3611. } else {
  3612. location = new google.maps.LatLng( options.lat, options.lng );
  3613. map = buildMap( options, location, newdiv );
  3614. }
  3615. }
  3616. } else {
  3617. setTimeout(function () {
  3618. isMapReady();
  3619. }, 5);
  3620. }
  3621. };
  3622. isMapReady();
  3623. return {
  3624. /**
  3625. * @member webpage
  3626. * The start function will be executed when the currentTime
  3627. * of the video reaches the start time provided by the
  3628. * options variable
  3629. */
  3630. start: function( event, options ) {
  3631. var that = this,
  3632. sView;
  3633. // ensure the map has been initialized in the setup function above
  3634. var isMapSetup = function() {
  3635. if ( map ) {
  3636. options._map = map;
  3637. map.getDiv().style.display = "block";
  3638. // reset the location and zoom just in case the user plaid with the map
  3639. google.maps.event.trigger( map, "resize" );
  3640. map.setCenter( location );
  3641. // make sure options.zoom is a number
  3642. if ( options.zoom && typeof options.zoom !== "number" ) {
  3643. options.zoom = +options.zoom;
  3644. }
  3645. map.setZoom( options.zoom );
  3646. //Make sure heading is a number
  3647. if ( options.heading && typeof options.heading !== "number" ) {
  3648. options.heading = +options.heading;
  3649. }
  3650. //Make sure pitch is a number
  3651. if ( options.pitch && typeof options.pitch !== "number" ) {
  3652. options.pitch = +options.pitch;
  3653. }
  3654. if ( options.type === "STREETVIEW" ) {
  3655. // Switch this map into streeview mode
  3656. map.setStreetView(
  3657. // Pass a new StreetViewPanorama instance into our map
  3658. sView = new google.maps.StreetViewPanorama( newdiv, {
  3659. position: location,
  3660. pov: {
  3661. heading: options.heading = options.heading || 0,
  3662. pitch: options.pitch = options.pitch || 0,
  3663. zoom: options.zoom
  3664. }
  3665. })
  3666. );
  3667. // Function to handle tweening using a set timeout
  3668. var tween = function( rM, t ) {
  3669. var computeHeading = google.maps.geometry.spherical.computeHeading;
  3670. setTimeout(function() {
  3671. var current_time = that.media.currentTime;
  3672. // Checks whether this is a generated route or not
  3673. if ( typeof options.tween === "object" ) {
  3674. for ( var i = 0, m = rM.length; i < m; i++ ) {
  3675. var waypoint = rM[ i ];
  3676. // Checks if this position along the tween should be displayed or not
  3677. if ( current_time >= ( waypoint.interval * ( i + 1 ) ) / 1000 &&
  3678. ( current_time <= ( waypoint.interval * ( i + 2 ) ) / 1000 ||
  3679. current_time >= waypoint.interval * ( m ) / 1000 ) ) {
  3680. sView3.setPosition( new google.maps.LatLng( waypoint.position.lat, waypoint.position.lng ) );
  3681. sView3.setPov({
  3682. heading: waypoint.pov.heading || computeHeading( waypoint, rM[ i + 1 ] ) || 0,
  3683. zoom: waypoint.pov.zoom || 0,
  3684. pitch: waypoint.pov.pitch || 0
  3685. });
  3686. }
  3687. }
  3688. // Calls the tween function again at the interval set by the user
  3689. tween( rM, rM[ 0 ].interval );
  3690. } else {
  3691. for ( var k = 0, l = rM.length; k < l; k++ ) {
  3692. var interval = options.interval;
  3693. if( current_time >= (interval * ( k + 1 ) ) / 1000 &&
  3694. ( current_time <= (interval * ( k + 2 ) ) / 1000 ||
  3695. current_time >= interval * ( l ) / 1000 ) ) {
  3696. sView2.setPov({
  3697. heading: computeHeading( rM[ k ], rM[ k + 1 ] ) || 0,
  3698. zoom: options.zoom,
  3699. pitch: options.pitch || 0
  3700. });
  3701. sView2.setPosition( checkpoints[ k ] );
  3702. }
  3703. }
  3704. tween( checkpoints, options.interval );
  3705. }
  3706. }, t );
  3707. };
  3708. // Determines if we should use hardcoded values ( using options.tween ),
  3709. // or if we should use a start and end location and let google generate
  3710. // the route for us
  3711. if ( options.location && typeof options.tween === "string" ) {
  3712. // Creating another variable to hold the streetview map for tweening,
  3713. // Doing this because if there was more then one streetview map, the tweening would sometimes appear in other maps
  3714. var sView2 = sView;
  3715. // Create an array to store all the lat/lang values along our route
  3716. var checkpoints = [];
  3717. // Creates a new direction service, later used to create a route
  3718. var directionsService = new google.maps.DirectionsService();
  3719. // Creates a new direction renderer using the current map
  3720. // This enables us to access all of the route data that is returned to us
  3721. var directionsDisplay = new google.maps.DirectionsRenderer( sView2 );
  3722. var request = {
  3723. origin: options.location,
  3724. destination: options.tween,
  3725. travelMode: google.maps.TravelMode.DRIVING
  3726. };
  3727. // Create the route using the direction service and renderer
  3728. directionsService.route( request, function( response, status ) {
  3729. if ( status == google.maps.DirectionsStatus.OK ) {
  3730. directionsDisplay.setDirections( response );
  3731. showSteps( response, that );
  3732. }
  3733. });
  3734. var showSteps = function ( directionResult, that ) {
  3735. // Push new google map lat and lng values into an array from our list of lat and lng values
  3736. var routes = directionResult.routes[ 0 ].overview_path;
  3737. for ( var j = 0, k = routes.length; j < k; j++ ) {
  3738. checkpoints.push( new google.maps.LatLng( routes[ j ].lat(), routes[ j ].lng() ) );
  3739. }
  3740. // Check to make sure the interval exists, if not, set to a default of 1000
  3741. options.interval = options.interval || 1000;
  3742. tween( checkpoints, 10 );
  3743. };
  3744. } else if ( typeof options.tween === "object" ) {
  3745. // Same as the above to stop streetview maps from overflowing into one another
  3746. var sView3 = sView;
  3747. for ( var i = 0, l = options.tween.length; i < l; i++ ) {
  3748. // Make sure interval exists, if not, set to 1000
  3749. options.tween[ i ].interval = options.tween[ i ].interval || 1000;
  3750. tween( options.tween, 10 );
  3751. }
  3752. }
  3753. }
  3754. if ( options.onmaploaded ) {
  3755. options.onmaploaded( options, map );
  3756. }
  3757. } else {
  3758. setTimeout(function () {
  3759. isMapSetup();
  3760. }, 13);
  3761. }
  3762. };
  3763. isMapSetup();
  3764. },
  3765. /**
  3766. * @member webpage
  3767. * The end function will be executed when the currentTime
  3768. * of the video reaches the end time provided by the
  3769. * options variable
  3770. */
  3771. end: function ( event, options ) {
  3772. // if the map exists hide it do not delete the map just in
  3773. // case the user seeks back to time b/w start and end
  3774. if ( map ) {
  3775. map.getDiv().style.display = "none";
  3776. }
  3777. },
  3778. _teardown: function ( options ) {
  3779. var target = document.getElementById( options.target );
  3780. // the map must be manually removed
  3781. target && target.removeChild( newdiv );
  3782. newdiv = map = location = null;
  3783. options._map = null;
  3784. }
  3785. };
  3786. }, {
  3787. about: {
  3788. name: "Popcorn Google Map Plugin",
  3789. version: "0.1",
  3790. author: "@annasob",
  3791. website: "annasob.wordpress.com"
  3792. },
  3793. options: {
  3794. start: {
  3795. elem: "input",
  3796. type: "start",
  3797. label: "Start"
  3798. },
  3799. end: {
  3800. elem: "input",
  3801. type: "start",
  3802. label: "End"
  3803. },
  3804. target: "map-container",
  3805. type: {
  3806. elem: "select",
  3807. options: [ "ROADMAP", "SATELLITE", "STREETVIEW", "HYBRID", "TERRAIN", "STAMEN-WATERCOLOR", "STAMEN-TERRAIN", "STAMEN-TONER" ],
  3808. label: "Map Type",
  3809. optional: true
  3810. },
  3811. zoom: {
  3812. elem: "input",
  3813. type: "text",
  3814. label: "Zoom",
  3815. "default": 0,
  3816. optional: true
  3817. },
  3818. lat: {
  3819. elem: "input",
  3820. type: "text",
  3821. label: "Lat",
  3822. optional: true
  3823. },
  3824. lng: {
  3825. elem: "input",
  3826. type: "text",
  3827. label: "Lng",
  3828. optional: true
  3829. },
  3830. location: {
  3831. elem: "input",
  3832. type: "text",
  3833. label: "Location",
  3834. "default": "Toronto, Ontario, Canada"
  3835. },
  3836. heading: {
  3837. elem: "input",
  3838. type: "text",
  3839. label: "Heading",
  3840. "default": 0,
  3841. optional: true
  3842. },
  3843. pitch: {
  3844. elem: "input",
  3845. type: "text",
  3846. label: "Pitch",
  3847. "default": 1,
  3848. optional: true
  3849. }
  3850. }
  3851. });
  3852. })( Popcorn );
  3853. // PLUGIN: IMAGE
  3854. (function ( Popcorn ) {
  3855. /**
  3856. * Images popcorn plug-in
  3857. * Shows an image element
  3858. * Options parameter will need a start, end, href, target and src.
  3859. * Start is the time that you want this plug-in to execute
  3860. * End is the time that you want this plug-in to stop executing
  3861. * href is the url of the destination of a anchor - optional
  3862. * Target is the id of the document element that the iframe needs to be attached to,
  3863. * this target element must exist on the DOM
  3864. * Src is the url of the image that you want to display
  3865. * text is the overlayed text on the image - optional
  3866. *
  3867. * @param {Object} options
  3868. *
  3869. * Example:
  3870. var p = Popcorn('#video')
  3871. .image({
  3872. start: 5, // seconds
  3873. end: 15, // seconds
  3874. href: 'http://www.drumbeat.org/',
  3875. src: 'http://www.drumbeat.org/sites/default/files/domain-2/drumbeat_logo.png',
  3876. text: 'DRUMBEAT',
  3877. target: 'imagediv'
  3878. } )
  3879. *
  3880. */
  3881. var VIDEO_OVERLAY_Z = 2000,
  3882. CHECK_INTERVAL_DURATION = 10;
  3883. function trackMediaElement( mediaElement ) {
  3884. var checkInterval = -1,
  3885. container = document.createElement( "div" ),
  3886. videoZ = getComputedStyle( mediaElement ).zIndex;
  3887. container.setAttribute( "data-popcorn-helper-container", true );
  3888. container.style.position = "absolute";
  3889. if ( !isNaN( videoZ ) ) {
  3890. container.style.zIndex = videoZ + 1;
  3891. }
  3892. else {
  3893. container.style.zIndex = VIDEO_OVERLAY_Z;
  3894. }
  3895. document.body.appendChild( container );
  3896. function check() {
  3897. var mediaRect = mediaElement.getBoundingClientRect(),
  3898. containerRect = container.getBoundingClientRect();
  3899. if ( containerRect.left !== mediaRect.left ) {
  3900. container.style.left = mediaRect.left + "px";
  3901. }
  3902. if ( containerRect.top !== mediaRect.top ) {
  3903. container.style.top = mediaRect.top + "px";
  3904. }
  3905. }
  3906. return {
  3907. element: container,
  3908. start: function() {
  3909. checkInterval = setInterval( check, CHECK_INTERVAL_DURATION );
  3910. },
  3911. stop: function() {
  3912. clearInterval( checkInterval );
  3913. checkInterval = -1;
  3914. },
  3915. destroy: function() {
  3916. document.body.removeChild( container );
  3917. if ( checkInterval !== -1 ) {
  3918. clearInterval( checkInterval );
  3919. }
  3920. }
  3921. };
  3922. }
  3923. Popcorn.plugin( "image", {
  3924. manifest: {
  3925. about: {
  3926. name: "Popcorn image Plugin",
  3927. version: "0.1",
  3928. author: "Scott Downe",
  3929. website: "http://scottdowne.wordpress.com/"
  3930. },
  3931. options: {
  3932. start: {
  3933. elem: "input",
  3934. type: "number",
  3935. label: "Start"
  3936. },
  3937. end: {
  3938. elem: "input",
  3939. type: "number",
  3940. label: "End"
  3941. },
  3942. src: {
  3943. elem: "input",
  3944. type: "url",
  3945. label: "Image URL",
  3946. "default": "http://mozillapopcorn.org/wp-content/themes/popcorn/images/for_developers.png"
  3947. },
  3948. href: {
  3949. elem: "input",
  3950. type: "url",
  3951. label: "Link",
  3952. "default": "http://mozillapopcorn.org/wp-content/themes/popcorn/images/for_developers.png",
  3953. optional: true
  3954. },
  3955. target: "image-container",
  3956. text: {
  3957. elem: "input",
  3958. type: "text",
  3959. label: "Caption",
  3960. "default": "Popcorn.js",
  3961. optional: true
  3962. }
  3963. }
  3964. },
  3965. _setup: function( options ) {
  3966. var img = document.createElement( "img" ),
  3967. target = document.getElementById( options.target );
  3968. options.anchor = document.createElement( "a" );
  3969. options.anchor.style.position = "relative";
  3970. options.anchor.style.textDecoration = "none";
  3971. options.anchor.style.display = "none";
  3972. // add the widget's div to the target div.
  3973. // if target is <video> or <audio>, create a container and routinely
  3974. // update its size/position to be that of the media
  3975. if ( target ) {
  3976. if ( [ "VIDEO", "AUDIO" ].indexOf( target.nodeName ) > -1 ) {
  3977. options.trackedContainer = trackMediaElement( target );
  3978. options.trackedContainer.element.appendChild( options.anchor );
  3979. }
  3980. else {
  3981. target && target.appendChild( options.anchor );
  3982. }
  3983. }
  3984. img.addEventListener( "load", function() {
  3985. // borders look really bad, if someone wants it they can put it on their div target
  3986. img.style.borderStyle = "none";
  3987. options.anchor.href = options.href || options.src || "#";
  3988. options.anchor.target = "_blank";
  3989. var fontHeight, divText;
  3990. img.style.height = target.style.height;
  3991. img.style.width = target.style.width;
  3992. options.anchor.appendChild( img );
  3993. // If display text was provided, display it:
  3994. if ( options.text ) {
  3995. fontHeight = ( img.height / 12 ) + "px";
  3996. divText = document.createElement( "div" );
  3997. Popcorn.extend( divText.style, {
  3998. color: "black",
  3999. fontSize: fontHeight,
  4000. fontWeight: "bold",
  4001. position: "relative",
  4002. textAlign: "center",
  4003. width: img.style.width || img.width + "px",
  4004. zIndex: "10"
  4005. });
  4006. divText.innerHTML = options.text || "";
  4007. divText.style.top = ( ( img.style.height.replace( "px", "" ) || img.height ) / 2 ) - ( divText.offsetHeight / 2 ) + "px";
  4008. options.anchor.insertBefore( divText, img );
  4009. }
  4010. }, false );
  4011. img.src = options.src;
  4012. },
  4013. /**
  4014. * @member image
  4015. * The start function will be executed when the currentTime
  4016. * of the video reaches the start time provided by the
  4017. * options variable
  4018. */
  4019. start: function( event, options ) {
  4020. options.anchor.style.display = "inline";
  4021. if ( options.trackedContainer ) {
  4022. options.trackedContainer.start();
  4023. }
  4024. },
  4025. /**
  4026. * @member image
  4027. * The end function will be executed when the currentTime
  4028. * of the video reaches the end time provided by the
  4029. * options variable
  4030. */
  4031. end: function( event, options ) {
  4032. options.anchor.style.display = "none";
  4033. if ( options.trackedContainer ) {
  4034. options.trackedContainer.stop();
  4035. }
  4036. },
  4037. _teardown: function( options ) {
  4038. if ( options.trackedContainer ) {
  4039. options.trackedContainer.destroy();
  4040. }
  4041. else if ( options.anchor.parentNode ) {
  4042. options.anchor.parentNode.removeChild( options.anchor );
  4043. }
  4044. }
  4045. });
  4046. })( Popcorn );
  4047. // PLUGIN: Google Feed
  4048. (function ( Popcorn ) {
  4049. var i = 1,
  4050. scriptLoaded = false;
  4051. /**
  4052. * googlefeed popcorn plug-in
  4053. * Adds a feed from the specified blog url at the target div
  4054. * Options parameter will need a start, end, target, url and title
  4055. * -Start is the time that you want this plug-in to execute
  4056. * -End is the time that you want this plug-in to stop executing
  4057. * -Target is the id of the DOM element that you want the map to appear in. This element must be in the DOM
  4058. * -Url is the url of the blog's feed you are trying to access
  4059. * -Title is the title of the blog you want displayed above the feed
  4060. * -Orientation is the orientation of the blog, accepts either Horizontal or Vertical, defaults to Vertical
  4061. * @param {Object} options
  4062. *
  4063. * Example:
  4064. var p = Popcorn("#video")
  4065. .googlefeed({
  4066. start: 5, // seconds
  4067. end: 15, // seconds
  4068. target: "map",
  4069. url: "http://zenit.senecac.on.ca/~chris.tyler/planet/rss20.xml",
  4070. title: "Planet Feed"
  4071. } )
  4072. *
  4073. */
  4074. Popcorn.plugin( "googlefeed", function( options ) {
  4075. var dynamicFeedLoad = function() {
  4076. var dontLoad = false,
  4077. k = 0,
  4078. links = document.getElementsByTagName( "link" ),
  4079. len = links.length,
  4080. head = document.head || document.getElementsByTagName( "head" )[ 0 ],
  4081. css = document.createElement( "link" ),
  4082. resource = "//www.google.com/uds/solutions/dynamicfeed/gfdynamicfeedcontrol.";
  4083. if ( !window.GFdynamicFeedControl ) {
  4084. Popcorn.getScript( resource + "js", function() {
  4085. scriptLoaded = true;
  4086. });
  4087. } else {
  4088. scriptLoaded = true;
  4089. }
  4090. // Checking if the css file is already included
  4091. for ( ; k < len; k++ ){
  4092. if ( links[ k ].href === resource + "css" ) {
  4093. dontLoad = true;
  4094. }
  4095. }
  4096. if ( !dontLoad ) {
  4097. css.type = "text/css";
  4098. css.rel = "stylesheet";
  4099. css.href = resource + "css";
  4100. head.insertBefore( css, head.firstChild );
  4101. }
  4102. };
  4103. if ( !window.google ) {
  4104. Popcorn.getScript( "//www.google.com/jsapi", function() {
  4105. google.load( "feeds", "1", {
  4106. callback: function () {
  4107. dynamicFeedLoad();
  4108. }
  4109. });
  4110. });
  4111. } else {
  4112. dynamicFeedLoad();
  4113. }
  4114. // create a new div and append it to the parent div so nothing
  4115. // that already exists in the parent div gets overwritten
  4116. var newdiv = document.createElement( "div" ),
  4117. target = document.getElementById( options.target ),
  4118. initialize = function() {
  4119. //ensure that the script has been loaded
  4120. if ( !scriptLoaded ) {
  4121. setTimeout( function () {
  4122. initialize();
  4123. }, 5 );
  4124. } else {
  4125. // Create the feed control using the user entered url and title
  4126. options.feed = new GFdynamicFeedControl( options.url, newdiv, {
  4127. vertical: options.orientation.toLowerCase() === "vertical" ? true : false,
  4128. horizontal: options.orientation.toLowerCase() === "horizontal" ? true : false,
  4129. title: options.title = options.title || "Blog"
  4130. });
  4131. }
  4132. };
  4133. // Default to vertical orientation if empty or incorrect input
  4134. if( !options.orientation || ( options.orientation.toLowerCase() !== "vertical" &&
  4135. options.orientation.toLowerCase() !== "horizontal" ) ) {
  4136. options.orientation = "vertical";
  4137. }
  4138. newdiv.style.display = "none";
  4139. newdiv.id = "_feed" + i;
  4140. newdiv.style.width = "100%";
  4141. newdiv.style.height = "100%";
  4142. i++;
  4143. target && target.appendChild( newdiv );
  4144. initialize();
  4145. return {
  4146. /**
  4147. * @member webpage
  4148. * The start function will be executed when the currentTime
  4149. * of the video reaches the start time provided by the
  4150. * options variable
  4151. */
  4152. start: function( event, options ){
  4153. newdiv.setAttribute( "style", "display:inline" );
  4154. },
  4155. /**
  4156. * @member webpage
  4157. * The end function will be executed when the currentTime
  4158. * of the video reaches the end time provided by the
  4159. * options variable
  4160. */
  4161. end: function( event, options ){
  4162. newdiv.setAttribute( "style", "display:none" );
  4163. },
  4164. _teardown: function( options ) {
  4165. document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( newdiv );
  4166. delete options.feed;
  4167. }
  4168. };
  4169. },
  4170. {
  4171. about: {
  4172. name: "Popcorn Google Feed Plugin",
  4173. version: "0.1",
  4174. author: "David Seifried",
  4175. website: "dseifried.wordpress.com"
  4176. },
  4177. options: {
  4178. start: {
  4179. elem: "input",
  4180. type: "number",
  4181. label: "Start"
  4182. },
  4183. end: {
  4184. elem: "input",
  4185. type: "number",
  4186. label: "End"
  4187. },
  4188. target: "feed-container",
  4189. url: {
  4190. elem: "input",
  4191. type: "url",
  4192. label: "Feed URL",
  4193. "default": "http://planet.mozilla.org/rss20.xml"
  4194. },
  4195. title: {
  4196. elem: "input",
  4197. type: "text",
  4198. label: "Title",
  4199. "default": "Planet Mozilla",
  4200. optional: true
  4201. },
  4202. orientation: {
  4203. elem: "select",
  4204. options: [ "Vertical", "Horizontal" ],
  4205. label: "Orientation",
  4206. "default": "Vertical",
  4207. optional: true
  4208. }
  4209. }
  4210. });
  4211. })( Popcorn );
  4212. // PLUGIN: Subtitle
  4213. (function ( Popcorn ) {
  4214. var i = 0,
  4215. createDefaultContainer = function( context, id ) {
  4216. var ctxContainer = context.container = document.createElement( "div" ),
  4217. style = ctxContainer.style,
  4218. media = context.media;
  4219. var updatePosition = function() {
  4220. var position = context.position();
  4221. // the video element must have height and width defined
  4222. style.fontSize = "18px";
  4223. style.width = media.offsetWidth + "px";
  4224. style.top = position.top + media.offsetHeight - ctxContainer.offsetHeight - 40 + "px";
  4225. style.left = position.left + "px";
  4226. setTimeout( updatePosition, 10 );
  4227. };
  4228. ctxContainer.id = id || Popcorn.guid();
  4229. style.position = "absolute";
  4230. style.color = "white";
  4231. style.textShadow = "black 2px 2px 6px";
  4232. style.fontWeight = "bold";
  4233. style.textAlign = "center";
  4234. updatePosition();
  4235. context.media.parentNode.appendChild( ctxContainer );
  4236. return ctxContainer;
  4237. };
  4238. /**
  4239. * Subtitle popcorn plug-in
  4240. * Displays a subtitle over the video, or in the target div
  4241. * Options parameter will need a start, and end.
  4242. * Optional parameters are target and text.
  4243. * Start is the time that you want this plug-in to execute
  4244. * End is the time that you want this plug-in to stop executing
  4245. * Target is the id of the document element that the content is
  4246. * appended to, this target element must exist on the DOM
  4247. * Text is the text of the subtitle you want to display.
  4248. *
  4249. * @param {Object} options
  4250. *
  4251. * Example:
  4252. var p = Popcorn('#video')
  4253. .subtitle({
  4254. start: 5, // seconds, mandatory
  4255. end: 15, // seconds, mandatory
  4256. text: 'Hellow world', // optional
  4257. target: 'subtitlediv', // optional
  4258. } )
  4259. *
  4260. */
  4261. Popcorn.plugin( "subtitle" , {
  4262. manifest: {
  4263. about: {
  4264. name: "Popcorn Subtitle Plugin",
  4265. version: "0.1",
  4266. author: "Scott Downe",
  4267. website: "http://scottdowne.wordpress.com/"
  4268. },
  4269. options: {
  4270. start: {
  4271. elem: "input",
  4272. type: "text",
  4273. label: "Start"
  4274. },
  4275. end: {
  4276. elem: "input",
  4277. type: "text",
  4278. label: "End"
  4279. },
  4280. target: "subtitle-container",
  4281. text: {
  4282. elem: "input",
  4283. type: "text",
  4284. label: "Text"
  4285. }
  4286. }
  4287. },
  4288. _setup: function( options ) {
  4289. var newdiv = document.createElement( "div" );
  4290. newdiv.id = "subtitle-" + i++;
  4291. newdiv.style.display = "none";
  4292. // Creates a div for all subtitles to use
  4293. ( !this.container && ( !options.target || options.target === "subtitle-container" ) ) &&
  4294. createDefaultContainer( this );
  4295. // if a target is specified, use that
  4296. if ( options.target && options.target !== "subtitle-container" ) {
  4297. // In case the target doesn't exist in the DOM
  4298. options.container = document.getElementById( options.target ) || createDefaultContainer( this, options.target );
  4299. } else {
  4300. // use shared default container
  4301. options.container = this.container;
  4302. }
  4303. document.getElementById( options.container.id ) && document.getElementById( options.container.id ).appendChild( newdiv );
  4304. options.innerContainer = newdiv;
  4305. options.showSubtitle = function() {
  4306. options.innerContainer.innerHTML = options.text || "";
  4307. };
  4308. },
  4309. /**
  4310. * @member subtitle
  4311. * The start function will be executed when the currentTime
  4312. * of the video reaches the start time provided by the
  4313. * options variable
  4314. */
  4315. start: function( event, options ){
  4316. options.innerContainer.style.display = "inline";
  4317. options.showSubtitle( options, options.text );
  4318. },
  4319. /**
  4320. * @member subtitle
  4321. * The end function will be executed when the currentTime
  4322. * of the video reaches the end time provided by the
  4323. * options variable
  4324. */
  4325. end: function( event, options ) {
  4326. options.innerContainer.style.display = "none";
  4327. options.innerContainer.innerHTML = "";
  4328. },
  4329. _teardown: function ( options ) {
  4330. options.container.removeChild( options.innerContainer );
  4331. }
  4332. });
  4333. })( Popcorn );
  4334. // PLUGIN: TWITTER
  4335. (function ( Popcorn ) {
  4336. var scriptLoading = false;
  4337. /**
  4338. * Twitter popcorn plug-in
  4339. * Appends a Twitter widget to an element on the page.
  4340. * Options parameter will need a start, end, target and source.
  4341. * Optional parameters are height and width.
  4342. * Start is the time that you want this plug-in to execute
  4343. * End is the time that you want this plug-in to stop executing
  4344. * Src is the hash tag or twitter user to get tweets from
  4345. * Target is the id of the document element that the images are
  4346. * appended to, this target element must exist on the DOM
  4347. * Height is the height of the widget, defaults to 200
  4348. * Width is the width of the widget, defaults to 250
  4349. *
  4350. * @param {Object} options
  4351. *
  4352. * Example:
  4353. var p = Popcorn('#video')
  4354. .twitter({
  4355. start: 5, // seconds, mandatory
  4356. end: 15, // seconds, mandatory
  4357. src: '@stevesong', // mandatory, also accepts hash tags
  4358. height: 200, // optional
  4359. width: 250, // optional
  4360. target: 'twitterdiv' // mandatory
  4361. } )
  4362. *
  4363. */
  4364. Popcorn.plugin( "twitter" , {
  4365. manifest: {
  4366. about: {
  4367. name: "Popcorn Twitter Plugin",
  4368. version: "0.1",
  4369. author: "Scott Downe",
  4370. website: "http://scottdowne.wordpress.com/"
  4371. },
  4372. options:{
  4373. start: {
  4374. elem: "input",
  4375. type: "number",
  4376. label: "Start"
  4377. },
  4378. end: {
  4379. elem: "input",
  4380. type: "number",
  4381. label: "End"
  4382. },
  4383. src: {
  4384. elem: "input",
  4385. type: "text",
  4386. label: "Tweet Source (# or @)",
  4387. "default": "@popcornjs"
  4388. },
  4389. target: "twitter-container",
  4390. height: {
  4391. elem: "input",
  4392. type: "number",
  4393. label: "Height",
  4394. "default": "200",
  4395. optional: true
  4396. },
  4397. width: {
  4398. elem: "input",
  4399. type: "number",
  4400. label: "Width",
  4401. "default": "250",
  4402. optional: true
  4403. }
  4404. }
  4405. },
  4406. _setup: function( options ) {
  4407. if ( !window.TWTR && !scriptLoading ) {
  4408. scriptLoading = true;
  4409. Popcorn.getScript( "//widgets.twimg.com/j/2/widget.js" );
  4410. }
  4411. var target = document.getElementById( options.target );
  4412. // create the div to store the widget
  4413. // setup widget div that is unique per track
  4414. options.container = document.createElement( "div" );
  4415. // use this id to connect it to the widget
  4416. options.container.setAttribute( "id", Popcorn.guid() );
  4417. // display none by default
  4418. options.container.style.display = "none";
  4419. // add the widget's div to the target div
  4420. target && target.appendChild( options.container );
  4421. // setup info for the widget
  4422. var src = options.src || "",
  4423. width = options.width || 250,
  4424. height = options.height || 200,
  4425. profile = /^@/.test( src ),
  4426. widgetOptions = {
  4427. version: 2,
  4428. // use this id to connect it to the div
  4429. id: options.container.getAttribute( "id" ),
  4430. rpp: 30,
  4431. width: width,
  4432. height: height,
  4433. interval: 6000,
  4434. theme: {
  4435. shell: {
  4436. background: "#ffffff",
  4437. color: "#000000"
  4438. },
  4439. tweets: {
  4440. background: "#ffffff",
  4441. color: "#444444",
  4442. links: "#1985b5"
  4443. }
  4444. },
  4445. features: {
  4446. loop: true,
  4447. timestamp: true,
  4448. avatars: true,
  4449. hashtags: true,
  4450. toptweets: true,
  4451. live: true,
  4452. scrollbar: false,
  4453. behavior: 'default'
  4454. }
  4455. };
  4456. // create widget
  4457. var isReady = function( that ) {
  4458. if ( window.TWTR ) {
  4459. if ( profile ) {
  4460. widgetOptions.type = "profile";
  4461. new TWTR.Widget( widgetOptions ).render().setUser( src ).start();
  4462. } else {
  4463. widgetOptions.type = "search";
  4464. widgetOptions.search = src;
  4465. widgetOptions.subject = src;
  4466. new TWTR.Widget( widgetOptions ).render().start();
  4467. }
  4468. } else {
  4469. setTimeout( function() {
  4470. isReady( that );
  4471. }, 1);
  4472. }
  4473. };
  4474. isReady( this );
  4475. },
  4476. /**
  4477. * @member Twitter
  4478. * The start function will be executed when the currentTime
  4479. * of the video reaches the start time provided by the
  4480. * options variable
  4481. */
  4482. start: function( event, options ) {
  4483. options.container.style.display = "inline";
  4484. },
  4485. /**
  4486. * @member Twitter
  4487. * The end function will be executed when the currentTime
  4488. * of the video reaches the end time provided by the
  4489. * options variable
  4490. */
  4491. end: function( event, options ) {
  4492. options.container.style.display = "none";
  4493. },
  4494. _teardown: function( options ) {
  4495. document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( options.container );
  4496. }
  4497. });
  4498. })( Popcorn );
  4499. // PLUGIN: WEBPAGE
  4500. (function ( Popcorn ) {
  4501. /**
  4502. * Webpages popcorn plug-in
  4503. * Creates an iframe showing a website specified by the user
  4504. * Options parameter will need a start, end, id, target and src.
  4505. * Start is the time that you want this plug-in to execute
  4506. * End is the time that you want this plug-in to stop executing
  4507. * Id is the id that you want assigned to the iframe
  4508. * Target is the id of the document element that the iframe needs to be attached to,
  4509. * this target element must exist on the DOM
  4510. * Src is the url of the website that you want the iframe to display
  4511. *
  4512. * @param {Object} options
  4513. *
  4514. * Example:
  4515. var p = Popcorn('#video')
  4516. .webpage({
  4517. id: "webpages-a",
  4518. start: 5, // seconds
  4519. end: 15, // seconds
  4520. src: 'http://www.webmademovies.org',
  4521. target: 'webpagediv'
  4522. } )
  4523. *
  4524. */
  4525. Popcorn.plugin( "webpage" , {
  4526. manifest: {
  4527. about: {
  4528. name: "Popcorn Webpage Plugin",
  4529. version: "0.1",
  4530. author: "@annasob",
  4531. website: "annasob.wordpress.com"
  4532. },
  4533. options: {
  4534. id: {
  4535. elem: "input",
  4536. type: "text",
  4537. label: "Id",
  4538. optional: true
  4539. },
  4540. start: {
  4541. elem: "input",
  4542. type: "number",
  4543. label: "Start"
  4544. },
  4545. end: {
  4546. elem: "input",
  4547. type: "number",
  4548. label: "End"
  4549. },
  4550. src: {
  4551. elem: "input",
  4552. type: "url",
  4553. label: "Webpage URL",
  4554. "default": "http://mozillapopcorn.org"
  4555. },
  4556. target: "iframe-container"
  4557. }
  4558. },
  4559. _setup: function( options ) {
  4560. var target = document.getElementById( options.target );
  4561. // make src an iframe acceptable string
  4562. options.src = options.src.replace( /^(https?:)?(\/\/)?/, "//" );
  4563. // make an iframe
  4564. options._iframe = document.createElement( "iframe" );
  4565. options._iframe.setAttribute( "width", "100%" );
  4566. options._iframe.setAttribute( "height", "100%" );
  4567. options._iframe.id = options.id;
  4568. options._iframe.src = options.src;
  4569. options._iframe.style.display = "none";
  4570. // add the hidden iframe to the DOM
  4571. target && target.appendChild( options._iframe );
  4572. },
  4573. /**
  4574. * @member webpage
  4575. * The start function will be executed when the currentTime
  4576. * of the video reaches the start time provided by the
  4577. * options variable
  4578. */
  4579. start: function( event, options ){
  4580. // make the iframe visible
  4581. options._iframe.src = options.src;
  4582. options._iframe.style.display = "inline";
  4583. },
  4584. /**
  4585. * @member webpage
  4586. * The end function will be executed when the currentTime
  4587. * of the video reaches the end time provided by the
  4588. * options variable
  4589. */
  4590. end: function( event, options ){
  4591. // make the iframe invisible
  4592. options._iframe.style.display = "none";
  4593. },
  4594. _teardown: function( options ) {
  4595. document.getElementById( options.target ) && document.getElementById( options.target ).removeChild( options._iframe );
  4596. }
  4597. });
  4598. })( Popcorn );
  4599. // PLUGIN: WIKIPEDIA
  4600. var wikiCallback;
  4601. (function ( Popcorn ) {
  4602. /**
  4603. * Wikipedia popcorn plug-in
  4604. * Displays a wikipedia aricle in the target specified by the user by using
  4605. * new DOM element instead overwriting them
  4606. * Options parameter will need a start, end, target, lang, src, title and numberofwords.
  4607. * -Start is the time that you want this plug-in to execute
  4608. * -End is the time that you want this plug-in to stop executing
  4609. * -Target is the id of the document element that the text from the article needs to be
  4610. * attached to, this target element must exist on the DOM
  4611. * -Lang (optional, defaults to english) is the language in which the article is in.
  4612. * -Src is the url of the article
  4613. * -Title (optional) is the title of the article
  4614. * -numberofwords (optional, defaults to 200) is the number of words you want displaid.
  4615. *
  4616. * @param {Object} options
  4617. *
  4618. * Example:
  4619. var p = Popcorn("#video")
  4620. .wikipedia({
  4621. start: 5, // seconds
  4622. end: 15, // seconds
  4623. src: "http://en.wikipedia.org/wiki/Cape_Town",
  4624. target: "wikidiv"
  4625. } )
  4626. *
  4627. */
  4628. Popcorn.plugin( "wikipedia" , {
  4629. manifest: {
  4630. about:{
  4631. name: "Popcorn Wikipedia Plugin",
  4632. version: "0.1",
  4633. author: "@annasob",
  4634. website: "annasob.wordpress.com"
  4635. },
  4636. options:{
  4637. start: {
  4638. elem: "input",
  4639. type: "number",
  4640. label: "Start"
  4641. },
  4642. end: {
  4643. elem: "input",
  4644. type: "number",
  4645. label: "End"
  4646. },
  4647. lang: {
  4648. elem: "input",
  4649. type: "text",
  4650. label: "Language",
  4651. "default": "english",
  4652. optional: true
  4653. },
  4654. src: {
  4655. elem: "input",
  4656. type: "url",
  4657. label: "Wikipedia URL",
  4658. "default": "http://en.wikipedia.org/wiki/Cat"
  4659. },
  4660. title: {
  4661. elem: "input",
  4662. type: "text",
  4663. label: "Title",
  4664. "default": "Cats",
  4665. optional: true
  4666. },
  4667. numberofwords: {
  4668. elem: "input",
  4669. type: "number",
  4670. label: "Number of Words",
  4671. "default": "200",
  4672. optional: true
  4673. },
  4674. target: "wikipedia-container"
  4675. }
  4676. },
  4677. /**
  4678. * @member wikipedia
  4679. * The setup function will get all of the needed
  4680. * items in place before the start function is called.
  4681. * This includes getting data from wikipedia, if the data
  4682. * is not received and processed before start is called start
  4683. * will not do anything
  4684. */
  4685. _setup : function( options ) {
  4686. // declare needed variables
  4687. // get a guid to use for the global wikicallback function
  4688. var _text, _guid = Popcorn.guid();
  4689. // if the user didn't specify a language default to english
  4690. if ( !options.lang ) {
  4691. options.lang = "en";
  4692. }
  4693. // if the user didn't specify number of words to use default to 200
  4694. options.numberofwords = options.numberofwords || 200;
  4695. // wiki global callback function with a unique id
  4696. // function gets the needed information from wikipedia
  4697. // and stores it by appending values to the options object
  4698. window[ "wikiCallback" + _guid ] = function ( data ) {
  4699. options._link = document.createElement( "a" );
  4700. options._link.setAttribute( "href", options.src );
  4701. options._link.setAttribute( "target", "_blank" );
  4702. // add the title of the article to the link
  4703. options._link.innerHTML = options.title || data.parse.displaytitle;
  4704. // get the content of the wiki article
  4705. options._desc = document.createElement( "p" );
  4706. // get the article text and remove any special characters
  4707. _text = data.parse.text[ "*" ].substr( data.parse.text[ "*" ].indexOf( "<p>" ) );
  4708. _text = _text.replace( /((<(.|\n)+?>)|(\((.*?)\) )|(\[(.*?)\]))/g, "" );
  4709. _text = _text.split( " " );
  4710. options._desc.innerHTML = ( _text.slice( 0, ( _text.length >= options.numberofwords ? options.numberofwords : _text.length ) ).join (" ") + " ..." ) ;
  4711. options._fired = true;
  4712. };
  4713. if ( options.src ) {
  4714. Popcorn.getScript( "//" + options.lang + ".wikipedia.org/w/api.php?action=parse&props=text&redirects&page=" +
  4715. options.src.slice( options.src.lastIndexOf( "/" ) + 1 ) + "&format=json&callback=wikiCallback" + _guid );
  4716. }
  4717. },
  4718. /**
  4719. * @member wikipedia
  4720. * The start function will be executed when the currentTime
  4721. * of the video reaches the start time provided by the
  4722. * options variable
  4723. */
  4724. start: function( event, options ){
  4725. // dont do anything if the information didn't come back from wiki
  4726. var isReady = function () {
  4727. if ( !options._fired ) {
  4728. setTimeout( function () {
  4729. isReady();
  4730. }, 13);
  4731. } else {
  4732. if ( options._link && options._desc ) {
  4733. if ( document.getElementById( options.target ) ) {
  4734. document.getElementById( options.target ).appendChild( options._link );
  4735. document.getElementById( options.target ).appendChild( options._desc );
  4736. options._added = true;
  4737. }
  4738. }
  4739. }
  4740. };
  4741. isReady();
  4742. },
  4743. /**
  4744. * @member wikipedia
  4745. * The end function will be executed when the currentTime
  4746. * of the video reaches the end time provided by the
  4747. * options variable
  4748. */
  4749. end: function( event, options ){
  4750. // ensure that the data was actually added to the
  4751. // DOM before removal
  4752. if ( options._added ) {
  4753. document.getElementById( options.target ).removeChild( options._link );
  4754. document.getElementById( options.target ).removeChild( options._desc );
  4755. }
  4756. },
  4757. _teardown: function( options ){
  4758. if ( options._added ) {
  4759. options._link.parentNode && document.getElementById( options.target ).removeChild( options._link );
  4760. options._desc.parentNode && document.getElementById( options.target ).removeChild( options._desc );
  4761. delete options.target;
  4762. }
  4763. }
  4764. });
  4765. })( Popcorn );
  4766. // PLUGIN: Mustache
  4767. (function ( Popcorn ) {
  4768. /**
  4769. * Mustache Popcorn Plug-in
  4770. *
  4771. * Adds the ability to render JSON using templates via the Mustache templating library.
  4772. *
  4773. * @param {Object} options
  4774. *
  4775. * Required parameters: start, end, template, data, and target.
  4776. * Optional parameter: static.
  4777. *
  4778. * start: the time in seconds when the mustache template should be rendered
  4779. * in the target div.
  4780. *
  4781. * end: the time in seconds when the rendered mustache template should be
  4782. * removed from the target div.
  4783. *
  4784. * target: a String -- the target div's id.
  4785. *
  4786. * template: the mustache template for the plugin to use when rendering. This can be
  4787. * a String containing the template, or a Function that returns the template's
  4788. * String.
  4789. *
  4790. * data: the data to be rendered using the mustache template. This can be a JSON String,
  4791. * a JavaScript Object literal, or a Function returning a String or Literal.
  4792. *
  4793. * dynamic: an optional argument indicating that the template and json data are dynamic
  4794. * and need to be loaded dynamically on every use. Defaults to True.
  4795. *
  4796. * Example:
  4797. var p = Popcorn('#video')
  4798. // Example using template and JSON strings.
  4799. .mustache({
  4800. start: 5, // seconds
  4801. end: 15, // seconds
  4802. target: 'mustache',
  4803. template: '<h1>{{header}}</h1>' +
  4804. '{{#bug}}' +
  4805. '{{/bug}}' +
  4806. '' +
  4807. '{{#items}}' +
  4808. ' {{#first}}' +
  4809. ' <li><strong>{{name}}</strong></li>' +
  4810. ' {{/first}}' +
  4811. ' {{#link}}' +
  4812. ' <li><a href="{{url}}">{{name}}</a></li>' +
  4813. ' {{/link}}' +
  4814. '{{/items}}' +
  4815. '' +
  4816. '{{#empty}}' +
  4817. ' <p>The list is empty.</p>' +
  4818. '{{/empty}}' ,
  4819. data: '{' +
  4820. ' "header": "Colors", ' +
  4821. ' "items": [ ' +
  4822. ' {"name": "red", "first": true, "url": "#Red"}, ' +
  4823. ' {"name": "green", "link": true, "url": "#Green"}, ' +
  4824. ' {"name": "blue", "link": true, "url": "#Blue"} ' +
  4825. ' ],' +
  4826. ' 'empty': false' +
  4827. '}',
  4828. dynamic: false // The json is not going to change, load it early.
  4829. } )
  4830. // Example showing Functions instead of Strings.
  4831. .mustache({
  4832. start: 20, // seconds
  4833. end: 25, // seconds
  4834. target: 'mustache',
  4835. template: function(instance, options) {
  4836. var template = // load your template file here...
  4837. return template;
  4838. },
  4839. data: function(instance, options) {
  4840. var json = // load your json here...
  4841. return json;
  4842. }
  4843. } );
  4844. *
  4845. */
  4846. Popcorn.plugin( "mustache" , function( options ){
  4847. var getData, data, getTemplate, template;
  4848. Popcorn.getScript( "http://mustache.github.com/extras/mustache.js" );
  4849. var shouldReload = !!options.dynamic,
  4850. typeOfTemplate = typeof options.template,
  4851. typeOfData = typeof options.data,
  4852. target = document.getElementById( options.target );
  4853. options.container = target || document.createElement( "div" );
  4854. if ( typeOfTemplate === "function" ) {
  4855. if ( !shouldReload ) {
  4856. template = options.template( options );
  4857. } else {
  4858. getTemplate = options.template;
  4859. }
  4860. } else if ( typeOfTemplate === "string" ) {
  4861. template = options.template;
  4862. } else {
  4863. template = "";
  4864. }
  4865. if ( typeOfData === "function" ) {
  4866. if ( !shouldReload ) {
  4867. data = options.data( options );
  4868. } else {
  4869. getData = options.data;
  4870. }
  4871. } else if ( typeOfData === "string" ) {
  4872. data = JSON.parse( options.data );
  4873. } else if ( typeOfData === "object" ) {
  4874. data = options.data;
  4875. } else {
  4876. data = "";
  4877. }
  4878. return {
  4879. start: function( event, options ) {
  4880. var interval = function() {
  4881. if( !window.Mustache ) {
  4882. setTimeout( function() {
  4883. interval();
  4884. }, 10 );
  4885. } else {
  4886. // if dynamic, freshen json data on every call to start, just in case.
  4887. if ( getData ) {
  4888. data = getData( options );
  4889. }
  4890. if ( getTemplate ) {
  4891. template = getTemplate( options );
  4892. }
  4893. var html = Mustache.to_html( template,
  4894. data
  4895. ).replace( /^\s*/mg, "" );
  4896. options.container.innerHTML = html;
  4897. }
  4898. };
  4899. interval();
  4900. },
  4901. end: function( event, options ) {
  4902. options.container.innerHTML = "";
  4903. },
  4904. _teardown: function( options ) {
  4905. getData = data = getTemplate = template = null;
  4906. }
  4907. };
  4908. },
  4909. {
  4910. about: {
  4911. name: "Popcorn Mustache Plugin",
  4912. version: "0.1",
  4913. author: "David Humphrey (@humphd)",
  4914. website: "http://vocamus.net/dave"
  4915. },
  4916. options: {
  4917. start: {
  4918. elem: "input",
  4919. type: "number",
  4920. label: "Start"
  4921. },
  4922. end: {
  4923. elem: "input",
  4924. type: "number",
  4925. label: "End"
  4926. },
  4927. target: "mustache-container",
  4928. template: {
  4929. elem: "input",
  4930. type: "text",
  4931. label: "Template"
  4932. },
  4933. data: {
  4934. elem: "input",
  4935. type: "text",
  4936. label: "Data"
  4937. },
  4938. dynamic: {
  4939. elem: "input",
  4940. type: "checkbox",
  4941. label: "Dynamic",
  4942. "default": true
  4943. }
  4944. }
  4945. });
  4946. })( Popcorn );
  4947. // PLUGIN: OPENMAP
  4948. ( function ( Popcorn ) {
  4949. /**
  4950. * openmap popcorn plug-in
  4951. * Adds an OpenLayers map and open map tiles (OpenStreetMap [default], NASA WorldWind, or USGS Topographic)
  4952. * Based on the googlemap popcorn plug-in. No StreetView support
  4953. * Options parameter will need a start, end, target, type, zoom, lat and lng
  4954. * -Start is the time that you want this plug-in to execute
  4955. * -End is the time that you want this plug-in to stop executing
  4956. * -Target is the id of the DOM element that you want the map to appear in. This element must be in the DOM
  4957. * -Type [optional] either: ROADMAP (OpenStreetMap), SATELLITE (NASA WorldWind / LandSat), or TERRAIN (USGS).
  4958. * The Stamen custom map types can also be used (http://maps.stamen.com): STAMEN-TONER,
  4959. * STAMEN-TERRAIN, or STAMEN-WATERCOLOR.
  4960. * -Zoom [optional] defaults to 2
  4961. * -Lat and Lng are the coordinates of the map if location is not named
  4962. * -Location is a name of a place to center the map, geocoded to coordinates using TinyGeocoder.com
  4963. * -Markers [optional] is an array of map marker objects, with the following properties:
  4964. * --Icon is the URL of a map marker image
  4965. * --Size [optional] is the radius in pixels of the scaled marker image (default is 14)
  4966. * --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
  4967. * --Lat and Lng are coordinates of the map marker if location is not specified
  4968. * --Location is a name of a place for the map marker, geocoded to coordinates using TinyGeocoder.com
  4969. * Note: using location requires extra loading time, also not specifying both lat/lng and location will
  4970. * cause a JavaScript error.
  4971. * @param {Object} options
  4972. *
  4973. * Example:
  4974. var p = Popcorn( '#video' )
  4975. .openmap({
  4976. start: 5,
  4977. end: 15,
  4978. type: 'ROADMAP',
  4979. target: 'map',
  4980. lat: 43.665429,
  4981. lng: -79.403323
  4982. })
  4983. *
  4984. */
  4985. var newdiv,
  4986. i = 1;
  4987. function toggle( container, display ) {
  4988. if ( container.map ) {
  4989. container.map.div.style.display = display;
  4990. return;
  4991. }
  4992. setTimeout(function() {
  4993. toggle( container, display );
  4994. }, 10 );
  4995. }
  4996. Popcorn.plugin( "openmap", function( options ){
  4997. var newdiv,
  4998. centerlonlat,
  4999. projection,
  5000. displayProjection,
  5001. pointLayer,
  5002. selectControl,
  5003. popup,
  5004. isGeoReady,
  5005. target = document.getElementById( options.target );
  5006. // create a new div within the target div
  5007. // this is later passed on to the maps api
  5008. newdiv = document.createElement( "div" );
  5009. newdiv.id = "openmapdiv" + i;
  5010. newdiv.style.width = "100%";
  5011. newdiv.style.height = "100%";
  5012. i++;
  5013. target && target.appendChild( newdiv );
  5014. // callback function fires when the script is run
  5015. isGeoReady = function() {
  5016. if ( ! ( window.OpenLayers && window.OpenLayers.Layer.Stamen ) ) {
  5017. setTimeout(function() {
  5018. isGeoReady();
  5019. }, 50);
  5020. } else {
  5021. if ( options.location ) {
  5022. // set a dummy center at start
  5023. location = new OpenLayers.LonLat( 0, 0 );
  5024. // query TinyGeocoder and re-center in callback
  5025. Popcorn.getJSONP(
  5026. "//tinygeocoder.com/create-api.php?q=" + options.location + "&callback=jsonp",
  5027. function( latlng ) {
  5028. centerlonlat = new OpenLayers.LonLat( latlng[ 1 ], latlng[ 0 ] );
  5029. }
  5030. );
  5031. } else {
  5032. centerlonlat = new OpenLayers.LonLat( options.lng, options.lat );
  5033. }
  5034. options.type = options.type || "ROADMAP";
  5035. switch( options.type ) {
  5036. case "SATELLITE" :
  5037. // add NASA WorldWind / LANDSAT map
  5038. options.map = new OpenLayers.Map({
  5039. div: newdiv,
  5040. maxResolution: 0.28125,
  5041. tileSize: new OpenLayers.Size( 512, 512 )
  5042. });
  5043. var worldwind = new OpenLayers.Layer.WorldWind(
  5044. "LANDSAT",
  5045. "//worldwind25.arc.nasa.gov/tile/tile.aspx",
  5046. 2.25, 4,
  5047. { T: "105" }
  5048. );
  5049. options.map.addLayer( worldwind );
  5050. displayProjection = new OpenLayers.Projection( "EPSG:4326" );
  5051. projection = new OpenLayers.Projection( "EPSG:4326" );
  5052. break;
  5053. case "TERRAIN":
  5054. // add terrain map ( USGS )
  5055. displayProjection = new OpenLayers.Projection( "EPSG:4326" );
  5056. projection = new OpenLayers.Projection( "EPSG:4326" );
  5057. options.map = new OpenLayers.Map({
  5058. div: newdiv,
  5059. projection: projection
  5060. });
  5061. var relief = new OpenLayers.Layer.WMS(
  5062. "USGS Terraserver",
  5063. "//terraserver-usa.org/ogcmap.ashx?",
  5064. { layers: "DRG" }
  5065. );
  5066. options.map.addLayer( relief );
  5067. break;
  5068. case "STAMEN-TONER":
  5069. case "STAMEN-WATERCOLOR":
  5070. case "STAMEN-TERRAIN":
  5071. var layerName = options.type.replace("STAMEN-", "").toLowerCase();
  5072. var sLayer = new OpenLayers.Layer.Stamen( layerName );
  5073. displayProjection = new OpenLayers.Projection( "EPSG:4326" );
  5074. projection = new OpenLayers.Projection( 'EPSG:900913' );
  5075. centerlonlat = centerlonlat.transform( displayProjection, projection );
  5076. options.map = new OpenLayers.Map( {
  5077. div: newdiv,
  5078. projection: projection,
  5079. displayProjection: displayProjection,
  5080. controls: [
  5081. new OpenLayers.Control.Navigation(),
  5082. new OpenLayers.Control.PanPanel(),
  5083. new OpenLayers.Control.ZoomPanel()
  5084. ]
  5085. } );
  5086. options.map.addLayer( sLayer );
  5087. break;
  5088. default: /* case "ROADMAP": */
  5089. // add OpenStreetMap layer
  5090. projection = new OpenLayers.Projection( 'EPSG:900913' );
  5091. displayProjection = new OpenLayers.Projection( 'EPSG:4326' );
  5092. centerlonlat = centerlonlat.transform( displayProjection, projection );
  5093. options.map = new OpenLayers.Map({
  5094. div: newdiv,
  5095. projection: projection,
  5096. "displayProjection": displayProjection
  5097. });
  5098. var osm = new OpenLayers.Layer.OSM();
  5099. options.map.addLayer( osm );
  5100. break;
  5101. }
  5102. if ( options.map ) {
  5103. options.map.setCenter(centerlonlat, options.zoom || 10);
  5104. options.map.div.style.display = "none";
  5105. }
  5106. }
  5107. };
  5108. isGeoReady();
  5109. return {
  5110. /**
  5111. * @member openmap
  5112. * The setup function will be executed when the plug-in is instantiated
  5113. */
  5114. _setup: function( options ) {
  5115. // insert openlayers api script once
  5116. if ( !window.OpenLayers ) {
  5117. Popcorn.getScript( "//openlayers.org/api/OpenLayers.js", function() {
  5118. Popcorn.getScript( "//maps.stamen.com/js/tile.stamen.js" );
  5119. } );
  5120. }
  5121. var isReady = function() {
  5122. // wait until OpenLayers has been loaded, and the start function is run, before adding map
  5123. if ( !options.map ) {
  5124. setTimeout(function() {
  5125. isReady();
  5126. }, 13 );
  5127. } else {
  5128. // default zoom is 2
  5129. options.zoom = options.zoom || 2;
  5130. // make sure options.zoom is a number
  5131. if ( options.zoom && typeof options.zoom !== "number" ) {
  5132. options.zoom = +options.zoom;
  5133. }
  5134. // reset the location and zoom just in case the user played with the map
  5135. options.map.setCenter( centerlonlat, options.zoom );
  5136. if ( options.markers ) {
  5137. var layerStyle = OpenLayers.Util.extend( {} , OpenLayers.Feature.Vector.style[ "default" ] ),
  5138. featureSelected = function( clickInfo ) {
  5139. clickedFeature = clickInfo.feature;
  5140. if ( !clickedFeature.attributes.text ) {
  5141. return;
  5142. }
  5143. popup = new OpenLayers.Popup.FramedCloud(
  5144. "featurePopup",
  5145. clickedFeature.geometry.getBounds().getCenterLonLat(),
  5146. new OpenLayers.Size( 120, 250 ),
  5147. clickedFeature.attributes.text,
  5148. null,
  5149. true,
  5150. function( closeInfo ) {
  5151. selectControl.unselect( this.feature );
  5152. }
  5153. );
  5154. clickedFeature.popup = popup;
  5155. popup.feature = clickedFeature;
  5156. options.map.addPopup( popup );
  5157. },
  5158. featureUnSelected = function( clickInfo ) {
  5159. feature = clickInfo.feature;
  5160. if ( feature.popup ) {
  5161. popup.feature = null;
  5162. options.map.removePopup( feature.popup );
  5163. feature.popup.destroy();
  5164. feature.popup = null;
  5165. }
  5166. },
  5167. gcThenPlotMarker = function( myMarker ) {
  5168. Popcorn.getJSONP(
  5169. "//tinygeocoder.com/create-api.php?q=" + myMarker.location + "&callback=jsonp",
  5170. function( latlng ) {
  5171. var myPoint = new OpenLayers.Geometry.Point( latlng[1], latlng[0] ).transform( displayProjection, projection ),
  5172. myPointStyle = OpenLayers.Util.extend( {}, layerStyle );
  5173. if ( !myMarker.size || isNaN( myMarker.size ) ) {
  5174. myMarker.size = 14;
  5175. }
  5176. myPointStyle.pointRadius = myMarker.size;
  5177. myPointStyle.graphicOpacity = 1;
  5178. myPointStyle.externalGraphic = myMarker.icon;
  5179. var myPointFeature = new OpenLayers.Feature.Vector( myPoint, null, myPointStyle );
  5180. if ( myMarker.text ) {
  5181. myPointFeature.attributes = {
  5182. text: myMarker.text
  5183. };
  5184. }
  5185. pointLayer.addFeatures( [ myPointFeature ] );
  5186. }
  5187. );
  5188. };
  5189. pointLayer = new OpenLayers.Layer.Vector( "Point Layer", { style: layerStyle } );
  5190. options.map.addLayer( pointLayer );
  5191. for ( var m = 0, l = options.markers.length; m < l ; m++ ) {
  5192. var myMarker = options.markers[ m ];
  5193. if( myMarker.text ){
  5194. if( !selectControl ){
  5195. selectControl = new OpenLayers.Control.SelectFeature( pointLayer );
  5196. options.map.addControl( selectControl );
  5197. selectControl.activate();
  5198. pointLayer.events.on({
  5199. "featureselected": featureSelected,
  5200. "featureunselected": featureUnSelected
  5201. });
  5202. }
  5203. }
  5204. if ( myMarker.location ) {
  5205. var geocodeThenPlotMarker = gcThenPlotMarker;
  5206. geocodeThenPlotMarker( myMarker );
  5207. } else {
  5208. var myPoint = new OpenLayers.Geometry.Point( myMarker.lng, myMarker.lat ).transform( displayProjection, projection ),
  5209. myPointStyle = OpenLayers.Util.extend( {}, layerStyle );
  5210. if ( !myMarker.size || isNaN( myMarker.size ) ) {
  5211. myMarker.size = 14;
  5212. }
  5213. myPointStyle.pointRadius = myMarker.size;
  5214. myPointStyle.graphicOpacity = 1;
  5215. myPointStyle.externalGraphic = myMarker.icon;
  5216. var myPointFeature = new OpenLayers.Feature.Vector( myPoint, null, myPointStyle );
  5217. if ( myMarker.text ) {
  5218. myPointFeature.attributes = {
  5219. text: myMarker.text
  5220. };
  5221. }
  5222. pointLayer.addFeatures( [ myPointFeature ] );
  5223. }
  5224. }
  5225. }
  5226. }
  5227. };
  5228. isReady();
  5229. },
  5230. /**
  5231. * @member openmap
  5232. * The start function will be executed when the currentTime
  5233. * of the video reaches the start time provided by the
  5234. * options variable
  5235. */
  5236. start: function( event, options ) {
  5237. toggle( options, "block" );
  5238. },
  5239. /**
  5240. * @member openmap
  5241. * The end function will be executed when the currentTime
  5242. * of the video reaches the end time provided by the
  5243. * options variable
  5244. */
  5245. end: function( event, options ) {
  5246. toggle( options, "none" );
  5247. },
  5248. _teardown: function( options ) {
  5249. target && target.removeChild( newdiv );
  5250. newdiv = map = centerlonlat = projection = displayProjection = pointLayer = selectControl = popup = null;
  5251. }
  5252. };
  5253. },
  5254. {
  5255. about:{
  5256. name: "Popcorn OpenMap Plugin",
  5257. version: "0.3",
  5258. author: "@mapmeld",
  5259. website: "mapadelsur.blogspot.com"
  5260. },
  5261. options:{
  5262. start: {
  5263. elem: "input",
  5264. type: "number",
  5265. label: "Start"
  5266. },
  5267. end: {
  5268. elem: "input",
  5269. type: "number",
  5270. label: "End"
  5271. },
  5272. target: "map-container",
  5273. type: {
  5274. elem: "select",
  5275. options: [ "ROADMAP", "SATELLITE", "TERRAIN" ],
  5276. label: "Map Type",
  5277. optional: true
  5278. },
  5279. zoom: {
  5280. elem: "input",
  5281. type: "number",
  5282. label: "Zoom",
  5283. "default": 2
  5284. },
  5285. lat: {
  5286. elem: "input",
  5287. type: "text",
  5288. label: "Lat",
  5289. optional: true
  5290. },
  5291. lng: {
  5292. elem: "input",
  5293. type: "text",
  5294. label: "Lng",
  5295. optional: true
  5296. },
  5297. location: {
  5298. elem: "input",
  5299. type: "text",
  5300. label: "Location",
  5301. "default": "Toronto, Ontario, Canada"
  5302. },
  5303. markers: {
  5304. elem: "input",
  5305. type: "text",
  5306. label: "List Markers",
  5307. optional: true
  5308. }
  5309. }
  5310. });
  5311. }) ( Popcorn );
  5312. /**
  5313. * Pause Popcorn Plug-in
  5314. *
  5315. * When this plugin is used, links on the webpage, when clicked, will pause
  5316. * popcorn videos that especified 'pauseOnLinkClicked' as an option. Links may
  5317. * cause a new page to display on a new window, or may cause a new page to
  5318. * display in the current window, in which case the videos won't be available
  5319. * anymore. It only affects anchor tags. It does not affect objects with click
  5320. * events that act as anchors.
  5321. *
  5322. * Example:
  5323. var p = Popcorn('#video', { pauseOnLinkClicked : true } )
  5324. .play();
  5325. *
  5326. */
  5327. document.addEventListener( "click", function( event ) {
  5328. var targetElement = event.target;
  5329. //Some browsers use an element as the target, some use the text node inside
  5330. if ( targetElement.nodeName === "A" || targetElement.parentNode && targetElement.parentNode.nodeName === "A" ) {
  5331. Popcorn.instances.forEach( function( video ) {
  5332. if ( video.options.pauseOnLinkClicked ) {
  5333. video.pause();
  5334. }
  5335. });
  5336. }
  5337. }, false );
  5338. // PLUGIN: Wordriver
  5339. (function ( Popcorn ) {
  5340. var container = {},
  5341. spanLocation = 0,
  5342. setupContainer = function( target ) {
  5343. container[ target ] = document.createElement( "div" );
  5344. var t = document.getElementById( target );
  5345. t && t.appendChild( container[ target ] );
  5346. container[ target ].style.height = "100%";
  5347. container[ target ].style.position = "relative";
  5348. return container[ target ];
  5349. },
  5350. // creates an object of supported, cross platform css transitions
  5351. span = document.createElement( "span" ),
  5352. prefixes = [ "webkit", "Moz", "ms", "O", "" ],
  5353. specProp = [ "Transform", "TransitionDuration", "TransitionTimingFunction" ],
  5354. supports = {},
  5355. prop;
  5356. document.getElementsByTagName( "head" )[ 0 ].appendChild( span );
  5357. for ( var sIdx = 0, sLen = specProp.length; sIdx < sLen; sIdx++ ) {
  5358. for ( var pIdx = 0, pLen = prefixes.length; pIdx < pLen; pIdx++ ) {
  5359. prop = prefixes[ pIdx ] + specProp[ sIdx ];
  5360. if ( prop in span.style ) {
  5361. supports[ specProp[ sIdx ].toLowerCase() ] = prop;
  5362. break;
  5363. }
  5364. }
  5365. }
  5366. // Garbage collect support test span
  5367. document.getElementsByTagName( "head" )[ 0 ].appendChild( span );
  5368. /**
  5369. * Word River popcorn plug-in
  5370. * Displays a string of text, fading it in and out
  5371. * while transitioning across the height of the parent container
  5372. * for the duration of the instance (duration = end - start)
  5373. *
  5374. * @param {Object} options
  5375. *
  5376. * Example:
  5377. var p = Popcorn( '#video' )
  5378. .wordriver({
  5379. start: 5, // When to begin the Word River animation
  5380. end: 15, // When to finish the Word River animation
  5381. text: 'Hello World', // The text you want to be displayed by Word River
  5382. target: 'wordRiverDiv', // The target div to append the text to
  5383. color: "blue" // The color of the text. (can be Hex value i.e. #FFFFFF )
  5384. } )
  5385. *
  5386. */
  5387. Popcorn.plugin( "wordriver" , {
  5388. manifest: {
  5389. about:{
  5390. name: "Popcorn WordRiver Plugin"
  5391. },
  5392. options: {
  5393. start: {
  5394. elem: "input",
  5395. type: "number",
  5396. label: "Start"
  5397. },
  5398. end: {
  5399. elem: "input",
  5400. type: "number",
  5401. label: "End"
  5402. },
  5403. target: "wordriver-container",
  5404. text: {
  5405. elem: "input",
  5406. type: "text",
  5407. label: "Text",
  5408. "default": "Popcorn.js"
  5409. },
  5410. color: {
  5411. elem: "input",
  5412. type: "text",
  5413. label: "Color",
  5414. "default": "Green",
  5415. optional: true
  5416. }
  5417. }
  5418. },
  5419. _setup: function( options ) {
  5420. options._duration = options.end - options.start;
  5421. options._container = container[ options.target ] || setupContainer( options.target );
  5422. options.word = document.createElement( "span" );
  5423. options.word.style.position = "absolute";
  5424. options.word.style.whiteSpace = "nowrap";
  5425. options.word.style.opacity = 0;
  5426. options.word.style.MozTransitionProperty = "opacity, -moz-transform";
  5427. options.word.style.webkitTransitionProperty = "opacity, -webkit-transform";
  5428. options.word.style.OTransitionProperty = "opacity, -o-transform";
  5429. options.word.style.transitionProperty = "opacity, transform";
  5430. options.word.style[ supports.transitionduration ] = 1 + "s, " + options._duration + "s";
  5431. options.word.style[ supports.transitiontimingfunction ] = "linear";
  5432. options.word.innerHTML = options.text;
  5433. options.word.style.color = options.color || "black";
  5434. },
  5435. start: function( event, options ){
  5436. options._container.appendChild( options.word );
  5437. // Resets the transform when changing to a new currentTime before the end event occurred.
  5438. options.word.style[ supports.transform ] = "";
  5439. options.word.style.fontSize = ~~( 30 + 20 * Math.random() ) + "px";
  5440. spanLocation = spanLocation % ( options._container.offsetWidth - options.word.offsetWidth );
  5441. options.word.style.left = spanLocation + "px";
  5442. spanLocation += options.word.offsetWidth + 10;
  5443. options.word.style[ supports.transform ] = "translateY(" +
  5444. ( options._container.offsetHeight - options.word.offsetHeight ) + "px)";
  5445. options.word.style.opacity = 1;
  5446. // automatically clears the word based on time
  5447. setTimeout( function() {
  5448. options.word.style.opacity = 0;
  5449. // ensures at least one second exists, because the fade animation is 1 second
  5450. }, ( ( (options.end - options.start) - 1 ) || 1 ) * 1000 );
  5451. },
  5452. end: function( event, options ){
  5453. // manually clears the word based on user interaction
  5454. options.word.style.opacity = 0;
  5455. },
  5456. _teardown: function( options ) {
  5457. var target = document.getElementById( options.target );
  5458. // removes word span from generated container
  5459. options.word.parentNode && options._container.removeChild( options.word );
  5460. // if no more word spans exist in container, remove container
  5461. container[ options.target ] &&
  5462. !container[ options.target ].childElementCount &&
  5463. target && target.removeChild( container[ options.target ] ) &&
  5464. delete container[ options.target ];
  5465. }
  5466. });
  5467. })( Popcorn );
  5468. // PLUGIN: Timeline
  5469. (function ( Popcorn ) {
  5470. /**
  5471. * timeline popcorn plug-in
  5472. * Adds data associated with a certain time in the video, which creates a scrolling view of each item as the video progresses
  5473. * Options parameter will need a start, target, title, and text
  5474. * -Start is the time that you want this plug-in to execute
  5475. * -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 )
  5476. * -Target is the id of the DOM element that you want the timeline to appear in. This element must be in the DOM
  5477. * -Title is the title of the current timeline box
  5478. * -Text is text is simply related text that will be displayed
  5479. * -innerHTML gives the user the option to add things such as links, buttons and so on
  5480. * -direction specifies whether the timeline will grow from the top or the bottom, receives input as "UP" or "DOWN"
  5481. * @param {Object} options
  5482. *
  5483. * Example:
  5484. var p = Popcorn("#video")
  5485. .timeline( {
  5486. start: 5, // seconds
  5487. target: "timeline",
  5488. title: "Seneca",
  5489. text: "Welcome to seneca",
  5490. innerHTML: "Click this link <a href='http://www.google.ca'>Google</a>"
  5491. } )
  5492. *
  5493. */
  5494. var i = 1;
  5495. Popcorn.plugin( "timeline" , function( options ) {
  5496. var target = document.getElementById( options.target ),
  5497. contentDiv = document.createElement( "div" ),
  5498. container,
  5499. goingUp = true;
  5500. if ( target && !target.firstChild ) {
  5501. target.appendChild ( container = document.createElement( "div" ) );
  5502. container.style.width = "inherit";
  5503. container.style.height = "inherit";
  5504. container.style.overflow = "auto";
  5505. } else {
  5506. container = target.firstChild;
  5507. }
  5508. contentDiv.style.display = "none";
  5509. contentDiv.id = "timelineDiv" + i;
  5510. // Default to up if options.direction is non-existant or not up or down
  5511. options.direction = options.direction || "up";
  5512. if ( options.direction.toLowerCase() === "down" ) {
  5513. goingUp = false;
  5514. }
  5515. if ( target && container ) {
  5516. // if this isnt the first div added to the target div
  5517. if( goingUp ){
  5518. // insert the current div before the previous div inserted
  5519. container.insertBefore( contentDiv, container.firstChild );
  5520. }
  5521. else {
  5522. container.appendChild( contentDiv );
  5523. }
  5524. }
  5525. i++;
  5526. // Default to empty if not used
  5527. //options.innerHTML = options.innerHTML || "";
  5528. contentDiv.innerHTML = "<p><span id='big' style='font-size:24px; line-height: 130%;' >" + options.title + "</span><br />" +
  5529. "<span id='mid' style='font-size: 16px;'>" + options.text + "</span><br />" + options.innerHTML;
  5530. return {
  5531. start: function( event, options ) {
  5532. contentDiv.style.display = "block";
  5533. if( options.direction === "down" ) {
  5534. container.scrollTop = container.scrollHeight;
  5535. }
  5536. },
  5537. end: function( event, options ) {
  5538. contentDiv.style.display = "none";
  5539. },
  5540. _teardown: function( options ) {
  5541. ( container && contentDiv ) && container.removeChild( contentDiv ) && !container.firstChild && target.removeChild( container );
  5542. }
  5543. };
  5544. },
  5545. {
  5546. about: {
  5547. name: "Popcorn Timeline Plugin",
  5548. version: "0.1",
  5549. author: "David Seifried @dcseifried",
  5550. website: "dseifried.wordpress.com"
  5551. },
  5552. options: {
  5553. start: {
  5554. elem: "input",
  5555. type: "number",
  5556. label: "Start"
  5557. },
  5558. end: {
  5559. elem: "input",
  5560. type: "number",
  5561. label: "End"
  5562. },
  5563. target: "feed-container",
  5564. title: {
  5565. elem: "input",
  5566. type: "text",
  5567. label: "Title"
  5568. },
  5569. text: {
  5570. elem: "input",
  5571. type: "text",
  5572. label: "Text"
  5573. },
  5574. innerHTML: {
  5575. elem: "input",
  5576. type: "text",
  5577. label: "HTML Code",
  5578. optional: true
  5579. },
  5580. direction: {
  5581. elem: "select",
  5582. options: [ "DOWN", "UP" ],
  5583. label: "Direction",
  5584. optional: true
  5585. }
  5586. }
  5587. });
  5588. })( Popcorn );
  5589. // PLUGIN: documentcloud
  5590. (function( Popcorn, document ) {
  5591. /**
  5592. * Document Cloud popcorn plug-in
  5593. *
  5594. * @param {Object} options
  5595. *
  5596. * Example:
  5597. * var p = Popcorn("#video")
  5598. * // Let the pdf plugin load your PDF file for you using pdfUrl.
  5599. * .documentcloud({
  5600. * start: 45
  5601. * url: "http://www.documentcloud.org/documents/70050-urbina-day-1-in-progress.html", // or .js
  5602. * width: ...,
  5603. * height: ...,
  5604. * zoom: ...,
  5605. * page: ...,
  5606. * container: ...
  5607. * });
  5608. api - https://github.com/documentcloud/document-viewer/blob/master/public/javascripts/DV/controllers/api.js
  5609. */
  5610. // track registered plugins by document
  5611. var documentRegistry = {};
  5612. Popcorn.plugin( "documentcloud", {
  5613. manifest: {
  5614. about: {
  5615. name: "Popcorn Document Cloud Plugin",
  5616. version: "0.1",
  5617. author: "@humphd, @ChrisDeCairos",
  5618. website: "http://vocamus.net/dave"
  5619. },
  5620. options: {
  5621. start: {
  5622. elem: "input",
  5623. type: "number",
  5624. label: "Start"
  5625. },
  5626. end: {
  5627. elem: "input",
  5628. type: "number",
  5629. label: "End"
  5630. },
  5631. target: "documentcloud-container",
  5632. width: {
  5633. elem: "input",
  5634. type: "text",
  5635. label: "Width",
  5636. optional: true
  5637. },
  5638. height: {
  5639. elem: "input",
  5640. type: "text",
  5641. label: "Height",
  5642. optional: true
  5643. },
  5644. src: {
  5645. elem: "input",
  5646. type: "url",
  5647. label: "PDF URL",
  5648. "default": "http://www.documentcloud.org/documents/70050-urbina-day-1-in-progress.html"
  5649. },
  5650. preload: {
  5651. elem: "input",
  5652. type: "checkbox",
  5653. label: "Preload",
  5654. "default": true
  5655. },
  5656. page: {
  5657. elem: "input",
  5658. type: "number",
  5659. label: "Page Number",
  5660. optional: true
  5661. },
  5662. aid: {
  5663. elem: "input",
  5664. type: "number",
  5665. label: "Annotation Id",
  5666. optional: true
  5667. }
  5668. }
  5669. },
  5670. _setup: function( options ) {
  5671. var DV = window.DV = window.DV || {},
  5672. that = this;
  5673. //setup elem...
  5674. function load() {
  5675. DV.loaded = false;
  5676. // swap .html URL to .js for API call
  5677. var url = options.url.replace( /\.html$/, ".js" ),
  5678. target = options.target,
  5679. targetDiv = document.getElementById( target ),
  5680. containerDiv = document.createElement( "div" ),
  5681. containerDivSize = Popcorn.position( targetDiv ),
  5682. // need to use size of div if not given
  5683. width = options.width || containerDivSize.width,
  5684. height = options.height || containerDivSize.height,
  5685. sidebar = options.sidebar || true,
  5686. text = options.text || true,
  5687. pdf = options.pdf || true,
  5688. showAnnotations = options.showAnnotations || true,
  5689. zoom = options.zoom || 700,
  5690. search = options.search || true,
  5691. page = options.page,
  5692. container;
  5693. function setOptions( viewer ) {
  5694. options._key = viewer.api.getId();
  5695. options._changeView = function ( viewer ) {
  5696. if ( options.aid ) {
  5697. viewer.pageSet.showAnnotation( viewer.api.getAnnotation( options.aid ) );
  5698. } else {
  5699. viewer.api.setCurrentPage( options.page );
  5700. }
  5701. };
  5702. }
  5703. function documentIsLoaded( url ) {
  5704. var found = false;
  5705. Popcorn.forEach( DV.viewers, function( viewer, idx ) {
  5706. if( viewer.api.getSchema().canonicalURL === url ) {
  5707. var targetDoc;
  5708. setOptions( viewer );
  5709. targetDoc = documentRegistry[ options._key ];
  5710. options._containerId = targetDoc.id;
  5711. targetDoc.num += 1;
  5712. found = true;
  5713. DV.loaded = true;
  5714. }
  5715. });
  5716. return found;
  5717. }
  5718. function createRegistryEntry() {
  5719. var entry = {
  5720. num: 1,
  5721. id: options._containerId
  5722. };
  5723. documentRegistry[ options._key ] = entry;
  5724. DV.loaded = true;
  5725. }
  5726. if ( !documentIsLoaded( options.url ) ) {
  5727. containerDiv.id = options._containerId = Popcorn.guid( target );
  5728. container = "#" + containerDiv.id;
  5729. targetDiv.appendChild( containerDiv );
  5730. that.trigger( "documentready" );
  5731. // Figure out if we need a callback to change the page #
  5732. var afterLoad = options.page || options.aid ?
  5733. function( viewer ) {
  5734. setOptions( viewer );
  5735. options._changeView( viewer );
  5736. containerDiv.style.visibility = "hidden";
  5737. viewer.elements.pages.hide();
  5738. createRegistryEntry();
  5739. } :
  5740. function( viewer ) {
  5741. setOptions( viewer );
  5742. createRegistryEntry();
  5743. containerDiv.style.visibility = "hidden";
  5744. viewer.elements.pages.hide();
  5745. };
  5746. DV.load( url, {
  5747. width: width,
  5748. height: height,
  5749. sidebar: sidebar,
  5750. text: text,
  5751. pdf: pdf,
  5752. showAnnotations: showAnnotations,
  5753. zoom: zoom,
  5754. search: search,
  5755. container: container,
  5756. afterLoad: afterLoad
  5757. });
  5758. }
  5759. }
  5760. function readyCheck() {
  5761. if( window.DV.loaded ) {
  5762. load();
  5763. } else {
  5764. setTimeout( readyCheck, 25 );
  5765. }
  5766. }
  5767. // If the viewer is already loaded, don't repeat the process.
  5768. if ( !DV.loading ) {
  5769. DV.loading = true;
  5770. DV.recordHit = "//www.documentcloud.org/pixel.gif";
  5771. var link = document.createElement( "link" ),
  5772. head = document.getElementsByTagName( "head" )[ 0 ];
  5773. link.rel = "stylesheet";
  5774. link.type = "text/css";
  5775. link.media = "screen";
  5776. link.href = "//s3.documentcloud.org/viewer/viewer-datauri.css";
  5777. head.appendChild( link );
  5778. // Record the fact that the viewer is loaded.
  5779. DV.loaded = false;
  5780. // Request the viewer JavaScript.
  5781. Popcorn.getScript( "http://s3.documentcloud.org/viewer/viewer.js", function() {
  5782. DV.loading = false;
  5783. load();
  5784. });
  5785. } else {
  5786. readyCheck();
  5787. }
  5788. },
  5789. start: function( event, options ) {
  5790. var elem = document.getElementById( options._containerId ),
  5791. viewer = DV.viewers[ options._key ];
  5792. ( options.page || options.aid ) && viewer &&
  5793. options._changeView( viewer );
  5794. if ( elem && viewer) {
  5795. elem.style.visibility = "visible";
  5796. viewer.elements.pages.show();
  5797. }
  5798. },
  5799. end: function( event, options ) {
  5800. var elem = document.getElementById( options._containerId );
  5801. if ( elem && DV.viewers[ options._key ] ) {
  5802. elem.style.visibility = "hidden";
  5803. DV.viewers[ options._key ].elements.pages.hide();
  5804. }
  5805. },
  5806. _teardown: function( options ) {
  5807. var elem = document.getElementById( options._containerId ),
  5808. key = options._key;
  5809. if ( key && DV.viewers[ key ] && --documentRegistry[ key ].num === 0 ) {
  5810. DV.viewers[ key ].api.unload();
  5811. while ( elem.hasChildNodes() ) {
  5812. elem.removeChild( elem.lastChild );
  5813. }
  5814. elem.parentNode.removeChild( elem );
  5815. }
  5816. }
  5817. });
  5818. })( Popcorn, window.document );
  5819. // PARSER: 0.3 JSON
  5820. (function (Popcorn) {
  5821. Popcorn.parser( "parseJSON", "JSON", function( data ) {
  5822. // declare needed variables
  5823. var retObj = {
  5824. title: "",
  5825. remote: "",
  5826. data: []
  5827. },
  5828. manifestData = {},
  5829. dataObj = data;
  5830. /*
  5831. TODO: add support for filling in source children of the video element
  5832. remote: [
  5833. {
  5834. src: "whatever.mp4",
  5835. type: 'video/mp4; codecs="avc1, mp4a"'
  5836. },
  5837. {
  5838. src: "whatever.ogv",
  5839. type: 'video/ogg; codecs="theora, vorbis"'
  5840. }
  5841. ]
  5842. */
  5843. Popcorn.forEach( dataObj.data, function ( obj, key ) {
  5844. retObj.data.push( obj );
  5845. });
  5846. return retObj;
  5847. });
  5848. })( Popcorn );
  5849. // PARSER: 0.1 SBV
  5850. (function (Popcorn) {
  5851. /**
  5852. * SBV popcorn parser plug-in
  5853. * Parses subtitle files in the SBV format.
  5854. * Times are expected in H:MM:SS.MIL format, with hours optional
  5855. * Subtitles which don't match expected format are ignored
  5856. * Data parameter is given by Popcorn, will need a text.
  5857. * Text is the file contents to be parsed
  5858. *
  5859. * @param {Object} data
  5860. *
  5861. * Example:
  5862. 0:00:02.400,0:00:07.200
  5863. Senator, we're making our final approach into Coruscant.
  5864. */
  5865. Popcorn.parser( "parseSBV", function( data ) {
  5866. // declare needed variables
  5867. var retObj = {
  5868. title: "",
  5869. remote: "",
  5870. data: []
  5871. },
  5872. subs = [],
  5873. lines,
  5874. i = 0,
  5875. len = 0,
  5876. idx = 0;
  5877. // [H:]MM:SS.MIL string to SS.MIL
  5878. // Will thrown exception on bad time format
  5879. var toSeconds = function( t_in ) {
  5880. var t = t_in.split( ":" ),
  5881. l = t.length-1,
  5882. time;
  5883. try {
  5884. time = parseInt( t[l-1], 10 )*60 + parseFloat( t[l], 10 );
  5885. // Hours optionally given
  5886. if ( l === 2 ) {
  5887. time += parseInt( t[0], 10 )*3600;
  5888. }
  5889. } catch ( e ) {
  5890. throw "Bad cue";
  5891. }
  5892. return time;
  5893. };
  5894. var createTrack = function( name, attributes ) {
  5895. var track = {};
  5896. track[name] = attributes;
  5897. return track;
  5898. };
  5899. // Here is where the magic happens
  5900. // Split on line breaks
  5901. lines = data.text.split( /(?:\r\n|\r|\n)/gm );
  5902. len = lines.length;
  5903. while ( i < len ) {
  5904. var sub = {},
  5905. text = [],
  5906. time = lines[i++].split( "," );
  5907. try {
  5908. sub.start = toSeconds( time[0] );
  5909. sub.end = toSeconds( time[1] );
  5910. // Gather all lines of text
  5911. while ( i < len && lines[i] ) {
  5912. text.push( lines[i++] );
  5913. }
  5914. // Join line breaks in text
  5915. sub.text = text.join( "<br />" );
  5916. subs.push( createTrack( "subtitle", sub ) );
  5917. } catch ( e ) {
  5918. // Bad cue, advance to end of cue
  5919. while ( i < len && lines[i] ) {
  5920. i++;
  5921. }
  5922. }
  5923. // Consume empty whitespace
  5924. while ( i < len && !lines[i] ) {
  5925. i++;
  5926. }
  5927. }
  5928. retObj.data = subs;
  5929. return retObj;
  5930. });
  5931. })( Popcorn );
  5932. // PARSER: 0.3 SRT
  5933. (function (Popcorn) {
  5934. /**
  5935. * SRT popcorn parser plug-in
  5936. * Parses subtitle files in the SRT format.
  5937. * Times are expected in HH:MM:SS,MIL format, though HH:MM:SS.MIL also supported
  5938. * Ignore styling, which may occur after the end time or in-text
  5939. * While not part of the "official" spec, majority of players support HTML and SSA styling tags
  5940. * SSA-style tags are stripped, HTML style tags are left for the browser to handle:
  5941. * HTML: <font>, <b>, <i>, <u>, <s>
  5942. * SSA: \N or \n, {\cmdArg1}, {\cmd(arg1, arg2, ...)}
  5943. * Data parameter is given by Popcorn, will need a text.
  5944. * Text is the file contents to be parsed
  5945. *
  5946. * @param {Object} data
  5947. *
  5948. * Example:
  5949. 1
  5950. 00:00:25,712 --> 00:00:30.399
  5951. This text is <font color="red">RED</font> and has not been {\pos(142,120)} positioned.
  5952. This takes \Nup three \nentire lines.
  5953. This contains nested <b>bold, <i>italic, <u>underline</u> and <s>strike-through</s></u></i></b> HTML tags
  5954. Unclosed but <b>supported tags are left in
  5955. <ggg>Unsupported</ggg> HTML tags are left in, even if <hhh>not closed.
  5956. SSA tags with {\i1} would open and close italicize {\i0}, but are stripped
  5957. Multiple {\pos(142,120)\b1}SSA tags are stripped
  5958. */
  5959. Popcorn.parser( "parseSRT", function( data ) {
  5960. // declare needed variables
  5961. var retObj = {
  5962. title: "",
  5963. remote: "",
  5964. data: []
  5965. },
  5966. subs = [],
  5967. i = 0,
  5968. idx = 0,
  5969. lines,
  5970. time,
  5971. text,
  5972. endIdx,
  5973. sub;
  5974. // Here is where the magic happens
  5975. // Split on line breaks
  5976. lines = data.text.split( /(?:\r\n|\r|\n)/gm );
  5977. endIdx = lastNonEmptyLine( lines ) + 1;
  5978. for( i=0; i < endIdx; i++ ) {
  5979. sub = {};
  5980. text = [];
  5981. sub.id = parseInt( lines[i++], 10 );
  5982. // Split on '-->' delimiter, trimming spaces as well
  5983. time = lines[i++].split( /[\t ]*-->[\t ]*/ );
  5984. sub.start = toSeconds( time[0] );
  5985. // So as to trim positioning information from end
  5986. idx = time[1].indexOf( " " );
  5987. if ( idx !== -1) {
  5988. time[1] = time[1].substr( 0, idx );
  5989. }
  5990. sub.end = toSeconds( time[1] );
  5991. // Build single line of text from multi-line subtitle in file
  5992. while ( i < endIdx && lines[i] ) {
  5993. text.push( lines[i++] );
  5994. }
  5995. // Join into 1 line, SSA-style linebreaks
  5996. // Strip out other SSA-style tags
  5997. sub.text = text.join( "\\N" ).replace( /\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}/gi, "" );
  5998. // Escape HTML entities
  5999. sub.text = sub.text.replace( /</g, "&lt;" ).replace( />/g, "&gt;" );
  6000. // Unescape great than and less than when it makes a valid html tag of a supported style (font, b, u, s, i)
  6001. // Modified version of regex from Phil Haack's blog: http://haacked.com/archive/2004/10/25/usingregularexpressionstomatchhtml.aspx
  6002. // Later modified by kev: http://kevin.deldycke.com/2007/03/ultimate-regular-expression-for-html-tag-parsing-with-php/
  6003. sub.text = sub.text.replace( /&lt;(\/?(font|b|u|i|s))((\s+(\w|\w[\w\-]*\w)(\s*=\s*(?:\".*?\"|'.*?'|[^'\">\s]+))?)+\s*|\s*)(\/?)&gt;/gi, "<$1$3$7>" );
  6004. sub.text = sub.text.replace( /\\N/gi, "<br />" );
  6005. subs.push( createTrack( "subtitle", sub ) );
  6006. }
  6007. retObj.data = subs;
  6008. return retObj;
  6009. });
  6010. function createTrack( name, attributes ) {
  6011. var track = {};
  6012. track[name] = attributes;
  6013. return track;
  6014. }
  6015. // Simple function to convert HH:MM:SS,MMM or HH:MM:SS.MMM to SS.MMM
  6016. // Assume valid, returns 0 on error
  6017. function toSeconds( t_in ) {
  6018. var t = t_in.split( ':' );
  6019. try {
  6020. var s = t[2].split( ',' );
  6021. // Just in case a . is decimal seperator
  6022. if ( s.length === 1 ) {
  6023. s = t[2].split( '.' );
  6024. }
  6025. return parseFloat( t[0], 10 ) * 3600 + parseFloat( t[1], 10 ) * 60 + parseFloat( s[0], 10 ) + parseFloat( s[1], 10 ) / 1000;
  6026. } catch ( e ) {
  6027. return 0;
  6028. }
  6029. }
  6030. function lastNonEmptyLine( linesArray ) {
  6031. var idx = linesArray.length - 1;
  6032. while ( idx >= 0 && !linesArray[idx] ) {
  6033. idx--;
  6034. }
  6035. return idx;
  6036. }
  6037. })( Popcorn );
  6038. // PARSER: 0.3 SSA/ASS
  6039. (function ( Popcorn ) {
  6040. /**
  6041. * SSA/ASS popcorn parser plug-in
  6042. * Parses subtitle files in the identical SSA and ASS formats.
  6043. * Style information is ignored, and may be found in these
  6044. * formats: (\N \n {\pos(400,570)} {\kf89})
  6045. * Out of the [Script Info], [V4 Styles], [Events], [Pictures],
  6046. * and [Fonts] sections, only [Events] is processed.
  6047. * Data parameter is given by Popcorn, will need a text.
  6048. * Text is the file contents to be parsed
  6049. *
  6050. * @param {Object} data
  6051. *
  6052. * Example:
  6053. [Script Info]
  6054. Title: Testing subtitles for the SSA Format
  6055. [V4 Styles]
  6056. Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding
  6057. Style: Default,Arial,20,65535,65535,65535,-2147483640,-1,0,1,3,0,2,30,30,30,0,0
  6058. [Events]
  6059. Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
  6060. Dialogue: 0,0:00:02.40,0:00:07.20,Default,,0000,0000,0000,,Senator, {\kf89}we're \Nmaking our final \napproach into Coruscant.
  6061. Dialogue: 0,0:00:09.71,0:00:13.39,Default,,0000,0000,0000,,{\pos(400,570)}Very good, Lieutenant.
  6062. Dialogue: 0,0:00:15.04,0:00:18.04,Default,,0000,0000,0000,,It's \Na \ntrap!
  6063. *
  6064. */
  6065. // Register for SSA extensions
  6066. Popcorn.parser( "parseSSA", function( data ) {
  6067. // declare needed variables
  6068. var retObj = {
  6069. title: "",
  6070. remote: "",
  6071. data: [ ]
  6072. },
  6073. rNewLineFile = /(?:\r\n|\r|\n)/gm,
  6074. subs = [ ],
  6075. lines,
  6076. headers,
  6077. i = 0,
  6078. len;
  6079. // Here is where the magic happens
  6080. // Split on line breaks
  6081. lines = data.text.split( rNewLineFile );
  6082. len = lines.length;
  6083. // Ignore non-textual info
  6084. while ( i < len && lines[ i ] !== "[Events]" ) {
  6085. i++;
  6086. }
  6087. headers = parseFieldHeaders( lines[ ++i ] );
  6088. while ( ++i < len && lines[ i ] && lines[ i ][ 0 ] !== "[" ) {
  6089. try {
  6090. subs.push( createTrack( "subtitle", parseSub( lines[ i ], headers ) ) );
  6091. } catch ( e ) {}
  6092. }
  6093. retObj.data = subs;
  6094. return retObj;
  6095. });
  6096. function parseSub( line, headers ) {
  6097. // Trim beginning 'Dialogue: ' and split on delim
  6098. var fields = line.substr( 10 ).split( "," ),
  6099. rAdvancedStyles = /\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}/gi,
  6100. rNewLineSSA = /\\N/gi,
  6101. sub;
  6102. sub = {
  6103. start: toSeconds( fields[ headers.start ] ),
  6104. end: toSeconds( fields[ headers.end ] )
  6105. };
  6106. // Invalid time, skip
  6107. if ( sub.start === -1 || sub.end === -1 ) {
  6108. throw "Invalid time";
  6109. }
  6110. // Eliminate advanced styles and convert forced line breaks
  6111. sub.text = getTextFromFields( fields, headers.text ).replace( rAdvancedStyles, "" ).replace( rNewLineSSA, "<br />" );
  6112. return sub;
  6113. }
  6114. // h:mm:ss.cc (centisec) string to SS.mmm
  6115. // Returns -1 if invalid
  6116. function toSeconds( t_in ) {
  6117. var t = t_in.split( ":" );
  6118. // Not all there
  6119. if ( t_in.length !== 10 || t.length < 3 ) {
  6120. return -1;
  6121. }
  6122. return parseInt( t[ 0 ], 10 ) * 3600 + parseInt( t[ 1 ], 10 ) * 60 + parseFloat( t[ 2 ], 10 );
  6123. }
  6124. function getTextFromFields( fields, startIdx ) {
  6125. var fieldLen = fields.length,
  6126. text = [ ],
  6127. i = startIdx;
  6128. // There may be commas in the text which were split, append back together into one line
  6129. for( ; i < fieldLen; i++ ) {
  6130. text.push( fields[ i ] );
  6131. }
  6132. return text.join( "," );
  6133. }
  6134. function createTrack( name, attributes ) {
  6135. var track = {};
  6136. track[ name ] = attributes;
  6137. return track;
  6138. }
  6139. function parseFieldHeaders( line ) {
  6140. // Trim 'Format: ' off front, split on delim
  6141. var fields = line.substr( 8 ).split( ", " ),
  6142. result = {},
  6143. len,
  6144. i;
  6145. //Find where in Dialogue string the start, end and text info is
  6146. for ( i = 0, len = fields.length; i < len; i++ ) {
  6147. if ( fields[ i ] === "Start" ) {
  6148. result.start = i;
  6149. } else if ( fields[ i ] === "End" ) {
  6150. result.end = i;
  6151. } else if ( fields[ i ] === "Text" ) {
  6152. result.text = i;
  6153. }
  6154. }
  6155. return result;
  6156. }
  6157. })( Popcorn );
  6158. // PARSER: 1.0 TTML
  6159. (function ( Popcorn ) {
  6160. /**
  6161. * TTML popcorn parser plug-in
  6162. * Parses subtitle files in the TTML format.
  6163. * Times may be absolute to the timeline or relative
  6164. * Absolute times are ISO 8601 format (hh:mm:ss[.mmm])
  6165. * Relative times are a fraction followed by a unit metric (d.ddu)
  6166. * Relative times are relative to the time given on the parent node
  6167. * Styling information is ignored
  6168. * Data parameter is given by Popcorn, will need an xml.
  6169. * Xml is the file contents to be processed
  6170. *
  6171. * @param {Object} data
  6172. *
  6173. * Example:
  6174. <tt xmlns:tts="http://www.w3.org/2006/04/ttaf1#styling" xmlns="http://www.w3.org/2006/04/ttaf1">
  6175. <body region="subtitleArea">
  6176. <div>
  6177. <p xml:id="subtitle1" begin="0.76s" end="3.45s">
  6178. It seems a paradox, does it not,
  6179. </p>
  6180. </div>
  6181. </body>
  6182. </tt>
  6183. */
  6184. var rWhitespace = /^[\s]+|[\s]+$/gm,
  6185. rLineBreak = /(?:\r\n|\r|\n)/gm;
  6186. Popcorn.parser( "parseTTML", function( data ) {
  6187. var returnData = {
  6188. title: "",
  6189. remote: "",
  6190. data: []
  6191. },
  6192. node;
  6193. // Null checks
  6194. if ( !data.xml || !data.xml.documentElement ) {
  6195. return returnData;
  6196. }
  6197. node = data.xml.documentElement.firstChild;
  6198. if ( !node ) {
  6199. return returnData;
  6200. }
  6201. // Find body tag
  6202. while ( node.nodeName !== "body" ) {
  6203. node = node.nextSibling;
  6204. }
  6205. if ( node ) {
  6206. returnData.data = parseChildren( node, 0 );
  6207. }
  6208. return returnData;
  6209. });
  6210. // Parse the children of the given node
  6211. function parseChildren( node, timeOffset, region ) {
  6212. var currNode = node.firstChild,
  6213. currRegion = getNodeRegion( node, region ),
  6214. retVal = [],
  6215. newOffset;
  6216. while ( currNode ) {
  6217. if ( currNode.nodeType === 1 ) {
  6218. if ( currNode.nodeName === "p" ) {
  6219. // p is a textual node, process contents as subtitle
  6220. retVal.push( parseNode( currNode, timeOffset, currRegion ) );
  6221. } else if ( currNode.nodeName === "div" ) {
  6222. // div is container for subtitles, recurse
  6223. newOffset = toSeconds( currNode.getAttribute( "begin" ) );
  6224. if (newOffset < 0 ) {
  6225. newOffset = timeOffset;
  6226. }
  6227. retVal.push.apply( retVal, parseChildren( currNode, newOffset, currRegion ) );
  6228. }
  6229. }
  6230. currNode = currNode.nextSibling;
  6231. }
  6232. return retVal;
  6233. }
  6234. // Get the "region" attribute of a node, to know where to put the subtitles
  6235. function getNodeRegion( node, defaultTo ) {
  6236. var region = node.getAttribute( "region" );
  6237. if ( region !== null ) {
  6238. return region;
  6239. } else {
  6240. return defaultTo || "";
  6241. }
  6242. }
  6243. // Parse a node for text content
  6244. function parseNode( node, timeOffset, region ) {
  6245. var sub = {};
  6246. // Trim left and right whitespace from text and convert non-explicit line breaks
  6247. sub.text = ( node.textContent || node.text ).replace( rWhitespace, "" ).replace( rLineBreak, "<br />" );
  6248. sub.id = node.getAttribute( "xml:id" ) || node.getAttribute( "id" );
  6249. sub.start = toSeconds ( node.getAttribute( "begin" ), timeOffset );
  6250. sub.end = toSeconds( node.getAttribute( "end" ), timeOffset );
  6251. sub.target = getNodeRegion( node, region );
  6252. if ( sub.end < 0 ) {
  6253. // No end given, infer duration if possible
  6254. // Otherwise, give end as MAX_VALUE
  6255. sub.end = toSeconds( node.getAttribute( "duration" ), 0 );
  6256. if ( sub.end >= 0 ) {
  6257. sub.end += sub.start;
  6258. } else {
  6259. sub.end = Number.MAX_VALUE;
  6260. }
  6261. }
  6262. return { subtitle : sub };
  6263. }
  6264. // Convert time expression to SS.mmm
  6265. // Expression may be absolute to timeline (hh:mm:ss.ms)
  6266. // or relative ( decimal followed by metric ) ex: 3.4s, 5.7m
  6267. // Returns -1 if invalid
  6268. function toSeconds( t_in, offset ) {
  6269. var i;
  6270. if ( !t_in ) {
  6271. return -1;
  6272. }
  6273. try {
  6274. return Popcorn.util.toSeconds( t_in );
  6275. } catch ( e ) {
  6276. i = getMetricIndex( t_in );
  6277. return parseFloat( t_in.substring( 0, i ) ) * getMultipler( t_in.substring( i ) ) + ( offset || 0 );
  6278. }
  6279. }
  6280. // In a time string such as 3.4ms, get the index of the first character (m) of the time metric (ms)
  6281. function getMetricIndex( t_in ) {
  6282. var i = t_in.length - 1;
  6283. while ( i >= 0 && t_in[ i ] <= "9" && t_in[ i ] >= "0" ) {
  6284. i--;
  6285. }
  6286. return i;
  6287. }
  6288. // Determine multiplier for metric relative to seconds
  6289. function getMultipler( metric ) {
  6290. return {
  6291. "h" : 3600,
  6292. "m" : 60,
  6293. "s" : 1,
  6294. "ms" : 0.001
  6295. }[ metric ] || -1;
  6296. }
  6297. })( Popcorn );
  6298. // PARSER: 0.1 TTXT
  6299. (function (Popcorn) {
  6300. /**
  6301. * TTXT popcorn parser plug-in
  6302. * Parses subtitle files in the TTXT format.
  6303. * Style information is ignored.
  6304. * Data parameter is given by Popcorn, will need an xml.
  6305. * Xml is the file contents to be parsed as a DOM tree
  6306. *
  6307. * @param {Object} data
  6308. *
  6309. * Example:
  6310. <TextSample sampleTime="00:00:00.000" text=""></TextSample>
  6311. */
  6312. Popcorn.parser( "parseTTXT", function( data ) {
  6313. // declare needed variables
  6314. var returnData = {
  6315. title: "",
  6316. remote: "",
  6317. data: []
  6318. };
  6319. // Simple function to convert HH:MM:SS.MMM to SS.MMM
  6320. // Assume valid, returns 0 on error
  6321. var toSeconds = function(t_in) {
  6322. var t = t_in.split(":");
  6323. var time = 0;
  6324. try {
  6325. return parseFloat(t[0], 10)*60*60 + parseFloat(t[1], 10)*60 + parseFloat(t[2], 10);
  6326. } catch (e) { time = 0; }
  6327. return time;
  6328. };
  6329. // creates an object of all atrributes keyed by name
  6330. var createTrack = function( name, attributes ) {
  6331. var track = {};
  6332. track[name] = attributes;
  6333. return track;
  6334. };
  6335. // this is where things actually start
  6336. var node = data.xml.lastChild.lastChild; // Last Child of TextStreamHeader
  6337. var lastStart = Number.MAX_VALUE;
  6338. var cmds = [];
  6339. // Work backwards through DOM, processing TextSample nodes
  6340. while (node) {
  6341. if ( node.nodeType === 1 && node.nodeName === "TextSample") {
  6342. var sub = {};
  6343. sub.start = toSeconds(node.getAttribute('sampleTime'));
  6344. sub.text = node.getAttribute('text');
  6345. if (sub.text) { // Only process if text to display
  6346. // Infer end time from prior element, ms accuracy
  6347. sub.end = lastStart - 0.001;
  6348. cmds.push( createTrack("subtitle", sub) );
  6349. }
  6350. lastStart = sub.start;
  6351. }
  6352. node = node.previousSibling;
  6353. }
  6354. returnData.data = cmds.reverse();
  6355. return returnData;
  6356. });
  6357. })( Popcorn );
  6358. // PARSER: 0.3 WebSRT/VTT
  6359. (function ( Popcorn ) {
  6360. /**
  6361. * WebVTT popcorn parser plug-in
  6362. * Parses subtitle files in the WebVTT format.
  6363. * Specification here: http://www.whatwg.org/specs/web-apps/current-work/webvtt.html
  6364. * Styles which appear after timing information are presently ignored.
  6365. * Inline styling tags follow HTML conventions and are left in for the browser to handle (or ignore if VTT-specific)
  6366. * Data parameter is given by Popcorn, text property holds file contents.
  6367. * Text is the file contents to be parsed
  6368. *
  6369. * @param {Object} data
  6370. *
  6371. * Example:
  6372. 00:32.500 --> 00:00:33.500 A:start S:50% D:vertical L:98%
  6373. <v Neil DeGrass Tyson><i>Laughs</i>
  6374. */
  6375. Popcorn.parser( "parseVTT", function( data ) {
  6376. // declare needed variables
  6377. var retObj = {
  6378. title: "",
  6379. remote: "",
  6380. data: []
  6381. },
  6382. subs = [],
  6383. i = 0,
  6384. len = 0,
  6385. lines,
  6386. text,
  6387. sub,
  6388. rNewLine = /(?:\r\n|\r|\n)/gm;
  6389. // Here is where the magic happens
  6390. // Split on line breaks
  6391. lines = data.text.split( rNewLine );
  6392. len = lines.length;
  6393. // Check for BOF token
  6394. if ( len === 0 || lines[ 0 ] !== "WEBVTT" ) {
  6395. return retObj;
  6396. }
  6397. i++;
  6398. while ( i < len ) {
  6399. text = [];
  6400. try {
  6401. i = skipWhitespace( lines, len, i );
  6402. sub = parseCueHeader( lines[ i++ ] );
  6403. // Build single line of text from multi-line subtitle in file
  6404. while ( i < len && lines[ i ] ) {
  6405. text.push( lines[ i++ ] );
  6406. }
  6407. // Join lines together to one and build subtitle text
  6408. sub.text = text.join( "<br />" );
  6409. subs.push( createTrack( "subtitle", sub ) );
  6410. } catch ( e ) {
  6411. i = skipNonWhitespace( lines, len, i );
  6412. }
  6413. }
  6414. retObj.data = subs;
  6415. return retObj;
  6416. });
  6417. // [HH:]MM:SS.mmm string to SS.mmm float
  6418. // Throws exception if invalid
  6419. function toSeconds ( t_in ) {
  6420. var t = t_in.split( ":" ),
  6421. l = t_in.length,
  6422. time;
  6423. // Invalid time string provided
  6424. if ( l !== 12 && l !== 9 ) {
  6425. throw "Bad cue";
  6426. }
  6427. l = t.length - 1;
  6428. try {
  6429. time = parseInt( t[ l-1 ], 10 ) * 60 + parseFloat( t[ l ], 10 );
  6430. // Hours were given
  6431. if ( l === 2 ) {
  6432. time += parseInt( t[ 0 ], 10 ) * 3600;
  6433. }
  6434. } catch ( e ) {
  6435. throw "Bad cue";
  6436. }
  6437. return time;
  6438. }
  6439. function createTrack( name, attributes ) {
  6440. var track = {};
  6441. track[ name ] = attributes;
  6442. return track;
  6443. }
  6444. function parseCueHeader ( line ) {
  6445. var lineSegments,
  6446. args,
  6447. sub = {},
  6448. rToken = /-->/,
  6449. rWhitespace = /[\t ]+/;
  6450. if ( !line || line.indexOf( "-->" ) === -1 ) {
  6451. throw "Bad cue";
  6452. }
  6453. lineSegments = line.replace( rToken, " --> " ).split( rWhitespace );
  6454. if ( lineSegments.length < 2 ) {
  6455. throw "Bad cue";
  6456. }
  6457. sub.id = line;
  6458. sub.start = toSeconds( lineSegments[ 0 ] );
  6459. sub.end = toSeconds( lineSegments[ 2 ] );
  6460. return sub;
  6461. }
  6462. function skipWhitespace ( lines, len, i ) {
  6463. while ( i < len && !lines[ i ] ) {
  6464. i++;
  6465. }
  6466. return i;
  6467. }
  6468. function skipNonWhitespace ( lines, len, i ) {
  6469. while ( i < len && lines[ i ] ) {
  6470. i++;
  6471. }
  6472. return i;
  6473. }
  6474. })( Popcorn );
  6475. // PARSER: 0.1 XML
  6476. (function (Popcorn) {
  6477. /**
  6478. *
  6479. *
  6480. */
  6481. Popcorn.parser( "parseXML", "XML", function( data ) {
  6482. // declare needed variables
  6483. var returnData = {
  6484. title: "",
  6485. remote: "",
  6486. data: []
  6487. },
  6488. manifestData = {};
  6489. // Simple function to convert 0:05 to 0.5 in seconds
  6490. // acceptable formats are HH:MM:SS:MM, MM:SS:MM, SS:MM, SS
  6491. var toSeconds = function(time) {
  6492. var t = time.split(":");
  6493. if (t.length === 1) {
  6494. return parseFloat(t[0], 10);
  6495. } else if (t.length === 2) {
  6496. return parseFloat(t[0], 10) + parseFloat(t[1] / 12, 10);
  6497. } else if (t.length === 3) {
  6498. return parseInt(t[0] * 60, 10) + parseFloat(t[1], 10) + parseFloat(t[2] / 12, 10);
  6499. } else if (t.length === 4) {
  6500. return parseInt(t[0] * 3600, 10) + parseInt(t[1] * 60, 10) + parseFloat(t[2], 10) + parseFloat(t[3] / 12, 10);
  6501. }
  6502. };
  6503. // turns a node tree element into a straight up javascript object
  6504. // also converts in and out to start and end
  6505. // also links manifest data with ids
  6506. var objectifyAttributes = function ( nodeAttributes ) {
  6507. var returnObject = {};
  6508. for ( var i = 0, nal = nodeAttributes.length; i < nal; i++ ) {
  6509. var key = nodeAttributes.item(i).nodeName,
  6510. data = nodeAttributes.item(i).nodeValue,
  6511. manifestItem = manifestData[ data ];
  6512. // converts in into start
  6513. if (key === "in") {
  6514. returnObject.start = toSeconds( data );
  6515. // converts out into end
  6516. } else if ( key === "out" ){
  6517. returnObject.end = toSeconds( data );
  6518. // this is where ids in the manifest are linked
  6519. } else if ( key === "resourceid" ) {
  6520. for ( var item in manifestItem ) {
  6521. if ( manifestItem.hasOwnProperty( item ) ) {
  6522. if ( !returnObject[ item ] && item !== "id" ) {
  6523. returnObject[ item ] = manifestItem[ item ];
  6524. }
  6525. }
  6526. }
  6527. // everything else
  6528. } else {
  6529. returnObject[key] = data;
  6530. }
  6531. }
  6532. return returnObject;
  6533. };
  6534. // creates an object of all atrributes keyd by name
  6535. var createTrack = function( name, attributes ) {
  6536. var track = {};
  6537. track[name] = attributes;
  6538. return track;
  6539. };
  6540. // recursive function to process a node, or process the next child node
  6541. var parseNode = function ( node, allAttributes, manifest ) {
  6542. var attributes = {};
  6543. Popcorn.extend( attributes, allAttributes, objectifyAttributes( node.attributes ), { text: node.textContent || node.text } );
  6544. var childNodes = node.childNodes;
  6545. // processes the node
  6546. if ( childNodes.length < 1 || ( childNodes.length === 1 && childNodes[0].nodeType === 3 ) ) {
  6547. if ( !manifest ) {
  6548. returnData.data.push( createTrack( node.nodeName, attributes ) );
  6549. } else {
  6550. manifestData[attributes.id] = attributes;
  6551. }
  6552. // process the next child node
  6553. } else {
  6554. for ( var i = 0; i < childNodes.length; i++ ) {
  6555. if ( childNodes[i].nodeType === 1 ) {
  6556. parseNode( childNodes[i], attributes, manifest );
  6557. }
  6558. }
  6559. }
  6560. };
  6561. // this is where things actually start
  6562. var x = data.documentElement.childNodes;
  6563. for ( var i = 0, xl = x.length; i < xl; i++ ) {
  6564. if ( x[i].nodeType === 1 ) {
  6565. // start the process of each main node type, manifest or timeline
  6566. if ( x[i].nodeName === "manifest" ) {
  6567. parseNode( x[i], {}, true );
  6568. } else { // timeline
  6569. parseNode( x[i], {}, false );
  6570. }
  6571. }
  6572. }
  6573. return returnData;
  6574. });
  6575. })( Popcorn );
  6576. (function() {
  6577. var scriptLoaded = false,
  6578. loading = false;
  6579. Popcorn.player( "soundcloud", {
  6580. _canPlayType: function( nodeName, url ) {
  6581. return (/(?:http:\/\/www\.|http:\/\/|www\.|\.|^)(soundcloud)/).test( url ) && nodeName.toLowerCase() !== "video";
  6582. },
  6583. _setup: function( options ) {
  6584. var media = this,
  6585. container = document.createElement( "iframe" ),
  6586. lastVolume = 1,
  6587. currentTime = 0,
  6588. paused = true,
  6589. realPaused = true,
  6590. widget,
  6591. duration = 0,
  6592. muted = false,
  6593. playerQueue = Popcorn.player.playerQueue();
  6594. options._container = container;
  6595. media.style.visibility = "hidden";
  6596. media.play = function() {
  6597. paused = false;
  6598. playerQueue.add(function() {
  6599. if ( realPaused ) {
  6600. widget && widget.play();
  6601. } else {
  6602. playerQueue.next();
  6603. }
  6604. });
  6605. };
  6606. media.pause = function() {
  6607. paused = true;
  6608. playerQueue.add(function() {
  6609. if ( !realPaused ) {
  6610. widget && widget.pause();
  6611. } else {
  6612. playerQueue.next();
  6613. }
  6614. });
  6615. };
  6616. // getter and setter for muted property, multiply volume by 100 as that is the scale soundcloud works on
  6617. Object.defineProperties( media, {
  6618. muted: {
  6619. set: function( val ) {
  6620. if ( val ) {
  6621. widget && widget.getVolume(function( data ) {
  6622. lastVolume = data / 100;
  6623. });
  6624. widget && widget.setVolume( 0 );
  6625. muted = true;
  6626. } else {
  6627. widget && widget.setVolume( lastVolume * 100 );
  6628. muted = false;
  6629. }
  6630. media.dispatchEvent( "volumechange" );
  6631. },
  6632. get: function() {
  6633. return muted;
  6634. }
  6635. },
  6636. volume: {
  6637. set: function( val ) {
  6638. widget && widget.setVolume( val * 100 );
  6639. lastVolume = val ;
  6640. media.dispatchEvent( "volumechange" );
  6641. },
  6642. get: function() {
  6643. return muted ? 0 : lastVolume;
  6644. }
  6645. },
  6646. currentTime: {
  6647. set: function( val ) {
  6648. currentTime = val;
  6649. widget && widget.seekTo( val * 1000 );
  6650. media.dispatchEvent( "seeked" );
  6651. media.dispatchEvent( "timeupdate" );
  6652. },
  6653. get: function() {
  6654. return currentTime;
  6655. }
  6656. },
  6657. duration: {
  6658. get: function() {
  6659. return duration;
  6660. }
  6661. },
  6662. paused: {
  6663. get: function() {
  6664. return paused;
  6665. }
  6666. }
  6667. });
  6668. // called when the SoundCloud api script has loaded
  6669. function scriptReady() {
  6670. scriptLoaded = true;
  6671. SC.initialize({
  6672. client_id: "PRaNFlda6Bhf5utPjUsptg"
  6673. });
  6674. SC.get( "/resolve", {
  6675. url: media.src
  6676. }, function( data ) {
  6677. media.width = media.style.width ? "" + media.offsetWidth : "560";
  6678. media.height = media.style.height ? "" + media.offsetHeight : "315";
  6679. // TODO: There are quite a few options here that we should pass on to the user
  6680. container.scrolling = "no";
  6681. container.frameborder = "no";
  6682. container.id = "soundcloud-" + Popcorn.guid();
  6683. container.src = "http://w.soundcloud.com/player/?url=" + data.uri +
  6684. "&show_artwork=false" +
  6685. "&buying=false" +
  6686. "&liking=false" +
  6687. "&sharing=false";
  6688. container.width = "100%";
  6689. container.height = "100%";
  6690. options.loadListener = function( e ) {
  6691. options.widget = widget = SC.Widget( container.id );
  6692. // setup all of our listeners
  6693. widget.bind(SC.Widget.Events.FINISH, function() {
  6694. media.pause();
  6695. media.dispatchEvent( "ended" );
  6696. });
  6697. widget.bind(SC.Widget.Events.PLAY_PROGRESS, function( data ) {
  6698. currentTime = data.currentPosition / 1000;
  6699. media.dispatchEvent( "timeupdate" );
  6700. });
  6701. widget.bind(SC.Widget.Events.PLAY, function( data ) {
  6702. paused = realPaused = false;
  6703. media.dispatchEvent( "play" );
  6704. media.dispatchEvent( "playing" );
  6705. media.currentTime = currentTime;
  6706. playerQueue.next();
  6707. });
  6708. widget.bind(SC.Widget.Events.PAUSE, function( data ) {
  6709. paused = realPaused = true;
  6710. media.dispatchEvent( "pause" );
  6711. playerQueue.next();
  6712. });
  6713. widget.bind(SC.Widget.Events.READY, function( data ) {
  6714. widget.getDuration(function( data ) {
  6715. duration = data / 1000;
  6716. media.style.visibility = "visible";
  6717. media.dispatchEvent( "durationchange" );
  6718. // update the readyState after we have the duration
  6719. media.readyState = 4;
  6720. media.dispatchEvent( "readystatechange" );
  6721. media.dispatchEvent( "loadedmetadata" );
  6722. media.dispatchEvent( "loadeddata" );
  6723. media.dispatchEvent( "canplaythrough" );
  6724. media.dispatchEvent( "load" );
  6725. !media.paused && media.play();
  6726. });
  6727. widget.getVolume(function( data ) {
  6728. lastVolume = data / 100;
  6729. });
  6730. });
  6731. };
  6732. container.addEventListener( "load", options.loadListener, false );
  6733. media.appendChild( container );
  6734. });
  6735. }
  6736. // load the SoundCloud API script if it doesn't exist
  6737. function loadScript() {
  6738. if ( !loading ) {
  6739. loading = true;
  6740. Popcorn.getScript( "http://w.soundcloud.com/player/api.js", function() {
  6741. Popcorn.getScript( "http://connect.soundcloud.com/sdk.js", function() {
  6742. scriptReady();
  6743. });
  6744. });
  6745. } else {
  6746. (function isReady() {
  6747. setTimeout(function() {
  6748. if ( !scriptLoaded ) {
  6749. isReady();
  6750. } else {
  6751. scriptReady();
  6752. }
  6753. }, 100 );
  6754. })();
  6755. }
  6756. }
  6757. if ( !scriptLoaded ) {
  6758. loadScript();
  6759. } else {
  6760. scriptReady();
  6761. }
  6762. },
  6763. _teardown: function( options ) {
  6764. var widget = options.widget,
  6765. events = SC.Widget.Events,
  6766. container = options._container,
  6767. parentContainer = container.parentNode;
  6768. options.destroyed = true;
  6769. // if the widget never got setup, remove the containers load listener and return
  6770. if ( !widget ) {
  6771. container.removeEventListener( "load", options.loadEventListener, false );
  6772. return;
  6773. }
  6774. // remove all bound soundcloud listeners
  6775. for ( var prop in events ) {
  6776. widget && widget.unbind( events[ prop ] );
  6777. }
  6778. }
  6779. });
  6780. })();
  6781. (function() {
  6782. // parseUri 1.2.2
  6783. // http://blog.stevenlevithan.com/archives/parseuri
  6784. // (c) Steven Levithan <stevenlevithan.com>
  6785. // MIT License
  6786. function parseUri (str) {
  6787. var o = parseUri.options,
  6788. m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
  6789. uri = {},
  6790. i = 14;
  6791. while (i--) {
  6792. uri[o.key[i]] = m[i] || "";
  6793. }
  6794. uri[o.q.name] = {};
  6795. uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
  6796. if ($1) {
  6797. uri[o.q.name][$1] = $2;
  6798. }
  6799. });
  6800. return uri;
  6801. }
  6802. parseUri.options = {
  6803. strictMode: false,
  6804. key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
  6805. q: {
  6806. name: "queryKey",
  6807. parser: /(?:^|&)([^&=]*)=?([^&]*)/g
  6808. },
  6809. parser: {
  6810. strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
  6811. loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
  6812. }
  6813. };
  6814. function canPlayType( nodeName, url ) {
  6815. return ( /player.vimeo.com\/video\/\d+/ ).test( url ) ||
  6816. ( /vimeo.com\/\d+/ ).test( url );
  6817. }
  6818. Popcorn.player( "vimeo", {
  6819. _canPlayType: canPlayType,
  6820. _setup: function( options ) {
  6821. var TIMEUPDATE_INTERVAL_MS = 250,
  6822. CURRENT_TIME_MONITOR_MS = 16,
  6823. MediaErrorInterface = {
  6824. MEDIA_ERR_ABORTED: 1,
  6825. MEDIA_ERR_NETWORK: 2,
  6826. MEDIA_ERR_DECODE: 3,
  6827. MEDIA_ERR_SRC_NOT_SUPPORTED: 4
  6828. },
  6829. guid,
  6830. media = this,
  6831. commands = {
  6832. q: [],
  6833. queue: function queue( fn ) {
  6834. this.q.push( fn );
  6835. this.process();
  6836. },
  6837. process: function process() {
  6838. if ( !vimeoReady ) {
  6839. return;
  6840. }
  6841. while ( this.q.length ) {
  6842. var fn = this.q.shift();
  6843. fn();
  6844. }
  6845. }
  6846. },
  6847. currentTimeId,
  6848. timeUpdateId,
  6849. vimeoReady,
  6850. vimeoContainer = document.createElement( "iframe" ),
  6851. // Loosely based on HTMLMediaElement + HTMLVideoElement IDL
  6852. impl = {
  6853. // error state
  6854. error: null,
  6855. // network state
  6856. src: media.src,
  6857. NETWORK_EMPTY: 0,
  6858. NETWORK_IDLE: 1,
  6859. NETWORK_LOADING: 2,
  6860. NETWORK_NO_SOURCE: 3,
  6861. networkState: 0,
  6862. // ready state
  6863. HAVE_NOTHING: 0,
  6864. HAVE_METADATA: 1,
  6865. HAVE_CURRENT_DATA: 2,
  6866. HAVE_FUTURE_DATA: 3,
  6867. HAVE_ENOUGH_DATA: 4,
  6868. readyState: 0,
  6869. seeking: false,
  6870. // playback state
  6871. currentTime: 0,
  6872. duration: NaN,
  6873. paused: true,
  6874. ended: false,
  6875. autoplay: false,
  6876. loop: false,
  6877. // controls
  6878. volume: 1,
  6879. muted: false,
  6880. // Video attributes
  6881. width: 0,
  6882. height: 0
  6883. };
  6884. var readOnlyAttrs = "error networkState readyState seeking duration paused ended";
  6885. Popcorn.forEach( readOnlyAttrs.split(" "), function( value ) {
  6886. Object.defineProperty( media, value, {
  6887. get: function() {
  6888. return impl[ value ];
  6889. }
  6890. });
  6891. });
  6892. Object.defineProperties( media, {
  6893. "src": {
  6894. get: function() {
  6895. return impl.src;
  6896. },
  6897. set: function( value ) {
  6898. // Is there any sort of logic that determines whether to load the video or not?
  6899. impl.src = value;
  6900. media.load();
  6901. }
  6902. },
  6903. "currentTime": {
  6904. get: function() {
  6905. return impl.currentTime;
  6906. },
  6907. set: function( value ) {
  6908. commands.queue(function() {
  6909. sendMessage( "seekTo", value );
  6910. });
  6911. impl.seeking = true;
  6912. media.dispatchEvent( "seeking" );
  6913. }
  6914. },
  6915. "autoplay": {
  6916. get: function() {
  6917. return impl.autoplay;
  6918. },
  6919. set: function( value ) {
  6920. impl.autoplay = !!value;
  6921. }
  6922. },
  6923. "loop": {
  6924. get: function() {
  6925. return impl.loop;
  6926. },
  6927. set: function( value) {
  6928. impl.loop = !!value;
  6929. commands.queue(function() {
  6930. sendMessage( "setLoop", loop );
  6931. });
  6932. }
  6933. },
  6934. "volume": {
  6935. get: function() {
  6936. return impl.volume;
  6937. },
  6938. set: function( value ) {
  6939. impl.volume = value;
  6940. commands.queue(function() {
  6941. sendMessage( "setVolume", impl.muted ? 0 : impl.volume );
  6942. });
  6943. media.dispatchEvent( "volumechange" );
  6944. }
  6945. },
  6946. "muted": {
  6947. get: function() {
  6948. return impl.muted;
  6949. },
  6950. set: function( value ) {
  6951. impl.muted = !!value;
  6952. commands.queue(function() {
  6953. sendMessage( "setVolume", impl.muted ? 0 : impl.volume );
  6954. });
  6955. media.dispatchEvent( "volumechange" );
  6956. }
  6957. },
  6958. "width": {
  6959. get: function() {
  6960. return vimeoContainer.width;
  6961. },
  6962. set: function( value ) {
  6963. vimeoContainer.width = value;
  6964. }
  6965. },
  6966. "height": {
  6967. get: function() {
  6968. return vimeoContainer.height;
  6969. },
  6970. set: function( value ) {
  6971. vimeoContainer.height = value;
  6972. }
  6973. }
  6974. });
  6975. function sendMessage( method, params ) {
  6976. var url = vimeoContainer.src.split( "?" )[ 0 ],
  6977. data = JSON.stringify({
  6978. method: method,
  6979. value: params
  6980. });
  6981. if ( url.substr( 0, 2 ) === "//" ) {
  6982. url = window.location.protocol + url;
  6983. }
  6984. // The iframe has been destroyed, it just doesn't know it
  6985. if ( !vimeoContainer.contentWindow ) {
  6986. media.unload();
  6987. return;
  6988. }
  6989. vimeoContainer.contentWindow.postMessage( data, url );
  6990. }
  6991. var vimeoAPIMethods = {
  6992. "getCurrentTime": function( data ) {
  6993. impl.currentTime = parseFloat( data.value );
  6994. },
  6995. "getDuration": function( data ) {
  6996. impl.duration = parseFloat( data.value );
  6997. maybeReady();
  6998. },
  6999. "getVolume": function( data ) {
  7000. impl.volume = parseFloat( data.value );
  7001. }
  7002. };
  7003. var vimeoAPIEvents = {
  7004. "ready": function( data ) {
  7005. sendMessage( "addEventListener", "loadProgress" );
  7006. sendMessage( "addEventListener", "playProgress" );
  7007. sendMessage( "addEventListener", "play" );
  7008. sendMessage( "addEventListener", "pause" );
  7009. sendMessage( "addEventListener", "finish" );
  7010. sendMessage( "addEventListener", "seek" );
  7011. sendMessage( "getDuration" );
  7012. vimeoReady = true;
  7013. commands.process();
  7014. media.dispatchEvent( "loadstart" );
  7015. },
  7016. "loadProgress": function( data ) {
  7017. media.dispatchEvent( "progress" );
  7018. // loadProgress has a more accurate duration than getDuration
  7019. impl.duration = parseFloat( data.data.duration );
  7020. },
  7021. "playProgress": function( data ) {
  7022. impl.currentTime = parseFloat( data.data.seconds );
  7023. },
  7024. "play": function( data ) {
  7025. // Vimeo plays video if seeking from an unloaded state
  7026. if ( impl.seeking ) {
  7027. impl.seeking = false;
  7028. media.dispatchEvent( "seeked" );
  7029. }
  7030. impl.paused = false;
  7031. impl.ended = false;
  7032. startUpdateLoops();
  7033. media.dispatchEvent( "play" );
  7034. },
  7035. "pause": function( data ) {
  7036. impl.paused = true;
  7037. stopUpdateLoops();
  7038. media.dispatchEvent( "pause" );
  7039. },
  7040. "finish": function( data ) {
  7041. impl.ended = true;
  7042. stopUpdateLoops();
  7043. media.dispatchEvent( "ended" );
  7044. },
  7045. "seek": function( data ) {
  7046. impl.currentTime = parseFloat( data.data.seconds );
  7047. impl.seeking = false;
  7048. impl.ended = false;
  7049. media.dispatchEvent( "timeupdate" );
  7050. media.dispatchEvent( "seeked" );
  7051. }
  7052. };
  7053. function messageListener( event ) {
  7054. if ( event.origin !== "http://player.vimeo.com" ) {
  7055. return;
  7056. }
  7057. var data;
  7058. try {
  7059. data = JSON.parse( event.data );
  7060. } catch ( ex ) {
  7061. console.warn( ex );
  7062. }
  7063. if ( data.player_id != guid ) {
  7064. return;
  7065. }
  7066. // Methods
  7067. if ( data.method && vimeoAPIMethods[ data.method ] ) {
  7068. vimeoAPIMethods[ data.method ]( data );
  7069. }
  7070. // Events
  7071. if ( data.event && vimeoAPIEvents[ data.event ] ) {
  7072. vimeoAPIEvents[ data.event ]( data );
  7073. }
  7074. }
  7075. media.load = function() {
  7076. vimeoReady = false;
  7077. guid = Popcorn.guid();
  7078. var src = parseUri( impl.src ),
  7079. combinedOptions = {},
  7080. optionsArray = [],
  7081. vimeoAPIOptions = {
  7082. api: 1,
  7083. player_id: guid
  7084. };
  7085. if ( !canPlayType( media.nodeName, src.source ) ) {
  7086. setErrorAttr( impl.MEDIA_ERR_SRC_NOT_SUPPORTED );
  7087. return;
  7088. }
  7089. // Add Popcorn ctor options, url options, then the Vimeo API options
  7090. Popcorn.extend( combinedOptions, options );
  7091. Popcorn.extend( combinedOptions, src.queryKey );
  7092. Popcorn.extend( combinedOptions, vimeoAPIOptions );
  7093. // Create the base vimeo player string. It will always have query string options
  7094. src = "http://player.vimeo.com/video/" + ( /\d+$/ ).exec( src.path ) + "?";
  7095. for ( var key in combinedOptions ) {
  7096. if ( combinedOptions.hasOwnProperty( key ) ) {
  7097. optionsArray.push( encodeURIComponent( key ) + "=" + encodeURIComponent( combinedOptions[ key ] ) );
  7098. }
  7099. }
  7100. src += optionsArray.join( "&" );
  7101. impl.loop = !!src.match( /loop=1/ );
  7102. impl.autoplay = !!src.match( /autoplay=1/ );
  7103. vimeoContainer.width = media.style.width ? media.style.width : 500;
  7104. vimeoContainer.height = media.style.height ? media.style.height : 281;
  7105. vimeoContainer.frameBorder = 0;
  7106. vimeoContainer.webkitAllowFullScreen = true;
  7107. vimeoContainer.mozAllowFullScreen = true;
  7108. vimeoContainer.allowFullScreen = true;
  7109. vimeoContainer.src = src;
  7110. media.appendChild( vimeoContainer );
  7111. };
  7112. function setErrorAttr( value ) {
  7113. impl.error = {};
  7114. Popcorn.extend( impl.error, MediaErrorInterface );
  7115. impl.error.code = value;
  7116. media.dispatchEvent( "error" );
  7117. }
  7118. function maybeReady() {
  7119. if ( !isNaN( impl.duration ) ) {
  7120. impl.readyState = 4;
  7121. media.dispatchEvent( "durationchange" );
  7122. media.dispatchEvent( "loadedmetadata" );
  7123. media.dispatchEvent( "loadeddata" );
  7124. media.dispatchEvent( "canplay" );
  7125. media.dispatchEvent( "canplaythrough" );
  7126. }
  7127. }
  7128. function startUpdateLoops() {
  7129. if ( !timeUpdateId ) {
  7130. timeUpdateId = setInterval(function() {
  7131. media.dispatchEvent( "timeupdate" );
  7132. }, TIMEUPDATE_INTERVAL_MS );
  7133. }
  7134. if ( !currentTimeId ) {
  7135. currentTimeId = setInterval(function() {
  7136. sendMessage( "getCurrentTime" );
  7137. }, CURRENT_TIME_MONITOR_MS );
  7138. }
  7139. }
  7140. function stopUpdateLoops() {
  7141. if ( timeUpdateId ) {
  7142. clearInterval( timeUpdateId );
  7143. timeUpdateId = 0;
  7144. }
  7145. if ( currentTimeId ) {
  7146. clearInterval( currentTimeId );
  7147. currentTimeId = 0;
  7148. }
  7149. }
  7150. media.unload = function() {
  7151. stopUpdateLoops();
  7152. window.removeEventListener( "message", messageListener, false );
  7153. };
  7154. media.play = function() {
  7155. commands.queue(function() {
  7156. sendMessage( "play" );
  7157. });
  7158. };
  7159. media.pause = function() {
  7160. commands.queue(function() {
  7161. sendMessage( "pause" );
  7162. });
  7163. };
  7164. // Start the load process now, players behave like `preload="metadata"` is set
  7165. // Do it asynchronously so that users can attach event listeners
  7166. setTimeout(function() {
  7167. window.addEventListener( "message", messageListener, false );
  7168. media.load();
  7169. }, 0 );
  7170. },
  7171. _teardown: function( options ) {
  7172. // If the baseplayer doesn't call _setup
  7173. if ( this.unload ) {
  7174. this.unload();
  7175. }
  7176. }
  7177. });
  7178. })();
  7179. (function( window, Popcorn ) {
  7180. // A global callback for youtube... that makes me angry
  7181. window.onYouTubePlayerAPIReady = function() {
  7182. onYouTubePlayerAPIReady.ready = true;
  7183. for ( var i = 0; i < onYouTubePlayerAPIReady.waiting.length; i++ ) {
  7184. onYouTubePlayerAPIReady.waiting[ i ]();
  7185. }
  7186. };
  7187. // existing youtube references can break us.
  7188. // remove it and use the one we can trust.
  7189. if ( window.YT ) {
  7190. window.quarantineYT = window.YT;
  7191. window.YT = null;
  7192. }
  7193. onYouTubePlayerAPIReady.waiting = [];
  7194. var _loading = false;
  7195. Popcorn.player( "youtube", {
  7196. _canPlayType: function( nodeName, url ) {
  7197. return typeof url === "string" && (/(?:http:\/\/www\.|http:\/\/|www\.|\.|^)(youtu)/).test( url ) && nodeName.toLowerCase() !== "video";
  7198. },
  7199. _setup: function( options ) {
  7200. if ( !window.YT && !_loading ) {
  7201. _loading = true;
  7202. Popcorn.getScript( "//youtube.com/player_api" );
  7203. }
  7204. var media = this,
  7205. autoPlay = false,
  7206. container = document.createElement( "div" ),
  7207. currentTime = 0,
  7208. paused = true,
  7209. seekTime = 0,
  7210. firstGo = true,
  7211. seeking = false,
  7212. fragmentStart = 0,
  7213. // state code for volume changed polling
  7214. lastMuted = false,
  7215. lastVolume = 100,
  7216. playerQueue = Popcorn.player.playerQueue();
  7217. var createProperties = function() {
  7218. Popcorn.player.defineProperty( media, "currentTime", {
  7219. set: function( val ) {
  7220. if ( options.destroyed ) {
  7221. return;
  7222. }
  7223. seeking = true;
  7224. // make sure val is a number
  7225. currentTime = Math.round( +val * 100 ) / 100;
  7226. },
  7227. get: function() {
  7228. return currentTime;
  7229. }
  7230. });
  7231. Popcorn.player.defineProperty( media, "paused", {
  7232. get: function() {
  7233. return paused;
  7234. }
  7235. });
  7236. Popcorn.player.defineProperty( media, "muted", {
  7237. set: function( val ) {
  7238. if ( options.destroyed ) {
  7239. return val;
  7240. }
  7241. if ( options.youtubeObject.isMuted() !== val ) {
  7242. if ( val ) {
  7243. options.youtubeObject.mute();
  7244. } else {
  7245. options.youtubeObject.unMute();
  7246. }
  7247. lastMuted = options.youtubeObject.isMuted();
  7248. media.dispatchEvent( "volumechange" );
  7249. }
  7250. return options.youtubeObject.isMuted();
  7251. },
  7252. get: function() {
  7253. if ( options.destroyed ) {
  7254. return 0;
  7255. }
  7256. return options.youtubeObject.isMuted();
  7257. }
  7258. });
  7259. Popcorn.player.defineProperty( media, "volume", {
  7260. set: function( val ) {
  7261. if ( options.destroyed ) {
  7262. return val;
  7263. }
  7264. if ( options.youtubeObject.getVolume() / 100 !== val ) {
  7265. options.youtubeObject.setVolume( val * 100 );
  7266. lastVolume = options.youtubeObject.getVolume();
  7267. media.dispatchEvent( "volumechange" );
  7268. }
  7269. return options.youtubeObject.getVolume() / 100;
  7270. },
  7271. get: function() {
  7272. if ( options.destroyed ) {
  7273. return 0;
  7274. }
  7275. return options.youtubeObject.getVolume() / 100;
  7276. }
  7277. });
  7278. media.play = function() {
  7279. if ( options.destroyed ) {
  7280. return;
  7281. }
  7282. paused = false;
  7283. playerQueue.add(function() {
  7284. if ( options.youtubeObject.getPlayerState() !== 1 ) {
  7285. seeking = false;
  7286. options.youtubeObject.playVideo();
  7287. } else {
  7288. playerQueue.next();
  7289. }
  7290. });
  7291. };
  7292. media.pause = function() {
  7293. if ( options.destroyed ) {
  7294. return;
  7295. }
  7296. paused = true;
  7297. playerQueue.add(function() {
  7298. if ( options.youtubeObject.getPlayerState() !== 2 ) {
  7299. options.youtubeObject.pauseVideo();
  7300. } else {
  7301. playerQueue.next();
  7302. }
  7303. });
  7304. };
  7305. };
  7306. container.id = media.id + Popcorn.guid();
  7307. options._container = container;
  7308. media.appendChild( container );
  7309. var youtubeInit = function() {
  7310. var src, query, params, playerVars, queryStringItem, firstPlay = true;
  7311. var timeUpdate = function() {
  7312. if ( options.destroyed ) {
  7313. return;
  7314. }
  7315. if ( !seeking ) {
  7316. currentTime = options.youtubeObject.getCurrentTime();
  7317. media.dispatchEvent( "timeupdate" );
  7318. } else if ( currentTime === options.youtubeObject.getCurrentTime() ) {
  7319. seeking = false;
  7320. media.dispatchEvent( "seeked" );
  7321. media.dispatchEvent( "timeupdate" );
  7322. } else {
  7323. // keep trying the seek until it is right.
  7324. options.youtubeObject.seekTo( currentTime );
  7325. }
  7326. setTimeout( timeUpdate, 250 );
  7327. };
  7328. // delay is in seconds
  7329. var fetchDuration = function( delay ) {
  7330. var ytDuration = options.youtubeObject.getDuration();
  7331. if ( isNaN( ytDuration ) || ytDuration === 0 ) {
  7332. setTimeout( function() {
  7333. fetchDuration( delay * 2 );
  7334. }, delay*1000 );
  7335. } else {
  7336. // set duration and dispatch ready events
  7337. media.duration = ytDuration;
  7338. media.dispatchEvent( "durationchange" );
  7339. media.dispatchEvent( "loadedmetadata" );
  7340. media.dispatchEvent( "loadeddata" );
  7341. media.readyState = 4;
  7342. timeUpdate();
  7343. media.dispatchEvent( "canplaythrough" );
  7344. }
  7345. };
  7346. options.controls = +options.controls === 0 || +options.controls === 1 ? options.controls : 1;
  7347. options.annotations = +options.annotations === 1 || +options.annotations === 3 ? options.annotations : 1;
  7348. src = /^.*(?:\/|v=)(.{11})/.exec( media.src )[ 1 ];
  7349. query = ( media.src.split( "?" )[ 1 ] || "" )
  7350. .replace( /v=.{11}/, "" );
  7351. query = query.replace( /&t=(?:(\d+)m)?(?:(\d+)s)?/, function( all, minutes, seconds ) {
  7352. // Make sure we have real zeros
  7353. minutes = minutes | 0; // bit-wise OR
  7354. seconds = seconds | 0; // bit-wise OR
  7355. fragmentStart = ( +seconds + ( minutes * 60 ) );
  7356. return "";
  7357. });
  7358. query = query.replace( /&start=(\d+)?/, function( all, seconds ) {
  7359. // Make sure we have real zeros
  7360. seconds = seconds | 0; // bit-wise OR
  7361. fragmentStart = seconds;
  7362. return "";
  7363. });
  7364. autoPlay = ( /autoplay=1/.test( query ) );
  7365. params = query.split( /[\&\?]/g );
  7366. playerVars = { wmode: "transparent" };
  7367. for( var i = 0; i < params.length; i++ ) {
  7368. queryStringItem = params[ i ].split( "=" );
  7369. playerVars[ queryStringItem[ 0 ] ] = queryStringItem[ 1 ];
  7370. }
  7371. options.youtubeObject = new YT.Player( container.id, {
  7372. height: "100%",
  7373. width: "100%",
  7374. wmode: "transparent",
  7375. playerVars: playerVars,
  7376. videoId: src,
  7377. events: {
  7378. "onReady": function(){
  7379. // pulling initial volume states form baseplayer
  7380. lastVolume = media.volume;
  7381. lastMuted = media.muted;
  7382. volumeupdate();
  7383. paused = media.paused;
  7384. createProperties();
  7385. options.youtubeObject.playVideo();
  7386. media.currentTime = fragmentStart;
  7387. // wait to dispatch ready events until we get a duration
  7388. },
  7389. "onStateChange": function( state ){
  7390. if ( options.destroyed || state.data === -1 ) {
  7391. return;
  7392. }
  7393. // state.data === 2 is for pause events
  7394. // state.data === 1 is for play events
  7395. if ( state.data === 2 ) {
  7396. paused = true;
  7397. media.dispatchEvent( "pause" );
  7398. playerQueue.next();
  7399. } else if ( state.data === 1 && !firstPlay ) {
  7400. paused = false;
  7401. media.dispatchEvent( "play" );
  7402. media.dispatchEvent( "playing" );
  7403. playerQueue.next();
  7404. } else if ( state.data === 0 ) {
  7405. media.dispatchEvent( "ended" );
  7406. } else if ( state.data === 1 && firstPlay ) {
  7407. firstPlay = false;
  7408. // pulling initial paused state from autoplay or the baseplayer
  7409. // also need to explicitly set to paused otherwise.
  7410. if ( autoPlay || !media.paused ) {
  7411. paused = false;
  7412. }
  7413. if ( paused ) {
  7414. options.youtubeObject.pauseVideo();
  7415. }
  7416. fetchDuration( 0.025 );
  7417. }
  7418. },
  7419. "onError": function( error ) {
  7420. if ( [ 2, 100, 101, 150 ].indexOf( error.data ) !== -1 ) {
  7421. media.error = {
  7422. customCode: error.data
  7423. };
  7424. media.dispatchEvent( "error" );
  7425. }
  7426. }
  7427. }
  7428. });
  7429. var volumeupdate = function() {
  7430. if ( options.destroyed ) {
  7431. return;
  7432. }
  7433. if ( lastMuted !== options.youtubeObject.isMuted() ) {
  7434. lastMuted = options.youtubeObject.isMuted();
  7435. media.dispatchEvent( "volumechange" );
  7436. }
  7437. if ( lastVolume !== options.youtubeObject.getVolume() ) {
  7438. lastVolume = options.youtubeObject.getVolume();
  7439. media.dispatchEvent( "volumechange" );
  7440. }
  7441. setTimeout( volumeupdate, 250 );
  7442. };
  7443. };
  7444. if ( onYouTubePlayerAPIReady.ready ) {
  7445. youtubeInit();
  7446. } else {
  7447. onYouTubePlayerAPIReady.waiting.push( youtubeInit );
  7448. }
  7449. },
  7450. _teardown: function( options ) {
  7451. options.destroyed = true;
  7452. var youtubeObject = options.youtubeObject;
  7453. if( youtubeObject ){
  7454. youtubeObject.stopVideo();
  7455. youtubeObject.clearVideo && youtubeObject.clearVideo();
  7456. }
  7457. this.removeChild( document.getElementById( options._container.id ) );
  7458. }
  7459. });
  7460. }( window, Popcorn ));
  7461. // EFFECT: applyclass
  7462. (function (Popcorn) {
  7463. /**
  7464. * apply css class to jquery selector
  7465. * selector is relative to plugin target's id
  7466. * so .overlay is actually jQuery( "#target .overlay")
  7467. *
  7468. * @param {Object} options
  7469. *
  7470. * Example:
  7471. var p = Popcorn('#video')
  7472. .footnote({
  7473. start: 5, // seconds
  7474. end: 15, // seconds
  7475. text: 'This video made exclusively for drumbeat.org',
  7476. target: 'footnotediv',
  7477. effect: 'applyclass',
  7478. applyclass: 'selector: class'
  7479. })
  7480. *
  7481. */
  7482. var toggleClass = function( event, options ) {
  7483. var idx = 0, len = 0, elements;
  7484. Popcorn.forEach( options.classes, function( key, val ) {
  7485. elements = [];
  7486. if ( key === "parent" ) {
  7487. elements[ 0 ] = document.querySelectorAll("#" + options.target )[ 0 ].parentNode;
  7488. } else {
  7489. elements = document.querySelectorAll("#" + options.target + " " + key );
  7490. }
  7491. for ( idx = 0, len = elements.length; idx < len; idx++ ) {
  7492. elements[ idx ].classList.toggle( val );
  7493. }
  7494. });
  7495. };
  7496. Popcorn.compose( "applyclass", {
  7497. manifest: {
  7498. about: {
  7499. name: "Popcorn applyclass Effect",
  7500. version: "0.1",
  7501. author: "@scottdowne",
  7502. website: "scottdowne.wordpress.com"
  7503. },
  7504. options: {}
  7505. },
  7506. _setup: function( options ) {
  7507. options.classes = {};
  7508. options.applyclass = options.applyclass || "";
  7509. var classes = options.applyclass.replace( /\s/g, "" ).split( "," ),
  7510. item = [],
  7511. idx = 0, len = classes.length;
  7512. for ( ; idx < len; idx++ ) {
  7513. item = classes[ idx ].split( ":" );
  7514. if ( item[ 0 ] ) {
  7515. options.classes[ item[ 0 ] ] = item[ 1 ] || "";
  7516. }
  7517. }
  7518. },
  7519. start: toggleClass,
  7520. end: toggleClass
  7521. });
  7522. })( Popcorn );