1(function(global, document) { 2 3 // Popcorn.js does not support archaic browsers 4 if ( !document.addEventListener ) { 5 global.Popcorn = { 6 isSupported: false 7 }; 8 9 var methods = ( "byId forEach extend effects error guid sizeOf isArray nop position disable enable destroy" + 10 "addTrackEvent removeTrackEvent getTrackEvents getTrackEvent getLastTrackEventId " + 11 "timeUpdate plugin removePlugin compose effect xhr getJSONP getScript" ).split(/\s+/); 12 13 while ( methods.length ) { 14 global.Popcorn[ methods.shift() ] = function() {}; 15 } 16 return; 17 } 18 19 var 20 21 AP = Array.prototype, 22 OP = Object.prototype, 23 24 forEach = AP.forEach, 25 slice = AP.slice, 26 hasOwn = OP.hasOwnProperty, 27 toString = OP.toString, 28 29 // Copy global Popcorn (may not exist) 30 _Popcorn = global.Popcorn, 31 32 // Ready fn cache 33 readyStack = [], 34 readyBound = false, 35 readyFired = false, 36 37 // Non-public internal data object 38 internal = { 39 events: { 40 hash: {}, 41 apis: {} 42 } 43 }, 44 45 // Non-public `requestAnimFrame` 46 // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 47 requestAnimFrame = (function(){ 48 return global.requestAnimationFrame || 49 global.webkitRequestAnimationFrame || 50 global.mozRequestAnimationFrame || 51 global.oRequestAnimationFrame || 52 global.msRequestAnimationFrame || 53 function( callback, element ) { 54 global.setTimeout( callback, 16 ); 55 }; 56 }()), 57 58 // Non-public `getKeys`, return an object's keys as an array 59 getKeys = function( obj ) { 60 return Object.keys ? Object.keys( obj ) : (function( obj ) { 61 var item, 62 list = []; 63 64 for ( item in obj ) { 65 if ( hasOwn.call( obj, item ) ) { 66 list.push( item ); 67 } 68 } 69 return list; 70 })( obj ); 71 }, 72 73 Abstract = { 74 // [[Put]] props from dictionary onto |this| 75 // MUST BE CALLED FROM WITHIN A CONSTRUCTOR: 76 // Abstract.put.call( this, dictionary ); 77 put: function( dictionary ) { 78 // For each own property of src, let key be the property key 79 // and desc be the property descriptor of the property. 80 for ( var key in dictionary ) { 81 if ( dictionary.hasOwnProperty( key ) ) { 82 this[ key ] = dictionary[ key ]; 83 } 84 } 85 } 86 }, 87 88 89 // Declare constructor 90 // Returns an instance object. 91 Popcorn = function( entity, options ) { 92 // Return new Popcorn object 93 return new Popcorn.p.init( entity, options || null ); 94 }; 95 96 // Popcorn API version, automatically inserted via build system. 97 Popcorn.version = "@VERSION"; 98 99 // Boolean flag allowing a client to determine if Popcorn can be supported 100 Popcorn.isSupported = true; 101 102 // Instance caching 103 Popcorn.instances = []; 104 105 // Declare a shortcut (Popcorn.p) to and a definition of 106 // the new prototype for our Popcorn constructor 107 Popcorn.p = Popcorn.prototype = { 108 109 init: function( entity, options ) { 110 111 var matches, nodeName, 112 self = this; 113 114 // Supports Popcorn(function () { /../ }) 115 // Originally proposed by Daniel Brooks 116 117 if ( typeof entity === "function" ) { 118 119 // If document ready has already fired 120 if ( document.readyState === "complete" ) { 121 122 entity( document, Popcorn ); 123 124 return; 125 } 126 // Add `entity` fn to ready stack 127 readyStack.push( entity ); 128 129 // This process should happen once per page load 130 if ( !readyBound ) { 131 132 // set readyBound flag 133 readyBound = true; 134 135 var DOMContentLoaded = function() { 136 137 readyFired = true; 138 139 // Remove global DOM ready listener 140 document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); 141 142 // Execute all ready function in the stack 143 for ( var i = 0, readyStackLength = readyStack.length; i < readyStackLength; i++ ) { 144 145 readyStack[ i ].call( document, Popcorn ); 146 147 } 148 // GC readyStack 149 readyStack = null; 150 }; 151 152 // Register global DOM ready listener 153 document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); 154 } 155 156 return; 157 } 158 159 if ( typeof entity === "string" ) { 160 try { 161 matches = document.querySelector( entity ); 162 } catch( e ) { 163 throw new Error( "Popcorn.js Error: Invalid media element selector: " + entity ); 164 } 165 } 166 167 // Get media element by id or object reference 168 this.media = matches || entity; 169 170 // inner reference to this media element's nodeName string value 171 nodeName = ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video"; 172 173 // Create an audio or video element property reference 174 this[ nodeName ] = this.media; 175 176 this.options = Popcorn.extend( {}, options ) || {}; 177 178 // Resolve custom ID or default prefixed ID 179 this.id = this.options.id || Popcorn.guid( nodeName ); 180 181 // Throw if an attempt is made to use an ID that already exists 182 if ( Popcorn.byId( this.id ) ) { 183 throw new Error( "Popcorn.js Error: Cannot use duplicate ID (" + this.id + ")" ); 184 } 185 186 this.isDestroyed = false; 187 188 this.data = { 189 190 // data structure of all 191 running: { 192 cue: [] 193 }, 194 195 // Executed by either timeupdate event or in rAF loop 196 timeUpdate: Popcorn.nop, 197 198 // Allows disabling a plugin per instance 199 disabled: {}, 200 201 // Stores DOM event queues by type 202 events: {}, 203 204 // Stores Special event hooks data 205 hooks: {}, 206 207 // Store track event history data 208 history: [], 209 210 // Stores ad-hoc state related data] 211 state: { 212 volume: this.media.volume 213 }, 214 215 // Store track event object references by trackId 216 trackRefs: {}, 217 218 // Playback track event queues 219 trackEvents: new TrackEvents( this ) 220 }; 221 222 // Register new instance 223 Popcorn.instances.push( this ); 224 225 // function to fire when video is ready 226 var isReady = function() { 227 228 // chrome bug: http://code.google.com/p/chromium/issues/detail?id=119598 229 // it is possible the video's time is less than 0 230 // this has the potential to call track events more than once, when they should not 231 // start: 0, end: 1 will start, end, start again, when it should just start 232 // just setting it to 0 if it is below 0 fixes this issue 233 if ( self.media.currentTime < 0 ) { 234 235 self.media.currentTime = 0; 236 } 237 238 self.media.removeEventListener( "loadedmetadata", isReady, false ); 239 240 var duration, videoDurationPlus, 241 runningPlugins, runningPlugin, rpLength, rpNatives; 242 243 // Adding padding to the front and end of the arrays 244 // this is so we do not fall off either end 245 duration = self.media.duration; 246 247 // Check for no duration info (NaN) 248 videoDurationPlus = duration != duration ? Number.MAX_VALUE : duration + 1; 249 250 Popcorn.addTrackEvent( self, { 251 start: videoDurationPlus, 252 end: videoDurationPlus 253 }); 254 255 if ( !self.isDestroyed ) { 256 self.data.durationChange = function() { 257 var newDuration = self.media.duration, 258 newDurationPlus = newDuration + 1, 259 byStart = self.data.trackEvents.byStart, 260 byEnd = self.data.trackEvents.byEnd; 261 262 // Remove old padding events 263 byStart.pop(); 264 byEnd.pop(); 265 266 // Remove any internal tracking of events that have end times greater than duration 267 // otherwise their end events will never be hit. 268 for ( var k = byEnd.length - 1; k > 0; k-- ) { 269 if ( byEnd[ k ].end > newDuration ) { 270 self.removeTrackEvent( byEnd[ k ]._id ); 271 } 272 } 273 274 // Remove any internal tracking of events that have end times greater than duration 275 // otherwise their end events will never be hit. 276 for ( var i = 0; i < byStart.length; i++ ) { 277 if ( byStart[ i ].end > newDuration ) { 278 self.removeTrackEvent( byStart[ i ]._id ); 279 } 280 } 281 282 // References to byEnd/byStart are reset, so accessing it this way is 283 // forced upon us. 284 self.data.trackEvents.byEnd.push({ 285 start: newDurationPlus, 286 end: newDurationPlus 287 }); 288 289 self.data.trackEvents.byStart.push({ 290 start: newDurationPlus, 291 end: newDurationPlus 292 }); 293 }; 294 295 // Listen for duration changes and adjust internal tracking of event timings 296 self.media.addEventListener( "durationchange", self.data.durationChange, false ); 297 } 298 299 if ( self.options.frameAnimation ) { 300 301 // if Popcorn is created with frameAnimation option set to true, 302 // requestAnimFrame is used instead of "timeupdate" media event. 303 // This is for greater frame time accuracy, theoretically up to 304 // 60 frames per second as opposed to ~4 ( ~every 15-250ms) 305 self.data.timeUpdate = function () { 306 307 Popcorn.timeUpdate( self, {} ); 308 309 // fire frame for each enabled active plugin of every type 310 Popcorn.forEach( Popcorn.manifest, function( key, val ) { 311 312 runningPlugins = self.data.running[ val ]; 313 314 // ensure there are running plugins on this type on this instance 315 if ( runningPlugins ) { 316 317 rpLength = runningPlugins.length; 318 for ( var i = 0; i < rpLength; i++ ) { 319 320 runningPlugin = runningPlugins[ i ]; 321 rpNatives = runningPlugin._natives; 322 rpNatives && rpNatives.frame && 323 rpNatives.frame.call( self, {}, runningPlugin, self.currentTime() ); 324 } 325 } 326 }); 327 328 self.emit( "timeupdate" ); 329 330 !self.isDestroyed && requestAnimFrame( self.data.timeUpdate ); 331 }; 332 333 !self.isDestroyed && requestAnimFrame( self.data.timeUpdate ); 334 335 } else { 336 337 self.data.timeUpdate = function( event ) { 338 Popcorn.timeUpdate( self, event ); 339 }; 340 341 if ( !self.isDestroyed ) { 342 self.media.addEventListener( "timeupdate", self.data.timeUpdate, false ); 343 } 344 } 345 }; 346 347 self.media.addEventListener( "error", function() { 348 self.error = self.media.error; 349 }, false ); 350 351 // http://www.whatwg.org/specs/web-apps/current-work/#dom-media-readystate 352 // 353 // If media is in readyState (rS) >= 1, we know the media's duration, 354 // which is required before running the isReady function. 355 // If rS is 0, attach a listener for "loadedmetadata", 356 // ( Which indicates that the media has moved from rS 0 to 1 ) 357 // 358 // This has been changed from a check for rS 2 because 359 // in certain conditions, Firefox can enter this code after dropping 360 // to rS 1 from a higher state such as 2 or 3. This caused a "loadeddata" 361 // listener to be attached to the media object, an event that had 362 // already triggered and would not trigger again. This left Popcorn with an 363 // instance that could never start a timeUpdate loop. 364 if ( self.media.readyState >= 1 ) { 365 366 isReady(); 367 } else { 368 369 self.media.addEventListener( "loadedmetadata", isReady, false ); 370 } 371 372 return this; 373 } 374 }; 375 376 // Extend constructor prototype to instance prototype 377 // Allows chaining methods to instances 378 Popcorn.p.init.prototype = Popcorn.p; 379 380 Popcorn.byId = function( str ) { 381 var instances = Popcorn.instances, 382 length = instances.length, 383 i = 0; 384 385 for ( ; i < length; i++ ) { 386 if ( instances[ i ].id === str ) { 387 return instances[ i ]; 388 } 389 } 390 391 return null; 392 }; 393 394 Popcorn.forEach = function( obj, fn, context ) { 395 396 if ( !obj || !fn ) { 397 return {}; 398 } 399 400 context = context || this; 401 402 var key, len; 403 404 // Use native whenever possible 405 if ( forEach && obj.forEach === forEach ) { 406 return obj.forEach( fn, context ); 407 } 408 409 if ( toString.call( obj ) === "[object NodeList]" ) { 410 for ( key = 0, len = obj.length; key < len; key++ ) { 411 fn.call( context, obj[ key ], key, obj ); 412 } 413 return obj; 414 } 415 416 for ( key in obj ) { 417 if ( hasOwn.call( obj, key ) ) { 418 fn.call( context, obj[ key ], key, obj ); 419 } 420 } 421 return obj; 422 }; 423 424 Popcorn.extend = function( obj ) { 425 var dest = obj, src = slice.call( arguments, 1 ); 426 427 Popcorn.forEach( src, function( copy ) { 428 for ( var prop in copy ) { 429 dest[ prop ] = copy[ prop ]; 430 } 431 }); 432 433 return dest; 434 }; 435 436 437 // A Few reusable utils, memoized onto Popcorn 438 Popcorn.extend( Popcorn, { 439 noConflict: function( deep ) { 440 441 if ( deep ) { 442 global.Popcorn = _Popcorn; 443 } 444 445 return Popcorn; 446 }, 447 error: function( msg ) { 448 throw new Error( msg ); 449 }, 450 guid: function( prefix ) { 451 Popcorn.guid.counter++; 452 return ( prefix ? prefix : "" ) + ( +new Date() + Popcorn.guid.counter ); 453 }, 454 sizeOf: function( obj ) { 455 var size = 0; 456 457 for ( var prop in obj ) { 458 size++; 459 } 460 461 return size; 462 }, 463 isArray: Array.isArray || function( array ) { 464 return toString.call( array ) === "[object Array]"; 465 }, 466 467 nop: function() {}, 468 469 position: function( elem ) { 470 471 if ( !elem.parentNode ) { 472 return null; 473 } 474 475 var clientRect = elem.getBoundingClientRect(), 476 bounds = {}, 477 doc = elem.ownerDocument, 478 docElem = document.documentElement, 479 body = document.body, 480 clientTop, clientLeft, scrollTop, scrollLeft, top, left; 481 482 // Determine correct clientTop/Left 483 clientTop = docElem.clientTop || body.clientTop || 0; 484 clientLeft = docElem.clientLeft || body.clientLeft || 0; 485 486 // Determine correct scrollTop/Left 487 scrollTop = ( global.pageYOffset && docElem.scrollTop || body.scrollTop ); 488 scrollLeft = ( global.pageXOffset && docElem.scrollLeft || body.scrollLeft ); 489 490 // Temp top/left 491 top = Math.ceil( clientRect.top + scrollTop - clientTop ); 492 left = Math.ceil( clientRect.left + scrollLeft - clientLeft ); 493 494 for ( var p in clientRect ) { 495 bounds[ p ] = Math.round( clientRect[ p ] ); 496 } 497 498 return Popcorn.extend({}, bounds, { top: top, left: left }); 499 }, 500 501 disable: function( instance, plugin ) { 502 503 if ( instance.data.disabled[ plugin ] ) { 504 return; 505 } 506 507 instance.data.disabled[ plugin ] = true; 508 509 if ( plugin in Popcorn.registryByName && 510 instance.data.running[ plugin ] ) { 511 512 for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) { 513 514 event = instance.data.running[ plugin ][ i ]; 515 event._natives.end.call( instance, null, event ); 516 517 instance.emit( "trackend", 518 Popcorn.extend({}, event, { 519 plugin: event.type, 520 type: "trackend" 521 }) 522 ); 523 } 524 } 525 526 return instance; 527 }, 528 enable: function( instance, plugin ) { 529 530 if ( !instance.data.disabled[ plugin ] ) { 531 return; 532 } 533 534 instance.data.disabled[ plugin ] = false; 535 536 if ( plugin in Popcorn.registryByName && 537 instance.data.running[ plugin ] ) { 538 539 for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) { 540 541 event = instance.data.running[ plugin ][ i ]; 542 event._natives.start.call( instance, null, event ); 543 544 instance.emit( "trackstart", 545 Popcorn.extend({}, event, { 546 plugin: event.type, 547 type: "trackstart", 548 track: event 549 }) 550 ); 551 } 552 } 553 554 return instance; 555 }, 556 destroy: function( instance ) { 557 var events = instance.data.events, 558 trackEvents = instance.data.trackEvents, 559 singleEvent, item, fn, plugin; 560 561 // Iterate through all events and remove them 562 for ( item in events ) { 563 singleEvent = events[ item ]; 564 for ( fn in singleEvent ) { 565 delete singleEvent[ fn ]; 566 } 567 events[ item ] = null; 568 } 569 570 // remove all plugins off the given instance 571 for ( plugin in Popcorn.registryByName ) { 572 Popcorn.removePlugin( instance, plugin ); 573 } 574 575 // Remove all data.trackEvents #1178 576 trackEvents.byStart.length = 0; 577 trackEvents.byEnd.length = 0; 578 579 if ( !instance.isDestroyed ) { 580 instance.data.timeUpdate && instance.media.removeEventListener( "timeupdate", instance.data.timeUpdate, false ); 581 instance.isDestroyed = true; 582 } 583 584 Popcorn.instances.splice( Popcorn.instances.indexOf( instance ), 1 ); 585 } 586 }); 587 588 // Memoized GUID Counter 589 Popcorn.guid.counter = 1; 590 591 // Factory to implement getters, setters and controllers 592 // as Popcorn instance methods. The IIFE will create and return 593 // an object with defined methods 594 Popcorn.extend(Popcorn.p, (function() { 595 596 var methods = "load play pause currentTime playbackRate volume duration preload playbackRate " + 597 "autoplay loop controls muted buffered readyState seeking paused played seekable ended", 598 ret = {}; 599 600 601 // Build methods, store in object that is returned and passed to extend 602 Popcorn.forEach( methods.split( /\s+/g ), function( name ) { 603 604 ret[ name ] = function( arg ) { 605 var previous; 606 607 if ( typeof this.media[ name ] === "function" ) { 608 609 // Support for shorthanded play(n)/pause(n) jump to currentTime 610 // If arg is not null or undefined and called by one of the 611 // allowed shorthandable methods, then set the currentTime 612 // Supports time as seconds or SMPTE 613 if ( arg != null && /play|pause/.test( name ) ) { 614 this.media.currentTime = Popcorn.util.toSeconds( arg ); 615 } 616 617 this.media[ name ](); 618 619 return this; 620 } 621 622 if ( arg != null ) { 623 // Capture the current value of the attribute property 624 previous = this.media[ name ]; 625 626 // Set the attribute property with the new value 627 this.media[ name ] = arg; 628 629 // If the new value is not the same as the old value 630 // emit an "attrchanged event" 631 if ( previous !== arg ) { 632 this.emit( "attrchange", { 633 attribute: name, 634 previousValue: previous, 635 currentValue: arg 636 }); 637 } 638 return this; 639 } 640 641 return this.media[ name ]; 642 }; 643 }); 644 645 return ret; 646 647 })() 648 ); 649 650 Popcorn.forEach( "enable disable".split(" "), function( method ) { 651 Popcorn.p[ method ] = function( plugin ) { 652 return Popcorn[ method ]( this, plugin ); 653 }; 654 }); 655 656 Popcorn.extend(Popcorn.p, { 657 658 // Rounded currentTime 659 roundTime: function() { 660 return Math.round( this.media.currentTime ); 661 }, 662 663 // Attach an event to a single point in time 664 exec: function( id, time, fn ) { 665 var length = arguments.length, 666 eventType = "trackadded", 667 trackEvent, sec, options; 668 669 // Check if first could possibly be a SMPTE string 670 // p.cue( "smpte string", fn ); 671 // try/catch avoid awful throw in Popcorn.util.toSeconds 672 // TODO: Get rid of that, replace with NaN return? 673 try { 674 sec = Popcorn.util.toSeconds( id ); 675 } catch ( e ) {} 676 677 // If it can be converted into a number then 678 // it's safe to assume that the string was SMPTE 679 if ( typeof sec === "number" ) { 680 id = sec; 681 } 682 683 // Shift arguments based on use case 684 // 685 // Back compat for: 686 // p.cue( time, fn ); 687 if ( typeof id === "number" && length === 2 ) { 688 fn = time; 689 time = id; 690 id = Popcorn.guid( "cue" ); 691 } else { 692 // Support for new forms 693 694 // p.cue( "empty-cue" ); 695 if ( length === 1 ) { 696 // Set a time for an empty cue. It's not important what 697 // the time actually is, because the cue is a no-op 698 time = -1; 699 700 } else { 701 702 // Get the TrackEvent that matches the given id. 703 trackEvent = this.getTrackEvent( id ); 704 705 if ( trackEvent ) { 706 707 // remove existing cue so a new one can be added via trackEvents.add 708 this.data.trackEvents.remove( id ); 709 TrackEvent.end( this, trackEvent ); 710 // Update track event references 711 Popcorn.removeTrackEvent.ref( this, id ); 712 713 eventType = "cuechange"; 714 715 // p.cue( "my-id", 12 ); 716 // p.cue( "my-id", function() { ... }); 717 if ( typeof id === "string" && length === 2 ) { 718 719 // p.cue( "my-id", 12 ); 720 // The path will update the cue time. 721 if ( typeof time === "number" ) { 722 // Re-use existing TrackEvent start callback 723 fn = trackEvent._natives.start; 724 } 725 726 // p.cue( "my-id", function() { ... }); 727 // The path will update the cue function 728 if ( typeof time === "function" ) { 729 fn = time; 730 // Re-use existing TrackEvent start time 731 time = trackEvent.start; 732 } 733 } 734 } else { 735 736 if ( length >= 2 ) { 737 738 // p.cue( "a", "00:00:00"); 739 if ( typeof time === "string" ) { 740 try { 741 sec = Popcorn.util.toSeconds( time ); 742 } catch ( e ) {} 743 744 time = sec; 745 } 746 747 // p.cue( "b", 11 ); 748 // p.cue( "b", 11, function() {} ); 749 if ( typeof time === "number" ) { 750 fn = fn || Popcorn.nop(); 751 } 752 753 // p.cue( "c", function() {}); 754 if ( typeof time === "function" ) { 755 fn = time; 756 time = -1; 757 } 758 } 759 } 760 } 761 } 762 763 options = { 764 id: id, 765 start: time, 766 end: time + 1, 767 _running: false, 768 _natives: { 769 start: fn || Popcorn.nop, 770 end: Popcorn.nop, 771 type: "cue" 772 } 773 }; 774 775 if ( trackEvent ) { 776 options = Popcorn.extend( trackEvent, options ); 777 } 778 779 if ( eventType === "cuechange" ) { 780 781 // Supports user defined track event id 782 options._id = options.id || options._id || Popcorn.guid( options._natives.type ); 783 784 this.data.trackEvents.add( options ); 785 TrackEvent.start( this, options ); 786 787 this.timeUpdate( this, null, true ); 788 789 // Store references to user added trackevents in ref table 790 Popcorn.addTrackEvent.ref( this, options ); 791 792 this.emit( eventType, Popcorn.extend({}, options, { 793 id: id, 794 type: eventType, 795 previousValue: { 796 time: trackEvent.start, 797 fn: trackEvent._natives.start 798 }, 799 currentValue: { 800 time: time, 801 fn: fn || Popcorn.nop 802 }, 803 track: trackEvent 804 })); 805 } else { 806 // Creating a one second track event with an empty end 807 Popcorn.addTrackEvent( this, options ); 808 } 809 810 return this; 811 }, 812 813 // Mute the calling media, optionally toggle 814 mute: function( toggle ) { 815 816 var event = toggle == null || toggle === true ? "muted" : "unmuted"; 817 818 // If `toggle` is explicitly `false`, 819 // unmute the media and restore the volume level 820 if ( event === "unmuted" ) { 821 this.media.muted = false; 822 this.media.volume = this.data.state.volume; 823 } 824 825 // If `toggle` is either null or undefined, 826 // save the current volume and mute the media element 827 if ( event === "muted" ) { 828 this.data.state.volume = this.media.volume; 829 this.media.muted = true; 830 } 831 832 // Trigger either muted|unmuted event 833 this.emit( event ); 834 835 return this; 836 }, 837 838 // Convenience method, unmute the calling media 839 unmute: function( toggle ) { 840 841 return this.mute( toggle == null ? false : !toggle ); 842 }, 843 844 // Get the client bounding box of an instance element 845 position: function() { 846 return Popcorn.position( this.media ); 847 }, 848 849 // Toggle a plugin's playback behaviour (on or off) per instance 850 toggle: function( plugin ) { 851 return Popcorn[ this.data.disabled[ plugin ] ? "enable" : "disable" ]( this, plugin ); 852 }, 853 854 // Set default values for plugin options objects per instance 855 defaults: function( plugin, defaults ) { 856 857 // If an array of default configurations is provided, 858 // iterate and apply each to this instance 859 if ( Popcorn.isArray( plugin ) ) { 860 861 Popcorn.forEach( plugin, function( obj ) { 862 for ( var name in obj ) { 863 this.defaults( name, obj[ name ] ); 864 } 865 }, this ); 866 867 return this; 868 } 869 870 if ( !this.options.defaults ) { 871 this.options.defaults = {}; 872 } 873 874 if ( !this.options.defaults[ plugin ] ) { 875 this.options.defaults[ plugin ] = {}; 876 } 877 878 Popcorn.extend( this.options.defaults[ plugin ], defaults ); 879 880 return this; 881 } 882 }); 883 884 Popcorn.Events = { 885 UIEvents: "blur focus focusin focusout load resize scroll unload", 886 MouseEvents: "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave click dblclick", 887 Events: "loadstart progress suspend emptied stalled play pause error " + 888 "loadedmetadata loadeddata waiting playing canplay canplaythrough " + 889 "seeking seeked timeupdate ended ratechange durationchange volumechange" 890 }; 891 892 Popcorn.Events.Natives = Popcorn.Events.UIEvents + " " + 893 Popcorn.Events.MouseEvents + " " + 894 Popcorn.Events.Events; 895 896 internal.events.apiTypes = [ "UIEvents", "MouseEvents", "Events" ]; 897 898 // Privately compile events table at load time 899 (function( events, data ) { 900 901 var apis = internal.events.apiTypes, 902 eventsList = events.Natives.split( /\s+/g ), 903 idx = 0, len = eventsList.length, prop; 904 905 for( ; idx < len; idx++ ) { 906 data.hash[ eventsList[idx] ] = true; 907 } 908 909 apis.forEach(function( val, idx ) { 910 911 data.apis[ val ] = {}; 912 913 var apiEvents = events[ val ].split( /\s+/g ), 914 len = apiEvents.length, 915 k = 0; 916 917 for ( ; k < len; k++ ) { 918 data.apis[ val ][ apiEvents[ k ] ] = true; 919 } 920 }); 921 })( Popcorn.Events, internal.events ); 922 923 Popcorn.events = { 924 925 isNative: function( type ) { 926 return !!internal.events.hash[ type ]; 927 }, 928 getInterface: function( type ) { 929 930 if ( !Popcorn.events.isNative( type ) ) { 931 return false; 932 } 933 934 var eventApi = internal.events, 935 apis = eventApi.apiTypes, 936 apihash = eventApi.apis, 937 idx = 0, len = apis.length, api, tmp; 938 939 for ( ; idx < len; idx++ ) { 940 tmp = apis[ idx ]; 941 942 if ( apihash[ tmp ][ type ] ) { 943 api = tmp; 944 break; 945 } 946 } 947 return api; 948 }, 949 // Compile all native events to single array 950 all: Popcorn.Events.Natives.split( /\s+/g ), 951 // Defines all Event handling static functions 952 fn: { 953 trigger: function( type, data ) { 954 var eventInterface, evt, clonedEvents, 955 events = this.data.events[ type ]; 956 957 // setup checks for custom event system 958 if ( events ) { 959 eventInterface = Popcorn.events.getInterface( type ); 960 961 if ( eventInterface ) { 962 evt = document.createEvent( eventInterface ); 963 evt.initEvent( type, true, true, global, 1 ); 964 965 this.media.dispatchEvent( evt ); 966 967 return this; 968 } 969 970 // clone events in case callbacks remove callbacks themselves 971 clonedEvents = events.slice(); 972 973 // iterate through all callbacks 974 while ( clonedEvents.length ) { 975 clonedEvents.shift().call( this, data ); 976 } 977 } 978 979 return this; 980 }, 981 listen: function( type, fn ) { 982 var self = this, 983 hasEvents = true, 984 eventHook = Popcorn.events.hooks[ type ], 985 origType = type, 986 clonedEvents, 987 tmp; 988 989 if ( typeof fn !== "function" ) { 990 throw new Error( "Popcorn.js Error: Listener is not a function" ); 991 } 992 993 // Setup event registry entry 994 if ( !this.data.events[ type ] ) { 995 this.data.events[ type ] = []; 996 // Toggle if the previous assumption was untrue 997 hasEvents = false; 998 } 999 1000 // Check and setup event hooks 1001 if ( eventHook ) { 1002 // Execute hook add method if defined 1003 if ( eventHook.add ) { 1004 eventHook.add.call( this, {}, fn ); 1005 } 1006 1007 // Reassign event type to our piggyback event type if defined 1008 if ( eventHook.bind ) { 1009 type = eventHook.bind; 1010 } 1011 1012 // Reassign handler if defined 1013 if ( eventHook.handler ) { 1014 tmp = fn; 1015 1016 fn = function wrapper( event ) { 1017 eventHook.handler.call( self, event, tmp ); 1018 }; 1019 } 1020 1021 // assume the piggy back event is registered 1022 hasEvents = true; 1023 1024 // Setup event registry entry 1025 if ( !this.data.events[ type ] ) { 1026 this.data.events[ type ] = []; 1027 // Toggle if the previous assumption was untrue 1028 hasEvents = false; 1029 } 1030 } 1031 1032 // Register event and handler 1033 this.data.events[ type ].push( fn ); 1034 1035 // only attach one event of any type 1036 if ( !hasEvents && Popcorn.events.all.indexOf( type ) > -1 ) { 1037 this.media.addEventListener( type, function( event ) { 1038 if ( self.data.events[ type ] ) { 1039 // clone events in case callbacks remove callbacks themselves 1040 clonedEvents = self.data.events[ type ].slice(); 1041 1042 // iterate through all callbacks 1043 while ( clonedEvents.length ) { 1044 clonedEvents.shift().call( self, event ); 1045 } 1046 } 1047 }, false ); 1048 } 1049 return this; 1050 }, 1051 unlisten: function( type, fn ) { 1052 var ind, 1053 events = this.data.events[ type ]; 1054 1055 if ( !events ) { 1056 return; // no listeners = nothing to do 1057 } 1058 1059 if ( typeof fn === "string" ) { 1060 // legacy support for string-based removal -- not recommended 1061 for ( var i = 0; i < events.length; i++ ) { 1062 if ( events[ i ].name === fn ) { 1063 // decrement i because array length just got smaller 1064 events.splice( i--, 1 ); 1065 } 1066 } 1067 1068 return this; 1069 } else if ( typeof fn === "function" ) { 1070 while( ind !== -1 ) { 1071 ind = events.indexOf( fn ); 1072 if ( ind !== -1 ) { 1073 events.splice( ind, 1 ); 1074 } 1075 } 1076 1077 return this; 1078 } 1079 1080 // if we got to this point, we are deleting all functions of this type 1081 this.data.events[ type ] = null; 1082 1083 return this; 1084 } 1085 }, 1086 hooks: { 1087 canplayall: { 1088 bind: "canplaythrough", 1089 add: function( event, callback ) { 1090 1091 var state = false; 1092 1093 if ( this.media.readyState ) { 1094 1095 // always call canplayall asynchronously 1096 setTimeout(function() { 1097 callback.call( this, event ); 1098 }.bind(this), 0 ); 1099 1100 state = true; 1101 } 1102 1103 this.data.hooks.canplayall = { 1104 fired: state 1105 }; 1106 }, 1107 // declare special handling instructions 1108 handler: function canplayall( event, callback ) { 1109 1110 if ( !this.data.hooks.canplayall.fired ) { 1111 // trigger original user callback once 1112 callback.call( this, event ); 1113 1114 this.data.hooks.canplayall.fired = true; 1115 } 1116 } 1117 } 1118 } 1119 }; 1120 1121 // Extend Popcorn.events.fns (listen, unlisten, trigger) to all Popcorn instances 1122 // Extend aliases (on, off, emit) 1123 Popcorn.forEach( [ [ "trigger", "emit" ], [ "listen", "on" ], [ "unlisten", "off" ] ], function( key ) { 1124 Popcorn.p[ key[ 0 ] ] = Popcorn.p[ key[ 1 ] ] = Popcorn.events.fn[ key[ 0 ] ]; 1125 }); 1126 1127 // Internal Only - construct simple "TrackEvent" 1128 // data type objects 1129 function TrackEvent( track ) { 1130 Abstract.put.call( this, track ); 1131 } 1132 1133 // Determine if a TrackEvent's "start" and "trackstart" must be called. 1134 TrackEvent.start = function( instance, track ) { 1135 1136 if ( track.end > instance.media.currentTime && 1137 track.start <= instance.media.currentTime && !track._running ) { 1138 1139 track._running = true; 1140 instance.data.running[ track._natives.type ].push( track ); 1141 1142 if ( !instance.data.disabled[ track._natives.type ] ) { 1143 1144 track._natives.start.call( instance, null, track ); 1145 1146 instance.emit( "trackstart", 1147 Popcorn.extend( {}, track, { 1148 plugin: track._natives.type, 1149 type: "trackstart", 1150 track: track 1151 }) 1152 ); 1153 } 1154 } 1155 }; 1156 1157 // Determine if a TrackEvent's "end" and "trackend" must be called. 1158 TrackEvent.end = function( instance, track ) { 1159 1160 var runningPlugins; 1161 1162 if ( ( track.end <= instance.media.currentTime || 1163 track.start > instance.media.currentTime ) && track._running ) { 1164 1165 runningPlugins = instance.data.running[ track._natives.type ]; 1166 1167 track._running = false; 1168 runningPlugins.splice( runningPlugins.indexOf( track ), 1 ); 1169 1170 if ( !instance.data.disabled[ track._natives.type ] ) { 1171 1172 track._natives.end.call( instance, null, track ); 1173 1174 instance.emit( "trackend", 1175 Popcorn.extend( {}, track, { 1176 plugin: track._natives.type, 1177 type: "trackend", 1178 track: track 1179 }) 1180 ); 1181 } 1182 } 1183 }; 1184 1185 // Internal Only - construct "TrackEvents" 1186 // data type objects that are used by the Popcorn 1187 // instance, stored at p.data.trackEvents 1188 function TrackEvents( parent ) { 1189 this.parent = parent; 1190 1191 this.byStart = [{ 1192 start: -1, 1193 end: -1 1194 }]; 1195 1196 this.byEnd = [{ 1197 start: -1, 1198 end: -1 1199 }]; 1200 this.animating = []; 1201 this.startIndex = 0; 1202 this.endIndex = 0; 1203 this.previousUpdateTime = -1; 1204 1205 this.count = 1; 1206 } 1207 1208 function isMatch( obj, key, value ) { 1209 return obj[ key ] && obj[ key ] === value; 1210 } 1211 1212 TrackEvents.prototype.where = function( params ) { 1213 return ( this.parent.getTrackEvents() || [] ).filter(function( event ) { 1214 var key, value; 1215 1216 // If no explicit params, match all TrackEvents 1217 if ( !params ) { 1218 return true; 1219 } 1220 1221 // Filter keys in params against both the top level properties 1222 // and the _natives properties 1223 for ( key in params ) { 1224 value = params[ key ]; 1225 if ( isMatch( event, key, value ) || isMatch( event._natives, key, value ) ) { 1226 return true; 1227 } 1228 } 1229 return false; 1230 }); 1231 }; 1232 1233 TrackEvents.prototype.add = function( track ) { 1234 1235 // Store this definition in an array sorted by times 1236 var byStart = this.byStart, 1237 byEnd = this.byEnd, 1238 startIndex, endIndex; 1239 1240 // Push track event ids into the history 1241 if ( track && track._id ) { 1242 this.parent.data.history.push( track._id ); 1243 } 1244 1245 track.start = Popcorn.util.toSeconds( track.start, this.parent.options.framerate ); 1246 track.end = Popcorn.util.toSeconds( track.end, this.parent.options.framerate ); 1247 1248 for ( startIndex = byStart.length - 1; startIndex >= 0; startIndex-- ) { 1249 1250 if ( track.start >= byStart[ startIndex ].start ) { 1251 byStart.splice( startIndex + 1, 0, track ); 1252 break; 1253 } 1254 } 1255 1256 for ( endIndex = byEnd.length - 1; endIndex >= 0; endIndex-- ) { 1257 1258 if ( track.end > byEnd[ endIndex ].end ) { 1259 byEnd.splice( endIndex + 1, 0, track ); 1260 break; 1261 } 1262 } 1263 1264 // update startIndex and endIndex 1265 if ( startIndex <= this.parent.data.trackEvents.startIndex && 1266 track.start <= this.parent.data.trackEvents.previousUpdateTime ) { 1267 1268 this.parent.data.trackEvents.startIndex++; 1269 } 1270 1271 if ( endIndex <= this.parent.data.trackEvents.endIndex && 1272 track.end < this.parent.data.trackEvents.previousUpdateTime ) { 1273 1274 this.parent.data.trackEvents.endIndex++; 1275 } 1276 1277 this.count++; 1278 1279 }; 1280 1281 TrackEvents.prototype.remove = function( removeId, state ) { 1282 1283 if ( removeId instanceof TrackEvent ) { 1284 removeId = removeId.id; 1285 } 1286 1287 if ( typeof removeId === "object" ) { 1288 // Filter by key=val and remove all matching TrackEvents 1289 this.where( removeId ).forEach(function( event ) { 1290 // |this| refers to the calling Popcorn "parent" instance 1291 this.removeTrackEvent( event._id ); 1292 }, this.parent ); 1293 1294 return this; 1295 } 1296 1297 var start, end, animate, historyLen, track, 1298 length = this.byStart.length, 1299 index = 0, 1300 indexWasAt = 0, 1301 byStart = [], 1302 byEnd = [], 1303 animating = [], 1304 history = [], 1305 comparable = {}; 1306 1307 state = state || {}; 1308 1309 while ( --length > -1 ) { 1310 start = this.byStart[ index ]; 1311 end = this.byEnd[ index ]; 1312 1313 // Padding events will not have _id properties. 1314 // These should be safely pushed onto the front and back of the 1315 // track event array 1316 if ( !start._id ) { 1317 byStart.push( start ); 1318 byEnd.push( end ); 1319 } 1320 1321 // Filter for user track events (vs system track events) 1322 if ( start._id ) { 1323 1324 // If not a matching start event for removal 1325 if ( start._id !== removeId ) { 1326 byStart.push( start ); 1327 } 1328 1329 // If not a matching end event for removal 1330 if ( end._id !== removeId ) { 1331 byEnd.push( end ); 1332 } 1333 1334 // If the _id is matched, capture the current index 1335 if ( start._id === removeId ) { 1336 indexWasAt = index; 1337 1338 // cache the track event being removed 1339 track = start; 1340 } 1341 } 1342 // Increment the track index 1343 index++; 1344 } 1345 1346 // Reset length to be used by the condition below to determine 1347 // if animating track events should also be filtered for removal. 1348 // Reset index below to be used by the reverse while as an 1349 // incrementing counter 1350 length = this.animating.length; 1351 index = 0; 1352 1353 if ( length ) { 1354 while ( --length > -1 ) { 1355 animate = this.animating[ index ]; 1356 1357 // Padding events will not have _id properties. 1358 // These should be safely pushed onto the front and back of the 1359 // track event array 1360 if ( !animate._id ) { 1361 animating.push( animate ); 1362 } 1363 1364 // If not a matching animate event for removal 1365 if ( animate._id && animate._id !== removeId ) { 1366 animating.push( animate ); 1367 } 1368 // Increment the track index 1369 index++; 1370 } 1371 } 1372 1373 // Update 1374 if ( indexWasAt <= this.startIndex ) { 1375 this.startIndex--; 1376 } 1377 1378 if ( indexWasAt <= this.endIndex ) { 1379 this.endIndex--; 1380 } 1381 1382 this.byStart = byStart; 1383 this.byEnd = byEnd; 1384 this.animating = animating; 1385 this.count--; 1386 1387 historyLen = this.parent.data.history.length; 1388 1389 for ( var i = 0; i < historyLen; i++ ) { 1390 if ( this.parent.data.history[ i ] !== removeId ) { 1391 history.push( this.parent.data.history[ i ] ); 1392 } 1393 } 1394 1395 // Update ordered history array 1396 this.parent.data.history = history; 1397 1398 }; 1399 1400 // Helper function used to retrieve old values of properties that 1401 // are provided for update. 1402 function getPreviousProperties( oldOptions, newOptions ) { 1403 var matchProps = {}; 1404 1405 for ( var prop in oldOptions ) { 1406 if ( hasOwn.call( newOptions, prop ) && hasOwn.call( oldOptions, prop ) ) { 1407 matchProps[ prop ] = oldOptions[ prop ]; 1408 } 1409 } 1410 1411 return matchProps; 1412 } 1413 1414 // Internal Only - Adds track events to the instance object 1415 Popcorn.addTrackEvent = function( obj, track ) { 1416 var temp; 1417 1418 if ( track instanceof TrackEvent ) { 1419 return; 1420 } 1421 1422 track = new TrackEvent( track ); 1423 1424 // Determine if this track has default options set for it 1425 // If so, apply them to the track object 1426 if ( track && track._natives && track._natives.type && 1427 ( obj.options.defaults && obj.options.defaults[ track._natives.type ] ) ) { 1428 1429 // To ensure that the TrackEvent Invariant Policy is enforced, 1430 // First, copy the properties of the newly created track event event 1431 // to a temporary holder 1432 temp = Popcorn.extend( {}, track ); 1433 1434 // Next, copy the default onto the newly created trackevent, followed by the 1435 // temporary holder. 1436 Popcorn.extend( track, obj.options.defaults[ track._natives.type ], temp ); 1437 } 1438 1439 if ( track._natives ) { 1440 // Supports user defined track event id 1441 track._id = track.id || track._id || Popcorn.guid( track._natives.type ); 1442 1443 // Trigger _setup method if exists 1444 if ( track._natives._setup ) { 1445 1446 track._natives._setup.call( obj, track ); 1447 1448 obj.emit( "tracksetup", Popcorn.extend( {}, track, { 1449 plugin: track._natives.type, 1450 type: "tracksetup", 1451 track: track 1452 })); 1453 } 1454 } 1455 1456 obj.data.trackEvents.add( track ); 1457 TrackEvent.start( obj, track ); 1458 1459 this.timeUpdate( obj, null, true ); 1460 1461 // Store references to user added trackevents in ref table 1462 if ( track._id ) { 1463 Popcorn.addTrackEvent.ref( obj, track ); 1464 } 1465 1466 obj.emit( "trackadded", Popcorn.extend({}, track, 1467 track._natives ? { plugin: track._natives.type } : {}, { 1468 type: "trackadded", 1469 track: track 1470 })); 1471 }; 1472 1473 // Internal Only - Adds track event references to the instance object's trackRefs hash table 1474 Popcorn.addTrackEvent.ref = function( obj, track ) { 1475 obj.data.trackRefs[ track._id ] = track; 1476 1477 return obj; 1478 }; 1479 1480 Popcorn.removeTrackEvent = function( obj, removeId ) { 1481 var track = obj.getTrackEvent( removeId ); 1482 1483 if ( !track ) { 1484 return; 1485 } 1486 1487 // If a _teardown function was defined, 1488 // enforce for track event removals 1489 if ( track._natives._teardown ) { 1490 track._natives._teardown.call( obj, track ); 1491 } 1492 1493 obj.data.trackEvents.remove( removeId ); 1494 1495 // Update track event references 1496 Popcorn.removeTrackEvent.ref( obj, removeId ); 1497 1498 if ( track._natives ) { 1499 1500 // Fire a trackremoved event 1501 obj.emit( "trackremoved", Popcorn.extend({}, track, { 1502 plugin: track._natives.type, 1503 type: "trackremoved", 1504 track: track 1505 })); 1506 } 1507 }; 1508 1509 // Internal Only - Removes track event references from instance object's trackRefs hash table 1510 Popcorn.removeTrackEvent.ref = function( obj, removeId ) { 1511 delete obj.data.trackRefs[ removeId ]; 1512 1513 return obj; 1514 }; 1515 1516 // Return an array of track events bound to this instance object 1517 Popcorn.getTrackEvents = function( obj ) { 1518 1519 var trackevents = [], 1520 refs = obj.data.trackEvents.byStart, 1521 length = refs.length, 1522 idx = 0, 1523 ref; 1524 1525 for ( ; idx < length; idx++ ) { 1526 ref = refs[ idx ]; 1527 // Return only user attributed track event references 1528 if ( ref._id ) { 1529 trackevents.push( ref ); 1530 } 1531 } 1532 1533 return trackevents; 1534 }; 1535 1536 // Internal Only - Returns an instance object's trackRefs hash table 1537 Popcorn.getTrackEvents.ref = function( obj ) { 1538 return obj.data.trackRefs; 1539 }; 1540 1541 // Return a single track event bound to this instance object 1542 Popcorn.getTrackEvent = function( obj, trackId ) { 1543 return obj.data.trackRefs[ trackId ]; 1544 }; 1545 1546 // Internal Only - Returns an instance object's track reference by track id 1547 Popcorn.getTrackEvent.ref = function( obj, trackId ) { 1548 return obj.data.trackRefs[ trackId ]; 1549 }; 1550 1551 Popcorn.getLastTrackEventId = function( obj ) { 1552 return obj.data.history[ obj.data.history.length - 1 ]; 1553 }; 1554 1555 Popcorn.timeUpdate = function( obj, event ) { 1556 var currentTime = obj.media.currentTime, 1557 previousTime = obj.data.trackEvents.previousUpdateTime, 1558 tracks = obj.data.trackEvents, 1559 end = tracks.endIndex, 1560 start = tracks.startIndex, 1561 byStartLen = tracks.byStart.length, 1562 byEndLen = tracks.byEnd.length, 1563 registryByName = Popcorn.registryByName, 1564 trackstart = "trackstart", 1565 trackend = "trackend", 1566 1567 byEnd, byStart, byAnimate, natives, type, runningPlugins; 1568 1569 // Playbar advancing 1570 if ( previousTime <= currentTime ) { 1571 1572 while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end <= currentTime ) { 1573 1574 byEnd = tracks.byEnd[ end ]; 1575 natives = byEnd._natives; 1576 type = natives && natives.type; 1577 1578 // If plugin does not exist on this instance, remove it 1579 if ( !natives || 1580 ( !!registryByName[ type ] || 1581 !!obj[ type ] ) ) { 1582 1583 if ( byEnd._running === true ) { 1584 1585 byEnd._running = false; 1586 runningPlugins = obj.data.running[ type ]; 1587 runningPlugins.splice( runningPlugins.indexOf( byEnd ), 1 ); 1588 1589 if ( !obj.data.disabled[ type ] ) { 1590 1591 natives.end.call( obj, event, byEnd ); 1592 1593 obj.emit( trackend, 1594 Popcorn.extend({}, byEnd, { 1595 plugin: type, 1596 type: trackend, 1597 track: byEnd 1598 }) 1599 ); 1600 } 1601 } 1602 1603 end++; 1604 } else { 1605 // remove track event 1606 Popcorn.removeTrackEvent( obj, byEnd._id ); 1607 return; 1608 } 1609 } 1610 1611 while ( tracks.byStart[ start ] && tracks.byStart[ start ].start <= currentTime ) { 1612 1613 byStart = tracks.byStart[ start ]; 1614 natives = byStart._natives; 1615 type = natives && natives.type; 1616 // If plugin does not exist on this instance, remove it 1617 if ( !natives || 1618 ( !!registryByName[ type ] || 1619 !!obj[ type ] ) ) { 1620 if ( byStart.end > currentTime && 1621 byStart._running === false ) { 1622 1623 byStart._running = true; 1624 obj.data.running[ type ].push( byStart ); 1625 1626 if ( !obj.data.disabled[ type ] ) { 1627 1628 natives.start.call( obj, event, byStart ); 1629 1630 obj.emit( trackstart, 1631 Popcorn.extend({}, byStart, { 1632 plugin: type, 1633 type: trackstart, 1634 track: byStart 1635 }) 1636 ); 1637 } 1638 } 1639 start++; 1640 } else { 1641 // remove track event 1642 Popcorn.removeTrackEvent( obj, byStart._id ); 1643 return; 1644 } 1645 } 1646 1647 // Playbar receding 1648 } else if ( previousTime > currentTime ) { 1649 1650 while ( tracks.byStart[ start ] && tracks.byStart[ start ].start > currentTime ) { 1651 1652 byStart = tracks.byStart[ start ]; 1653 natives = byStart._natives; 1654 type = natives && natives.type; 1655 1656 // if plugin does not exist on this instance, remove it 1657 if ( !natives || 1658 ( !!registryByName[ type ] || 1659 !!obj[ type ] ) ) { 1660 1661 if ( byStart._running === true ) { 1662 1663 byStart._running = false; 1664 runningPlugins = obj.data.running[ type ]; 1665 runningPlugins.splice( runningPlugins.indexOf( byStart ), 1 ); 1666 1667 if ( !obj.data.disabled[ type ] ) { 1668 1669 natives.end.call( obj, event, byStart ); 1670 1671 obj.emit( trackend, 1672 Popcorn.extend({}, byStart, { 1673 plugin: type, 1674 type: trackend, 1675 track: byStart 1676 }) 1677 ); 1678 } 1679 } 1680 start--; 1681 } else { 1682 // remove track event 1683 Popcorn.removeTrackEvent( obj, byStart._id ); 1684 return; 1685 } 1686 } 1687 1688 while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end > currentTime ) { 1689 1690 byEnd = tracks.byEnd[ end ]; 1691 natives = byEnd._natives; 1692 type = natives && natives.type; 1693 1694 // if plugin does not exist on this instance, remove it 1695 if ( !natives || 1696 ( !!registryByName[ type ] || 1697 !!obj[ type ] ) ) { 1698 1699 if ( byEnd.start <= currentTime && 1700 byEnd._running === false ) { 1701 1702 byEnd._running = true; 1703 obj.data.running[ type ].push( byEnd ); 1704 1705 if ( !obj.data.disabled[ type ] ) { 1706 1707 natives.start.call( obj, event, byEnd ); 1708 1709 obj.emit( trackstart, 1710 Popcorn.extend({}, byEnd, { 1711 plugin: type, 1712 type: trackstart, 1713 track: byEnd 1714 }) 1715 ); 1716 } 1717 } 1718 end--; 1719 } else { 1720 // remove track event 1721 Popcorn.removeTrackEvent( obj, byEnd._id ); 1722 return; 1723 } 1724 } 1725 } 1726 1727 tracks.endIndex = end; 1728 tracks.startIndex = start; 1729 tracks.previousUpdateTime = currentTime; 1730 1731 //enforce index integrity if trackRemoved 1732 tracks.byStart.length < byStartLen && tracks.startIndex--; 1733 tracks.byEnd.length < byEndLen && tracks.endIndex--; 1734 1735 }; 1736 1737 // Map and Extend TrackEvent functions to all Popcorn instances 1738 Popcorn.extend( Popcorn.p, { 1739 1740 getTrackEvents: function() { 1741 return Popcorn.getTrackEvents.call( null, this ); 1742 }, 1743 1744 getTrackEvent: function( id ) { 1745 return Popcorn.getTrackEvent.call( null, this, id ); 1746 }, 1747 1748 getLastTrackEventId: function() { 1749 return Popcorn.getLastTrackEventId.call( null, this ); 1750 }, 1751 1752 removeTrackEvent: function( id ) { 1753 1754 Popcorn.removeTrackEvent.call( null, this, id ); 1755 return this; 1756 }, 1757 1758 removePlugin: function( name ) { 1759 Popcorn.removePlugin.call( null, this, name ); 1760 return this; 1761 }, 1762 1763 timeUpdate: function( event ) { 1764 Popcorn.timeUpdate.call( null, this, event ); 1765 return this; 1766 }, 1767 1768 destroy: function() { 1769 Popcorn.destroy.call( null, this ); 1770 return this; 1771 } 1772 }); 1773 1774 // Plugin manifests 1775 Popcorn.manifest = {}; 1776 // Plugins are registered 1777 Popcorn.registry = []; 1778 Popcorn.registryByName = {}; 1779 // An interface for extending Popcorn 1780 // with plugin functionality 1781 Popcorn.plugin = function( name, definition, manifest ) { 1782 1783 if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) { 1784 Popcorn.error( "'" + name + "' is a protected function name" ); 1785 return; 1786 } 1787 1788 // Provides some sugar, but ultimately extends 1789 // the definition into Popcorn.p 1790 var isfn = typeof definition === "function", 1791 blacklist = [ "start", "end", "type", "manifest" ], 1792 methods = [ "_setup", "_teardown", "start", "end", "frame" ], 1793 plugin = {}, 1794 setup; 1795 1796 // combines calls of two function calls into one 1797 var combineFn = function( first, second ) { 1798 1799 first = first || Popcorn.nop; 1800 second = second || Popcorn.nop; 1801 1802 return function() { 1803 first.apply( this, arguments ); 1804 second.apply( this, arguments ); 1805 }; 1806 }; 1807 1808 // If `manifest` arg is undefined, check for manifest within the `definition` object 1809 // If no `definition.manifest`, an empty object is a sufficient fallback 1810 Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {}; 1811 1812 // apply safe, and empty default functions 1813 methods.forEach(function( method ) { 1814 definition[ method ] = safeTry( definition[ method ] || Popcorn.nop, name ); 1815 }); 1816 1817 var pluginFn = function( setup, options ) { 1818 1819 if ( !options ) { 1820 return this; 1821 } 1822 1823 // When the "ranges" property is set and its value is an array, short-circuit 1824 // the pluginFn definition to recall itself with an options object generated from 1825 // each range object in the ranges array. (eg. { start: 15, end: 16 } ) 1826 if ( options.ranges && Popcorn.isArray(options.ranges) ) { 1827 Popcorn.forEach( options.ranges, function( range ) { 1828 // Create a fresh object, extend with current options 1829 // and start/end range object's properties 1830 // Works with in/out as well. 1831 var opts = Popcorn.extend( {}, options, range ); 1832 1833 // Remove the ranges property to prevent infinitely 1834 // entering this condition 1835 delete opts.ranges; 1836 1837 // Call the plugin with the newly created opts object 1838 this[ name ]( opts ); 1839 }, this); 1840 1841 // Return the Popcorn instance to avoid creating an empty track event 1842 return this; 1843 } 1844 1845 // Storing the plugin natives 1846 var natives = options._natives = {}, 1847 compose = "", 1848 originalOpts, manifestOpts; 1849 1850 Popcorn.extend( natives, setup ); 1851 1852 options._natives.type = options._natives.plugin = name; 1853 options._running = false; 1854 1855 natives.start = natives.start || natives[ "in" ]; 1856 natives.end = natives.end || natives[ "out" ]; 1857 1858 if ( options.once ) { 1859 natives.end = combineFn( natives.end, function() { 1860 this.removeTrackEvent( options._id ); 1861 }); 1862 } 1863 1864 // extend teardown to always call end if running 1865 natives._teardown = combineFn(function() { 1866 1867 var args = slice.call( arguments ), 1868 runningPlugins = this.data.running[ natives.type ]; 1869 1870 // end function signature is not the same as teardown, 1871 // put null on the front of arguments for the event parameter 1872 args.unshift( null ); 1873 1874 // only call end if event is running 1875 args[ 1 ]._running && 1876 runningPlugins.splice( runningPlugins.indexOf( options ), 1 ) && 1877 natives.end.apply( this, args ); 1878 1879 args[ 1 ]._running = false; 1880 this.emit( "trackend", 1881 Popcorn.extend( {}, options, { 1882 plugin: natives.type, 1883 type: "trackend", 1884 track: Popcorn.getTrackEvent( this, options.id || options._id ) 1885 }) 1886 ); 1887 }, natives._teardown ); 1888 1889 // extend teardown to always trigger trackteardown after teardown 1890 natives._teardown = combineFn( natives._teardown, function() { 1891 1892 this.emit( "trackteardown", Popcorn.extend( {}, options, { 1893 plugin: name, 1894 type: "trackteardown", 1895 track: Popcorn.getTrackEvent( this, options.id || options._id ) 1896 })); 1897 }); 1898 1899 // default to an empty string if no effect exists 1900 // split string into an array of effects 1901 options.compose = options.compose || []; 1902 if ( typeof options.compose === "string" ) { 1903 options.compose = options.compose.split( " " ); 1904 } 1905 options.effect = options.effect || []; 1906 if ( typeof options.effect === "string" ) { 1907 options.effect = options.effect.split( " " ); 1908 } 1909 1910 // join the two arrays together 1911 options.compose = options.compose.concat( options.effect ); 1912 1913 options.compose.forEach(function( composeOption ) { 1914 1915 // if the requested compose is garbage, throw it away 1916 compose = Popcorn.compositions[ composeOption ] || {}; 1917 1918 // extends previous functions with compose function 1919 methods.forEach(function( method ) { 1920 natives[ method ] = combineFn( natives[ method ], compose[ method ] ); 1921 }); 1922 }); 1923 1924 // Ensure a manifest object, an empty object is a sufficient fallback 1925 options._natives.manifest = manifest; 1926 1927 // Checks for expected properties 1928 if ( !( "start" in options ) ) { 1929 options.start = options[ "in" ] || 0; 1930 } 1931 1932 if ( !options.end && options.end !== 0 ) { 1933 options.end = options[ "out" ] || Number.MAX_VALUE; 1934 } 1935 1936 // Use hasOwn to detect non-inherited toString, since all 1937 // objects will receive a toString - its otherwise undetectable 1938 if ( !hasOwn.call( options, "toString" ) ) { 1939 options.toString = function() { 1940 var props = [ 1941 "start: " + options.start, 1942 "end: " + options.end, 1943 "id: " + (options.id || options._id) 1944 ]; 1945 1946 // Matches null and undefined, allows: false, 0, "" and truthy 1947 if ( options.target != null ) { 1948 props.push( "target: " + options.target ); 1949 } 1950 1951 return name + " ( " + props.join(", ") + " )"; 1952 }; 1953 } 1954 1955 // Resolves 239, 241, 242 1956 if ( !options.target ) { 1957 1958 // Sometimes the manifest may be missing entirely 1959 // or it has an options object that doesn't have a `target` property 1960 manifestOpts = "options" in manifest && manifest.options; 1961 1962 options.target = manifestOpts && "target" in manifestOpts && manifestOpts.target; 1963 } 1964 1965 if ( !options._id && options._natives ) { 1966 // ensure an initial id is there before setup is called 1967 options._id = Popcorn.guid( options._natives.type ); 1968 } 1969 1970 if ( options instanceof TrackEvent ) { 1971 1972 if ( options._natives ) { 1973 // Supports user defined track event id 1974 options._id = options.id || options._id || Popcorn.guid( options._natives.type ); 1975 1976 // Trigger _setup method if exists 1977 if ( options._natives._setup ) { 1978 1979 options._natives._setup.call( this, options ); 1980 1981 this.emit( "tracksetup", Popcorn.extend( {}, options, { 1982 plugin: options._natives.type, 1983 type: "tracksetup", 1984 track: options 1985 })); 1986 } 1987 } 1988 1989 this.data.trackEvents.add( options ); 1990 TrackEvent.start( this, options ); 1991 1992 this.timeUpdate( this, null, true ); 1993 1994 // Store references to user added trackevents in ref table 1995 if ( options._id ) { 1996 Popcorn.addTrackEvent.ref( this, options ); 1997 } 1998 } else { 1999 // Create new track event for this instance 2000 Popcorn.addTrackEvent( this, options ); 2001 } 2002 2003 // Future support for plugin event definitions 2004 // for all of the native events 2005 Popcorn.forEach( setup, function( callback, type ) { 2006 // Don't attempt to create events for certain properties: 2007 // "start", "end", "type", "manifest". Fixes #1365 2008 if ( blacklist.indexOf( type ) === -1 ) { 2009 this.on( type, callback ); 2010 } 2011 }, this ); 2012 2013 return this; 2014 }; 2015 2016 // Extend Popcorn.p with new named definition 2017 // Assign new named definition 2018 Popcorn.p[ name ] = plugin[ name ] = function( id, options ) { 2019 var length = arguments.length, 2020 trackEvent, defaults, mergedSetupOpts, previousOpts, newOpts; 2021 2022 // Shift arguments based on use case 2023 // 2024 // Back compat for: 2025 // p.plugin( options ); 2026 if ( id && !options ) { 2027 options = id; 2028 id = null; 2029 } else { 2030 2031 // Get the trackEvent that matches the given id. 2032 trackEvent = this.getTrackEvent( id ); 2033 2034 // If the track event does not exist, ensure that the options 2035 // object has a proper id 2036 if ( !trackEvent ) { 2037 options.id = id; 2038 2039 // If the track event does exist, merge the updated properties 2040 } else { 2041 2042 newOpts = options; 2043 previousOpts = getPreviousProperties( trackEvent, newOpts ); 2044 2045 // Call the plugins defined update method if provided. Allows for 2046 // custom defined updating for a track event to be defined by the plugin author 2047 if ( trackEvent._natives._update ) { 2048 2049 this.data.trackEvents.remove( trackEvent ); 2050 2051 // It's safe to say that the intent of Start/End will never change 2052 // Update them first before calling update 2053 if ( hasOwn.call( options, "start" ) ) { 2054 trackEvent.start = options.start; 2055 } 2056 2057 if ( hasOwn.call( options, "end" ) ) { 2058 trackEvent.end = options.end; 2059 } 2060 2061 TrackEvent.end( this, trackEvent ); 2062 2063 if ( isfn ) { 2064 definition.call( this, trackEvent ); 2065 } 2066 2067 trackEvent._natives._update.call( this, trackEvent, options ); 2068 2069 this.data.trackEvents.add( trackEvent ); 2070 TrackEvent.start( this, trackEvent ); 2071 } else { 2072 // This branch is taken when there is no explicitly defined 2073 // _update method for a plugin. Which will occur either explicitly or 2074 // as a result of the plugin definition being a function that _returns_ 2075 // a definition object. 2076 // 2077 // In either case, this path can ONLY be reached for TrackEvents that 2078 // already exist. 2079 2080 // Directly update the TrackEvent instance. 2081 // This supports TrackEvent invariant enforcement. 2082 Popcorn.extend( trackEvent, options ); 2083 2084 this.data.trackEvents.remove( id ); 2085 2086 // If a _teardown function was defined, 2087 // enforce for track event removals 2088 if ( trackEvent._natives._teardown ) { 2089 trackEvent._natives._teardown.call( this, trackEvent ); 2090 } 2091 2092 // Update track event references 2093 Popcorn.removeTrackEvent.ref( this, id ); 2094 2095 if ( isfn ) { 2096 pluginFn.call( this, definition.call( this, trackEvent ), trackEvent ); 2097 } else { 2098 2099 // Supports user defined track event id 2100 trackEvent._id = trackEvent.id || trackEvent._id || Popcorn.guid( trackEvent._natives.type ); 2101 2102 if ( trackEvent._natives && trackEvent._natives._setup ) { 2103 2104 trackEvent._natives._setup.call( this, trackEvent ); 2105 2106 this.emit( "tracksetup", Popcorn.extend( {}, trackEvent, { 2107 plugin: trackEvent._natives.type, 2108 type: "tracksetup", 2109 track: trackEvent 2110 })); 2111 } 2112 2113 this.data.trackEvents.add( trackEvent ); 2114 TrackEvent.start( this, trackEvent ); 2115 2116 this.timeUpdate( this, null, true ); 2117 2118 // Store references to user added trackevents in ref table 2119 Popcorn.addTrackEvent.ref( this, trackEvent ); 2120 } 2121 2122 // Fire an event with change information 2123 this.emit( "trackchange", { 2124 id: trackEvent.id, 2125 type: "trackchange", 2126 previousValue: previousOpts, 2127 currentValue: trackEvent, 2128 track: trackEvent 2129 }); 2130 2131 return this; 2132 } 2133 2134 if ( trackEvent._natives.type !== "cue" ) { 2135 // Fire an event with change information 2136 this.emit( "trackchange", { 2137 id: trackEvent.id, 2138 type: "trackchange", 2139 previousValue: previousOpts, 2140 currentValue: newOpts, 2141 track: trackEvent 2142 }); 2143 } 2144 2145 return this; 2146 } 2147 } 2148 2149 this.data.running[ name ] = this.data.running[ name ] || []; 2150 2151 // Merge with defaults if they exist, make sure per call is prioritized 2152 defaults = ( this.options.defaults && this.options.defaults[ name ] ) || {}; 2153 mergedSetupOpts = Popcorn.extend( {}, defaults, options ); 2154 2155 pluginFn.call( this, isfn ? definition.call( this, mergedSetupOpts ) : definition, 2156 mergedSetupOpts ); 2157 2158 return this; 2159 }; 2160 2161 // if the manifest parameter exists we should extend it onto the definition object 2162 // so that it shows up when calling Popcorn.registry and Popcorn.registryByName 2163 if ( manifest ) { 2164 Popcorn.extend( definition, { 2165 manifest: manifest 2166 }); 2167 } 2168 2169 // Push into the registry 2170 var entry = { 2171 fn: plugin[ name ], 2172 definition: definition, 2173 base: definition, 2174 parents: [], 2175 name: name 2176 }; 2177 Popcorn.registry.push( 2178 Popcorn.extend( plugin, entry, { 2179 type: name 2180 }) 2181 ); 2182 Popcorn.registryByName[ name ] = entry; 2183 2184 return plugin; 2185 }; 2186 2187 // Storage for plugin function errors 2188 Popcorn.plugin.errors = []; 2189 2190 // Returns wrapped plugin function 2191 function safeTry( fn, pluginName ) { 2192 return function() { 2193 2194 // When Popcorn.plugin.debug is true, do not suppress errors 2195 if ( Popcorn.plugin.debug ) { 2196 return fn.apply( this, arguments ); 2197 } 2198 2199 try { 2200 return fn.apply( this, arguments ); 2201 } catch ( ex ) { 2202 2203 // Push plugin function errors into logging queue 2204 Popcorn.plugin.errors.push({ 2205 plugin: pluginName, 2206 thrown: ex, 2207 source: fn.toString() 2208 }); 2209 2210 // Trigger an error that the instance can listen for 2211 // and react to 2212 this.emit( "pluginerror", Popcorn.plugin.errors ); 2213 } 2214 }; 2215 } 2216 2217 // Debug-mode flag for plugin development 2218 // True for Popcorn development versions, false for stable/tagged versions 2219 Popcorn.plugin.debug = ( Popcorn.version === "@" + "VERSION" ); 2220 2221 // removePlugin( type ) removes all tracks of that from all instances of popcorn 2222 // removePlugin( obj, type ) removes all tracks of type from obj, where obj is a single instance of popcorn 2223 Popcorn.removePlugin = function( obj, name ) { 2224 2225 // Check if we are removing plugin from an instance or from all of Popcorn 2226 if ( !name ) { 2227 2228 // Fix the order 2229 name = obj; 2230 obj = Popcorn.p; 2231 2232 if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) { 2233 Popcorn.error( "'" + name + "' is a protected function name" ); 2234 return; 2235 } 2236 2237 var registryLen = Popcorn.registry.length, 2238 registryIdx; 2239 2240 // remove plugin reference from registry 2241 for ( registryIdx = 0; registryIdx < registryLen; registryIdx++ ) { 2242 if ( Popcorn.registry[ registryIdx ].name === name ) { 2243 Popcorn.registry.splice( registryIdx, 1 ); 2244 delete Popcorn.registryByName[ name ]; 2245 delete Popcorn.manifest[ name ]; 2246 2247 // delete the plugin 2248 delete obj[ name ]; 2249 2250 // plugin found and removed, stop checking, we are done 2251 return; 2252 } 2253 } 2254 2255 } 2256 2257 var byStart = obj.data.trackEvents.byStart, 2258 byEnd = obj.data.trackEvents.byEnd, 2259 animating = obj.data.trackEvents.animating, 2260 idx, sl; 2261 2262 // remove all trackEvents 2263 for ( idx = 0, sl = byStart.length; idx < sl; idx++ ) { 2264 2265 if ( byStart[ idx ] && byStart[ idx ]._natives && byStart[ idx ]._natives.type === name ) { 2266 2267 byStart[ idx ]._natives._teardown && byStart[ idx ]._natives._teardown.call( obj, byStart[ idx ] ); 2268 2269 byStart.splice( idx, 1 ); 2270 2271 // update for loop if something removed, but keep checking 2272 idx--; sl--; 2273 if ( obj.data.trackEvents.startIndex <= idx ) { 2274 obj.data.trackEvents.startIndex--; 2275 obj.data.trackEvents.endIndex--; 2276 } 2277 } 2278 2279 // clean any remaining references in the end index 2280 // we do this seperate from the above check because they might not be in the same order 2281 if ( byEnd[ idx ] && byEnd[ idx ]._natives && byEnd[ idx ]._natives.type === name ) { 2282 2283 byEnd.splice( idx, 1 ); 2284 } 2285 } 2286 2287 //remove all animating events 2288 for ( idx = 0, sl = animating.length; idx < sl; idx++ ) { 2289 2290 if ( animating[ idx ] && animating[ idx ]._natives && animating[ idx ]._natives.type === name ) { 2291 2292 animating.splice( idx, 1 ); 2293 2294 // update for loop if something removed, but keep checking 2295 idx--; sl--; 2296 } 2297 } 2298 2299 }; 2300 2301 Popcorn.compositions = {}; 2302 2303 // Plugin inheritance 2304 Popcorn.compose = function( name, definition, manifest ) { 2305 2306 // If `manifest` arg is undefined, check for manifest within the `definition` object 2307 // If no `definition.manifest`, an empty object is a sufficient fallback 2308 Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {}; 2309 2310 // register the effect by name 2311 Popcorn.compositions[ name ] = definition; 2312 }; 2313 2314 Popcorn.plugin.effect = Popcorn.effect = Popcorn.compose; 2315 2316 var rnaiveExpr = /^(?:\.|#|\[)/; 2317 2318 // Basic DOM utilities and helpers API. See #1037 2319 Popcorn.dom = { 2320 debug: false, 2321 // Popcorn.dom.find( selector, context ) 2322 // 2323 // Returns the first element that matches the specified selector 2324 // Optionally provide a context element, defaults to `document` 2325 // 2326 // eg. 2327 // Popcorn.dom.find("video") returns the first video element 2328 // Popcorn.dom.find("#foo") returns the first element with `id="foo"` 2329 // Popcorn.dom.find("foo") returns the first element with `id="foo"` 2330 // Note: Popcorn.dom.find("foo") is the only allowed deviation 2331 // from valid querySelector selector syntax 2332 // 2333 // Popcorn.dom.find(".baz") returns the first element with `class="baz"` 2334 // Popcorn.dom.find("[preload]") returns the first element with `preload="..."` 2335 // ... 2336 // See https://developer.mozilla.org/En/DOM/Document.querySelector 2337 // 2338 // 2339 find: function( selector, context ) { 2340 var node = null; 2341 2342 // Default context is the `document` 2343 context = context || document; 2344 2345 if ( selector ) { 2346 2347 // If the selector does not begin with "#", "." or "[", 2348 // it could be either a nodeName or ID w/o "#" 2349 if ( !rnaiveExpr.test( selector ) ) { 2350 2351 // Try finding an element that matches by ID first 2352 node = document.getElementById( selector ); 2353 2354 // If a match was found by ID, return the element 2355 if ( node !== null ) { 2356 return node; 2357 } 2358 } 2359 // Assume no elements have been found yet 2360 // Catch any invalid selector syntax errors and bury them. 2361 try { 2362 node = context.querySelector( selector ); 2363 } catch ( e ) { 2364 if ( Popcorn.dom.debug ) { 2365 throw new Error(e); 2366 } 2367 } 2368 } 2369 return node; 2370 } 2371 }; 2372 2373 // Cache references to reused RegExps 2374 var rparams = /\?/, 2375 // XHR Setup object 2376 setup = { 2377 ajax: null, 2378 url: "", 2379 data: "", 2380 dataType: "", 2381 success: Popcorn.nop, 2382 type: "GET", 2383 async: true, 2384 contentType: "application/x-www-form-urlencoded; charset=UTF-8" 2385 }; 2386 2387 Popcorn.xhr = function( options ) { 2388 var settings; 2389 2390 options.dataType = options.dataType && options.dataType.toLowerCase() || null; 2391 2392 if ( options.dataType && 2393 ( options.dataType === "jsonp" || options.dataType === "script" ) ) { 2394 2395 Popcorn.xhr.getJSONP( 2396 options.url, 2397 options.success, 2398 options.dataType === "script" 2399 ); 2400 return; 2401 } 2402 2403 // Merge the "setup" defaults and custom "options" 2404 // into a new plain object. 2405 settings = Popcorn.extend( {}, setup, options ); 2406 2407 // Create new XMLHttpRequest object 2408 settings.ajax = new XMLHttpRequest(); 2409 2410 if ( settings.ajax ) { 2411 2412 if ( settings.type === "GET" && settings.data ) { 2413 2414 // append query string 2415 settings.url += ( rparams.test( settings.url ) ? "&" : "?" ) + settings.data; 2416 2417 // Garbage collect and reset settings.data 2418 settings.data = null; 2419 } 2420 2421 // Open the request 2422 settings.ajax.open( settings.type, settings.url, settings.async ); 2423 2424 // For POST, set the content-type request header 2425 if ( settings.type === "POST" ) { 2426 settings.ajax.setRequestHeader( 2427 "Content-Type", settings.contentType 2428 ); 2429 } 2430 2431 settings.ajax.send( settings.data || null ); 2432 2433 return Popcorn.xhr.httpData( settings ); 2434 } 2435 }; 2436 2437 2438 Popcorn.xhr.httpData = function( settings ) { 2439 2440 var data, json = null, 2441 parser, xml = null; 2442 2443 settings.ajax.onreadystatechange = function() { 2444 2445 if ( settings.ajax.readyState === 4 ) { 2446 2447 try { 2448 json = JSON.parse( settings.ajax.responseText ); 2449 } catch( e ) { 2450 //suppress 2451 } 2452 2453 data = { 2454 xml: settings.ajax.responseXML, 2455 text: settings.ajax.responseText, 2456 json: json 2457 }; 2458 2459 // Normalize: data.xml is non-null in IE9 regardless of if response is valid xml 2460 if ( !data.xml || !data.xml.documentElement ) { 2461 data.xml = null; 2462 2463 try { 2464 parser = new DOMParser(); 2465 xml = parser.parseFromString( settings.ajax.responseText, "text/xml" ); 2466 2467 if ( !xml.getElementsByTagName( "parsererror" ).length ) { 2468 data.xml = xml; 2469 } 2470 } catch ( e ) { 2471 // data.xml remains null 2472 } 2473 } 2474 2475 // If a dataType was specified, return that type of data 2476 if ( settings.dataType ) { 2477 data = data[ settings.dataType ]; 2478 } 2479 2480 2481 settings.success.call( settings.ajax, data ); 2482 2483 } 2484 }; 2485 return data; 2486 }; 2487 2488 Popcorn.xhr.getJSONP = function( url, success, isScript ) { 2489 2490 var head = document.head || document.getElementsByTagName( "head" )[ 0 ] || document.documentElement, 2491 script = document.createElement( "script" ), 2492 isFired = false, 2493 params = [], 2494 rjsonp = /(=)\?(?=&|$)|\?\?/, 2495 replaceInUrl, prefix, paramStr, callback, callparam; 2496 2497 if ( !isScript ) { 2498 2499 // is there a calback already in the url 2500 callparam = url.match( /(callback=[^&]*)/ ); 2501 2502 if ( callparam !== null && callparam.length ) { 2503 2504 prefix = callparam[ 1 ].split( "=" )[ 1 ]; 2505 2506 // Since we need to support developer specified callbacks 2507 // and placeholders in harmony, make sure matches to "callback=" 2508 // aren't just placeholders. 2509 // We coded ourselves into a corner here. 2510 // JSONP callbacks should never have been 2511 // allowed to have developer specified callbacks 2512 if ( prefix === "?" ) { 2513 prefix = "jsonp"; 2514 } 2515 2516 // get the callback name 2517 callback = Popcorn.guid( prefix ); 2518 2519 // replace existing callback name with unique callback name 2520 url = url.replace( /(callback=[^&]*)/, "callback=" + callback ); 2521 } else { 2522 2523 callback = Popcorn.guid( "jsonp" ); 2524 2525 if ( rjsonp.test( url ) ) { 2526 url = url.replace( rjsonp, "$1" + callback ); 2527 } 2528 2529 // split on first question mark, 2530 // this is to capture the query string 2531 params = url.split( /\?(.+)?/ ); 2532 2533 // rebuild url with callback 2534 url = params[ 0 ] + "?"; 2535 if ( params[ 1 ] ) { 2536 url += params[ 1 ] + "&"; 2537 } 2538 url += "callback=" + callback; 2539 } 2540 2541 // Define the JSONP success callback globally 2542 window[ callback ] = function( data ) { 2543 // Fire success callbacks 2544 success && success( data ); 2545 isFired = true; 2546 }; 2547 } 2548 2549 script.addEventListener( "load", function() { 2550 2551 // Handling remote script loading callbacks 2552 if ( isScript ) { 2553 // getScript 2554 success && success(); 2555 } 2556 2557 // Executing for JSONP requests 2558 if ( isFired ) { 2559 // Garbage collect the callback 2560 delete window[ callback ]; 2561 } 2562 // Garbage collect the script resource 2563 head.removeChild( script ); 2564 }, false ); 2565 2566 script.addEventListener( "error", function( e ) { 2567 // Handling remote script loading callbacks 2568 success && success( { error: e } ); 2569 2570 // Executing for JSONP requests 2571 if ( !isScript ) { 2572 // Garbage collect the callback 2573 delete window[ callback ]; 2574 } 2575 // Garbage collect the script resource 2576 head.removeChild( script ); 2577 }, false ); 2578 2579 script.src = url; 2580 head.insertBefore( script, head.firstChild ); 2581 2582 return; 2583 }; 2584 2585 Popcorn.getJSONP = Popcorn.xhr.getJSONP; 2586 2587 Popcorn.getScript = Popcorn.xhr.getScript = function( url, success ) { 2588 2589 return Popcorn.xhr.getJSONP( url, success, true ); 2590 }; 2591 2592 Popcorn.util = { 2593 // Simple function to parse a timestamp into seconds 2594 // Acceptable formats are: 2595 // HH:MM:SS.MMM 2596 // HH:MM:SS;FF 2597 // Hours and minutes are optional. They default to 0 2598 toSeconds: function( timeStr, framerate ) { 2599 // Hours and minutes are optional 2600 // Seconds must be specified 2601 // Seconds can be followed by milliseconds OR by the frame information 2602 var validTimeFormat = /^([0-9]+:){0,2}[0-9]+([.;][0-9]+)?$/, 2603 errorMessage = "Invalid time format", 2604 digitPairs, lastIndex, lastPair, firstPair, 2605 frameInfo, frameTime; 2606 2607 if ( typeof timeStr === "number" ) { 2608 return timeStr; 2609 } 2610 2611 if ( typeof timeStr === "string" && 2612 !validTimeFormat.test( timeStr ) ) { 2613 Popcorn.error( errorMessage ); 2614 } 2615 2616 digitPairs = timeStr.split( ":" ); 2617 lastIndex = digitPairs.length - 1; 2618 lastPair = digitPairs[ lastIndex ]; 2619 2620 // Fix last element: 2621 if ( lastPair.indexOf( ";" ) > -1 ) { 2622 2623 frameInfo = lastPair.split( ";" ); 2624 frameTime = 0; 2625 2626 if ( framerate && ( typeof framerate === "number" ) ) { 2627 frameTime = parseFloat( frameInfo[ 1 ], 10 ) / framerate; 2628 } 2629 2630 digitPairs[ lastIndex ] = parseInt( frameInfo[ 0 ], 10 ) + frameTime; 2631 } 2632 2633 firstPair = digitPairs[ 0 ]; 2634 2635 return { 2636 2637 1: parseFloat( firstPair, 10 ), 2638 2639 2: ( parseInt( firstPair, 10 ) * 60 ) + 2640 parseFloat( digitPairs[ 1 ], 10 ), 2641 2642 3: ( parseInt( firstPair, 10 ) * 3600 ) + 2643 ( parseInt( digitPairs[ 1 ], 10 ) * 60 ) + 2644 parseFloat( digitPairs[ 2 ], 10 ) 2645 2646 }[ digitPairs.length || 1 ]; 2647 } 2648 }; 2649 2650 // alias for exec function 2651 Popcorn.p.cue = Popcorn.p.exec; 2652 2653 // Protected API methods 2654 Popcorn.protect = { 2655 natives: getKeys( Popcorn.p ).map(function( val ) { 2656 return val.toLowerCase(); 2657 }) 2658 }; 2659 2660 // Setup logging for deprecated methods 2661 Popcorn.forEach({ 2662 // Deprecated: Recommended 2663 "listen": "on", 2664 "unlisten": "off", 2665 "trigger": "emit", 2666 "exec": "cue" 2667 2668 }, function( recommend, api ) { 2669 var original = Popcorn.p[ api ]; 2670 // Override the deprecated api method with a method of the same name 2671 // that logs a warning and defers to the new recommended method 2672 Popcorn.p[ api ] = function() { 2673 if ( typeof console !== "undefined" && console.warn ) { 2674 console.warn( 2675 "Deprecated method '" + api + "', " + 2676 (recommend == null ? "do not use." : "use '" + recommend + "' instead." ) 2677 ); 2678 2679 // Restore api after first warning 2680 Popcorn.p[ api ] = original; 2681 } 2682 return Popcorn.p[ recommend ].apply( this, [].slice.call( arguments ) ); 2683 }; 2684 }); 2685 2686 2687 // Exposes Popcorn to global context 2688 global.Popcorn = Popcorn; 2689 2690})(window, window.document); 2691