popcornjs-rails 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ });