popcornjs-rails 1.1.2 → 1.2.0

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