popcornjs-rails 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  /*
2
- * popcorn.js version 1.2
2
+ * popcorn.js version 1.3
3
3
  * http://popcornjs.org
4
4
  *
5
5
  * Copyright 2011, Mozilla Foundation
@@ -14,8 +14,7 @@
14
14
  isSupported: false
15
15
  };
16
16
 
17
- var methods = ( "removeInstance addInstance getInstanceById removeInstanceById " +
18
- "forEach extend effects error guid sizeOf isArray nop position disable enable destroy" +
17
+ var methods = ( "byId forEach extend effects error guid sizeOf isArray nop position disable enable destroy" +
19
18
  "addTrackEvent removeTrackEvent getTrackEvents getTrackEvent getLastTrackEventId " +
20
19
  "timeUpdate plugin removePlugin compose effect xhr getJSONP getScript" ).split(/\s+/);
21
20
 
@@ -38,9 +37,6 @@
38
37
  // Copy global Popcorn (may not exist)
39
38
  _Popcorn = global.Popcorn,
40
39
 
41
- // ID string matching
42
- rIdExp = /^(#([\w\-\_\.]+))$/,
43
-
44
40
  // Ready fn cache
45
41
  readyStack = [],
46
42
  readyBound = false,
@@ -82,61 +78,6 @@
82
78
  })( obj );
83
79
  },
84
80
 
85
- refresh = function( obj ) {
86
- var currentTime = obj.media.currentTime,
87
- animation = obj.options.frameAnimation,
88
- disabled = obj.data.disabled,
89
- tracks = obj.data.trackEvents,
90
- animating = tracks.animating,
91
- start = tracks.startIndex,
92
- registryByName = Popcorn.registryByName,
93
- animIndex = 0,
94
- byStart, natives, type;
95
-
96
- start = Math.min( start + 1, tracks.byStart.length - 2 );
97
-
98
- while ( start > 0 && tracks.byStart[ start ] ) {
99
-
100
- byStart = tracks.byStart[ start ];
101
- natives = byStart._natives;
102
- type = natives && natives.type;
103
-
104
- if ( !natives ||
105
- ( !!registryByName[ type ] || !!obj[ type ] ) ) {
106
-
107
- if ( ( byStart.start <= currentTime && byStart.end > currentTime ) &&
108
- disabled.indexOf( type ) === -1 ) {
109
-
110
- if ( !byStart._running ) {
111
- byStart._running = true;
112
- natives.start.call( obj, null, byStart );
113
-
114
- // if the 'frameAnimation' option is used,
115
- // push the current byStart object into the `animating` cue
116
- if ( animation &&
117
- ( byStart && byStart._running && byStart.natives.frame ) ) {
118
-
119
- natives.frame.call( obj, null, byStart, currentTime );
120
- }
121
- }
122
- } else if ( byStart._running === true ) {
123
-
124
- byStart._running = false;
125
- natives.end.call( obj, null, byStart );
126
-
127
- if ( animation && byStart._natives.frame ) {
128
- animIndex = animating.indexOf( byStart );
129
- if ( animIndex >= 0 ) {
130
- animating.splice( animIndex, 1 );
131
- }
132
- }
133
- }
134
- }
135
-
136
- start--;
137
- }
138
- },
139
-
140
81
  // Declare constructor
141
82
  // Returns an instance object.
142
83
  Popcorn = function( entity, options ) {
@@ -145,7 +86,7 @@
145
86
  };
146
87
 
147
88
  // Popcorn API version, automatically inserted via build system.
148
- Popcorn.version = "1.2";
89
+ Popcorn.version = "1.3";
149
90
 
150
91
  // Boolean flag allowing a client to determine if Popcorn can be supported
151
92
  Popcorn.isSupported = true;
@@ -159,7 +100,7 @@
159
100
 
160
101
  init: function( entity, options ) {
161
102
 
162
- var matches,
103
+ var matches, nodeName,
163
104
  self = this;
164
105
 
165
106
  // Supports Popcorn(function () { /../ })
@@ -207,31 +148,47 @@
207
148
  return;
208
149
  }
209
150
 
210
- // Check if entity is a valid string id
211
- matches = rIdExp.exec( entity );
151
+ if ( typeof entity === "string" ) {
152
+ try {
153
+ matches = document.querySelector( entity );
154
+ } catch( e ) {
155
+ throw new Error( "Popcorn.js Error: Invalid media element selector: " + entity );
156
+ }
157
+ }
212
158
 
213
159
  // Get media element by id or object reference
214
- this.media = matches && matches.length && matches[ 2 ] ?
215
- document.getElementById( matches[ 2 ] ) :
216
- entity;
160
+ this.media = matches || entity;
217
161
 
218
- // Create an audio or video element property reference
219
- this[ ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video" ] = this.media;
162
+ // inner reference to this media element's nodeName string value
163
+ nodeName = ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video";
220
164
 
221
- // Register new instance
222
- Popcorn.instances.push( this );
165
+ // Create an audio or video element property reference
166
+ this[ nodeName ] = this.media;
223
167
 
224
168
  this.options = options || {};
225
169
 
170
+ // Resolve custom ID or default prefixed ID
171
+ this.id = this.options.id || Popcorn.guid( nodeName );
172
+
173
+ // Throw if an attempt is made to use an ID that already exists
174
+ if ( Popcorn.byId( this.id ) ) {
175
+ throw new Error( "Popcorn.js Error: Cannot use duplicate ID (" + this.id + ")" );
176
+ }
177
+
226
178
  this.isDestroyed = false;
227
179
 
228
180
  this.data = {
229
181
 
182
+ // data structure of all
183
+ running: {
184
+ cue: []
185
+ },
186
+
230
187
  // Executed by either timeupdate event or in rAF loop
231
188
  timeUpdate: Popcorn.nop,
232
189
 
233
190
  // Allows disabling a plugin per instance
234
- disabled: [],
191
+ disabled: {},
235
192
 
236
193
  // Stores DOM event queues by type
237
194
  events: {},
@@ -268,12 +225,26 @@
268
225
  }
269
226
  };
270
227
 
228
+ // Register new instance
229
+ Popcorn.instances.push( this );
230
+
271
231
  // function to fire when video is ready
272
232
  var isReady = function() {
273
233
 
234
+ // chrome bug: http://code.google.com/p/chromium/issues/detail?id=119598
235
+ // it is possible the video's time is less than 0
236
+ // this has the potential to call track events more than once, when they should not
237
+ // start: 0, end: 1 will start, end, start again, when it should just start
238
+ // just setting it to 0 if it is below 0 fixes this issue
239
+ if ( self.media.currentTime < 0 ) {
240
+
241
+ self.media.currentTime = 0;
242
+ }
243
+
274
244
  self.media.removeEventListener( "loadeddata", isReady, false );
275
245
 
276
- var duration, videoDurationPlus;
246
+ var duration, videoDurationPlus,
247
+ runningPlugins, runningPlugin, rpLength, rpNatives;
277
248
 
278
249
  // Adding padding to the front and end of the arrays
279
250
  // this is so we do not fall off either end
@@ -288,6 +259,7 @@
288
259
  });
289
260
 
290
261
  if ( self.options.frameAnimation ) {
262
+
291
263
  // if Popcorn is created with frameAnimation option set to true,
292
264
  // requestAnimFrame is used instead of "timeupdate" media event.
293
265
  // This is for greater frame time accuracy, theoretically up to
@@ -296,6 +268,25 @@
296
268
 
297
269
  Popcorn.timeUpdate( self, {} );
298
270
 
271
+ // fire frame for each enabled active plugin of every type
272
+ Popcorn.forEach( Popcorn.manifest, function( key, val ) {
273
+
274
+ runningPlugins = self.data.running[ val ];
275
+
276
+ // ensure there are running plugins on this type on this instance
277
+ if ( runningPlugins ) {
278
+
279
+ rpLength = runningPlugins.length;
280
+ for ( var i = 0; i < rpLength; i++ ) {
281
+
282
+ runningPlugin = runningPlugins[ i ];
283
+ rpNatives = runningPlugin._natives;
284
+ rpNatives && rpNatives.frame &&
285
+ rpNatives.frame.call( self, {}, runningPlugin, self.currentTime() );
286
+ }
287
+ }
288
+ });
289
+
299
290
  self.emit( "timeupdate" );
300
291
 
301
292
  !self.isDestroyed && requestAnimFrame( self.data.timeUpdate );
@@ -315,6 +306,13 @@
315
306
  }
316
307
  };
317
308
 
309
+ Object.defineProperty( this, "error", {
310
+ get: function() {
311
+
312
+ return self.media.error;
313
+ }
314
+ });
315
+
318
316
  if ( self.media.readyState >= 2 ) {
319
317
 
320
318
  isReady();
@@ -331,6 +329,20 @@
331
329
  // Allows chaining methods to instances
332
330
  Popcorn.p.init.prototype = Popcorn.p;
333
331
 
332
+ Popcorn.byId = function( str ) {
333
+ var instances = Popcorn.instances,
334
+ length = instances.length,
335
+ i = 0;
336
+
337
+ for ( ; i < length; i++ ) {
338
+ if ( instances[ i ].id === str ) {
339
+ return instances[ i ];
340
+ }
341
+ }
342
+
343
+ return null;
344
+ };
345
+
334
346
  Popcorn.forEach = function( obj, fn, context ) {
335
347
 
336
348
  if ( !obj || !fn ) {
@@ -436,32 +448,38 @@
436
448
 
437
449
  disable: function( instance, plugin ) {
438
450
 
439
- var disabled = instance.data.disabled;
451
+ if ( !instance.data.disabled[ plugin ] ) {
440
452
 
441
- if ( disabled.indexOf( plugin ) === -1 ) {
442
- disabled.push( plugin );
443
- }
453
+ instance.data.disabled[ plugin ] = true;
454
+
455
+ for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) {
444
456
 
445
- refresh( instance );
457
+ event = instance.data.running[ plugin ][ i ];
458
+ event._natives.end.call( instance, null, event );
459
+ }
460
+ }
446
461
 
447
462
  return instance;
448
463
  },
449
464
  enable: function( instance, plugin ) {
450
465
 
451
- var disabled = instance.data.disabled,
452
- index = disabled.indexOf( plugin );
466
+ if ( instance.data.disabled[ plugin ] ) {
453
467
 
454
- if ( index > -1 ) {
455
- disabled.splice( index, 1 );
456
- }
468
+ instance.data.disabled[ plugin ] = false;
469
+
470
+ for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) {
457
471
 
458
- refresh( instance );
472
+ event = instance.data.running[ plugin ][ i ];
473
+ event._natives.start.call( instance, null, event );
474
+ }
475
+ }
459
476
 
460
477
  return instance;
461
478
  },
462
479
  destroy: function( instance ) {
463
480
  var events = instance.data.events,
464
- singleEvent, item, fn;
481
+ trackEvents = instance.data.trackEvents,
482
+ singleEvent, item, fn, plugin;
465
483
 
466
484
  // Iterate through all events and remove them
467
485
  for ( item in events ) {
@@ -472,6 +490,15 @@
472
490
  events[ item ] = null;
473
491
  }
474
492
 
493
+ // remove all plugins off the given instance
494
+ for ( plugin in Popcorn.registryByName ) {
495
+ Popcorn.removePlugin( instance, plugin );
496
+ }
497
+
498
+ // Remove all data.trackEvents #1178
499
+ trackEvents.byStart.length = 0;
500
+ trackEvents.byEnd.length = 0;
501
+
475
502
  if ( !instance.isDestroyed ) {
476
503
  instance.data.timeUpdate && instance.media.removeEventListener( "timeupdate", instance.data.timeUpdate, false );
477
504
  instance.isDestroyed = true;
@@ -496,6 +523,7 @@
496
523
  Popcorn.forEach( methods.split( /\s+/g ), function( name ) {
497
524
 
498
525
  ret[ name ] = function( arg ) {
526
+ var previous;
499
527
 
500
528
  if ( typeof this.media[ name ] === "function" ) {
501
529
 
@@ -512,11 +540,22 @@
512
540
  return this;
513
541
  }
514
542
 
515
-
516
543
  if ( arg != null ) {
544
+ // Capture the current value of the attribute property
545
+ previous = this.media[ name ];
517
546
 
547
+ // Set the attribute property with the new value
518
548
  this.media[ name ] = arg;
519
549
 
550
+ // If the new value is not the same as the old value
551
+ // emit an "attrchanged event"
552
+ if ( previous !== arg ) {
553
+ this.emit( "attrchange", {
554
+ attribute: name,
555
+ previousValue: previous,
556
+ currentValue: arg
557
+ });
558
+ }
520
559
  return this;
521
560
  }
522
561
 
@@ -539,14 +578,103 @@
539
578
 
540
579
  // Rounded currentTime
541
580
  roundTime: function() {
542
- return -~this.media.currentTime;
581
+ return Math.round( this.media.currentTime );
543
582
  },
544
583
 
545
584
  // Attach an event to a single point in time
546
- exec: function( time, fn ) {
585
+ exec: function( id, time, fn ) {
586
+ var length = arguments.length,
587
+ trackEvent, sec;
588
+
589
+ // Check if first could possibly be a SMPTE string
590
+ // p.cue( "smpte string", fn );
591
+ // try/catch avoid awful throw in Popcorn.util.toSeconds
592
+ // TODO: Get rid of that, replace with NaN return?
593
+ try {
594
+ sec = Popcorn.util.toSeconds( id );
595
+ } catch ( e ) {}
596
+
597
+ // If it can be converted into a number then
598
+ // it's safe to assume that the string was SMPTE
599
+ if ( typeof sec === "number" ) {
600
+ id = sec;
601
+ }
602
+
603
+ // Shift arguments based on use case
604
+ //
605
+ // Back compat for:
606
+ // p.cue( time, fn );
607
+ if ( typeof id === "number" && length === 2 ) {
608
+ fn = time;
609
+ time = id;
610
+ id = Popcorn.guid( "cue" );
611
+ } else {
612
+ // Support for new forms
613
+
614
+ // p.cue( "empty-cue" );
615
+ if ( length === 1 ) {
616
+ // Set a time for an empty cue. It's not important what
617
+ // the time actually is, because the cue is a no-op
618
+ time = -1;
619
+
620
+ } else {
621
+
622
+ // Get the trackEvent that matches the given id.
623
+ trackEvent = this.getTrackEvent( id );
624
+
625
+ if ( trackEvent ) {
626
+
627
+ // p.cue( "my-id", 12 );
628
+ // p.cue( "my-id", function() { ... });
629
+ if ( typeof id === "string" && length === 2 ) {
630
+
631
+ // p.cue( "my-id", 12 );
632
+ // The path will update the cue time.
633
+ if ( typeof time === "number" ) {
634
+ // Re-use existing trackEvent start callback
635
+ fn = trackEvent._natives.start;
636
+ }
637
+
638
+ // p.cue( "my-id", function() { ... });
639
+ // The path will update the cue function
640
+ if ( typeof time === "function" ) {
641
+ fn = time;
642
+ // Re-use existing trackEvent start time
643
+ time = trackEvent.start;
644
+ }
645
+ }
646
+ } else {
647
+
648
+ if ( length >= 2 ) {
649
+
650
+ // p.cue( "a", "00:00:00");
651
+ if ( typeof time === "string" ) {
652
+ try {
653
+ sec = Popcorn.util.toSeconds( time );
654
+ } catch ( e ) {}
655
+
656
+ time = sec;
657
+ }
658
+
659
+ // p.cue( "b", 11 );
660
+ if ( typeof time === "number" ) {
661
+ fn = Popcorn.nop();
662
+ }
663
+
664
+ // p.cue( "c", function() {});
665
+ if ( typeof time === "function" ) {
666
+ fn = time;
667
+ time = -1;
668
+ }
669
+ }
670
+ }
671
+ }
672
+ }
547
673
 
548
674
  // Creating a one second track event with an empty end
675
+ // Or update an existing track event with new values
549
676
  Popcorn.addTrackEvent( this, {
677
+ id: id,
550
678
  start: time,
551
679
  end: time + 1,
552
680
  _running: false,
@@ -598,7 +726,7 @@
598
726
 
599
727
  // Toggle a plugin's playback behaviour (on or off) per instance
600
728
  toggle: function( plugin ) {
601
- return Popcorn[ this.data.disabled.indexOf( plugin ) > -1 ? "enable" : "disable" ]( this, plugin );
729
+ return Popcorn[ this.data.disabled[ plugin ] ? "enable" : "disable" ]( this, plugin );
602
730
  },
603
731
 
604
732
  // Set default values for plugin options objects per instance
@@ -634,7 +762,7 @@
634
762
  Popcorn.Events = {
635
763
  UIEvents: "blur focus focusin focusout load resize scroll unload",
636
764
  MouseEvents: "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave click dblclick",
637
- Events: "loadstart progress suspend emptied stalled play pause " +
765
+ Events: "loadstart progress suspend emptied stalled play pause error " +
638
766
  "loadedmetadata loadeddata waiting playing canplay canplaythrough " +
639
767
  "seeking seeked timeupdate ended ratechange durationchange volumechange"
640
768
  };
@@ -847,6 +975,23 @@
847
975
 
848
976
  // Internal Only - Adds track events to the instance object
849
977
  Popcorn.addTrackEvent = function( obj, track ) {
978
+ var trackEvent, isUpdate, eventType;
979
+
980
+ // Do a lookup for existing trackevents with this id
981
+ if ( track.id ) {
982
+ trackEvent = obj.getTrackEvent( track.id );
983
+ }
984
+
985
+ // If a track event by this id currently exists, modify it
986
+ if ( trackEvent ) {
987
+ isUpdate = true;
988
+ // Create a new object with the existing trackEvent
989
+ // Extend with new track properties
990
+ track = Popcorn.extend( {}, trackEvent, track );
991
+
992
+ // Remove the existing track from the instance
993
+ obj.removeTrackEvent( track.id );
994
+ }
850
995
 
851
996
  // Determine if this track has default options set for it
852
997
  // If so, apply them to the track object
@@ -858,7 +1003,7 @@
858
1003
 
859
1004
  if ( track._natives ) {
860
1005
  // Supports user defined track event id
861
- track._id = !track.id ? Popcorn.guid( track._natives.type ) : track.id;
1006
+ track._id = track.id || track._id || Popcorn.guid( track._natives.type );
862
1007
 
863
1008
  // Push track event ids into the history
864
1009
  obj.data.history.push( track._id );
@@ -870,8 +1015,7 @@
870
1015
  // Store this definition in an array sorted by times
871
1016
  var byStart = obj.data.trackEvents.byStart,
872
1017
  byEnd = obj.data.trackEvents.byEnd,
873
- startIndex, endIndex,
874
- currentTime;
1018
+ startIndex, endIndex;
875
1019
 
876
1020
  for ( startIndex = byStart.length - 1; startIndex >= 0; startIndex-- ) {
877
1021
 
@@ -890,23 +1034,15 @@
890
1034
  }
891
1035
 
892
1036
  // Display track event immediately if it's enabled and current
893
- if ( track._natives &&
894
- ( !!Popcorn.registryByName[ track._natives.type ] || !!obj[ track._natives.type ] ) ) {
895
-
896
- currentTime = obj.media.currentTime;
897
- if ( track.end > currentTime &&
898
- track.start <= currentTime &&
899
- obj.data.disabled.indexOf( track._natives.type ) === -1 ) {
1037
+ if ( track.end > obj.media.currentTime &&
1038
+ track.start <= obj.media.currentTime ) {
900
1039
 
901
- track._running = true;
902
- track._natives.start.call( obj, null, track );
1040
+ track._running = true;
1041
+ obj.data.running[ track._natives.type ].push( track );
903
1042
 
904
- if ( obj.options.frameAnimation &&
905
- track._natives.frame ) {
1043
+ if ( !obj.data.disabled[ track._natives.type ] ) {
906
1044
 
907
- obj.data.trackEvents.animating.push( track );
908
- track._natives.frame.call( obj, null, track, currentTime );
909
- }
1045
+ track._natives.start.call( obj, null, track );
910
1046
  }
911
1047
  }
912
1048
 
@@ -929,6 +1065,33 @@
929
1065
  if ( track._id ) {
930
1066
  Popcorn.addTrackEvent.ref( obj, track );
931
1067
  }
1068
+
1069
+ // If the call to addTrackEvent was an update/modify call, fire an event
1070
+ if ( isUpdate ) {
1071
+
1072
+ // Determine appropriate event type to trigger
1073
+ // they are identical in function, but the naming
1074
+ // adds some level of intuition for the end developer
1075
+ // to rely on
1076
+ if ( track._natives.type === "cue" ) {
1077
+ eventType = "cuechange";
1078
+ } else {
1079
+ eventType = "trackchange";
1080
+ }
1081
+
1082
+ // Fire an event with change information
1083
+ obj.emit( eventType, {
1084
+ id: track.id,
1085
+ previousValue: {
1086
+ time: trackEvent.start,
1087
+ fn: trackEvent._natives.start
1088
+ },
1089
+ currentValue: {
1090
+ time: track.start,
1091
+ fn: track._natives.start
1092
+ }
1093
+ });
1094
+ }
932
1095
  };
933
1096
 
934
1097
  // Internal Only - Adds track event references to the instance object's trackRefs hash table
@@ -1094,17 +1257,15 @@
1094
1257
  var currentTime = obj.media.currentTime,
1095
1258
  previousTime = obj.data.trackEvents.previousUpdateTime,
1096
1259
  tracks = obj.data.trackEvents,
1097
- animating = tracks.animating,
1098
1260
  end = tracks.endIndex,
1099
1261
  start = tracks.startIndex,
1100
- animIndex = 0,
1101
1262
  byStartLen = tracks.byStart.length,
1102
1263
  byEndLen = tracks.byEnd.length,
1103
1264
  registryByName = Popcorn.registryByName,
1104
1265
  trackstart = "trackstart",
1105
1266
  trackend = "trackend",
1106
1267
 
1107
- byEnd, byStart, byAnimate, natives, type;
1268
+ byEnd, byStart, byAnimate, natives, type, runningPlugins;
1108
1269
 
1109
1270
  // Playbar advancing
1110
1271
  if ( previousTime <= currentTime ) {
@@ -1121,15 +1282,22 @@
1121
1282
  !!obj[ type ] ) ) {
1122
1283
 
1123
1284
  if ( byEnd._running === true ) {
1285
+
1124
1286
  byEnd._running = false;
1125
- natives.end.call( obj, event, byEnd );
1126
-
1127
- obj.emit( trackend,
1128
- Popcorn.extend({}, byEnd, {
1129
- plugin: type,
1130
- type: trackend
1131
- })
1132
- );
1287
+ runningPlugins = obj.data.running[ type ];
1288
+ runningPlugins.splice( runningPlugins.indexOf( byEnd ), 1 );
1289
+
1290
+ if ( !obj.data.disabled[ type ] ) {
1291
+
1292
+ natives.end.call( obj, event, byEnd );
1293
+
1294
+ obj.emit( trackend,
1295
+ Popcorn.extend({}, byEnd, {
1296
+ plugin: type,
1297
+ type: trackend
1298
+ })
1299
+ );
1300
+ }
1133
1301
  }
1134
1302
 
1135
1303
  end++;
@@ -1152,25 +1320,21 @@
1152
1320
  !!obj[ type ] ) ) {
1153
1321
 
1154
1322
  if ( byStart.end > currentTime &&
1155
- byStart._running === false &&
1156
- obj.data.disabled.indexOf( type ) === -1 ) {
1323
+ byStart._running === false ) {
1157
1324
 
1158
1325
  byStart._running = true;
1159
- natives.start.call( obj, event, byStart );
1326
+ obj.data.running[ type ].push( byStart );
1160
1327
 
1161
- obj.emit( trackstart,
1162
- Popcorn.extend({}, byStart, {
1163
- plugin: type,
1164
- type: trackstart
1165
- })
1166
- );
1328
+ if ( !obj.data.disabled[ type ] ) {
1167
1329
 
1168
- // If the `frameAnimation` option is used,
1169
- // push the current byStart object into the `animating` cue
1170
- if ( obj.options.frameAnimation &&
1171
- ( byStart && byStart._running && byStart._natives.frame ) ) {
1330
+ natives.start.call( obj, event, byStart );
1172
1331
 
1173
- animating.push( byStart );
1332
+ obj.emit( trackstart,
1333
+ Popcorn.extend({}, byStart, {
1334
+ plugin: type,
1335
+ type: trackstart
1336
+ })
1337
+ );
1174
1338
  }
1175
1339
  }
1176
1340
  start++;
@@ -1181,22 +1345,6 @@
1181
1345
  }
1182
1346
  }
1183
1347
 
1184
- // If the `frameAnimation` option is used, iterate the animating track
1185
- // and execute the `frame` callback
1186
- if ( obj.options.frameAnimation ) {
1187
- while ( animIndex < animating.length ) {
1188
-
1189
- byAnimate = animating[ animIndex ];
1190
-
1191
- if ( !byAnimate._running ) {
1192
- animating.splice( animIndex, 1 );
1193
- } else {
1194
- byAnimate._natives.frame.call( obj, event, byAnimate, currentTime );
1195
- animIndex++;
1196
- }
1197
- }
1198
- }
1199
-
1200
1348
  // Playbar receding
1201
1349
  } else if ( previousTime > currentTime ) {
1202
1350
 
@@ -1212,15 +1360,22 @@
1212
1360
  !!obj[ type ] ) ) {
1213
1361
 
1214
1362
  if ( byStart._running === true ) {
1363
+
1215
1364
  byStart._running = false;
1216
- natives.end.call( obj, event, byStart );
1217
-
1218
- obj.emit( trackend,
1219
- Popcorn.extend({}, byEnd, {
1220
- plugin: type,
1221
- type: trackend
1222
- })
1223
- );
1365
+ runningPlugins = obj.data.running[ type ];
1366
+ runningPlugins.splice( runningPlugins.indexOf( byStart ), 1 );
1367
+
1368
+ if ( !obj.data.disabled[ type ] ) {
1369
+
1370
+ natives.end.call( obj, event, byStart );
1371
+
1372
+ obj.emit( trackend,
1373
+ Popcorn.extend({}, byStart, {
1374
+ plugin: type,
1375
+ type: trackend
1376
+ })
1377
+ );
1378
+ }
1224
1379
  }
1225
1380
  start--;
1226
1381
  } else {
@@ -1242,24 +1397,21 @@
1242
1397
  !!obj[ type ] ) ) {
1243
1398
 
1244
1399
  if ( byEnd.start <= currentTime &&
1245
- byEnd._running === false &&
1246
- obj.data.disabled.indexOf( type ) === -1 ) {
1400
+ byEnd._running === false ) {
1247
1401
 
1248
1402
  byEnd._running = true;
1249
- natives.start.call( obj, event, byEnd );
1250
-
1251
- obj.emit( trackstart,
1252
- Popcorn.extend({}, byStart, {
1253
- plugin: type,
1254
- type: trackstart
1255
- })
1256
- );
1257
- // If the `frameAnimation` option is used,
1258
- // push the current byEnd object into the `animating` cue
1259
- if ( obj.options.frameAnimation &&
1260
- ( byEnd && byEnd._running && byEnd._natives.frame ) ) {
1261
-
1262
- animating.push( byEnd );
1403
+ obj.data.running[ type ].push( byEnd );
1404
+
1405
+ if ( !obj.data.disabled[ type ] ) {
1406
+
1407
+ natives.start.call( obj, event, byEnd );
1408
+
1409
+ obj.emit( trackstart,
1410
+ Popcorn.extend({}, byEnd, {
1411
+ plugin: type,
1412
+ type: trackstart
1413
+ })
1414
+ );
1263
1415
  }
1264
1416
  }
1265
1417
  end--;
@@ -1269,23 +1421,6 @@
1269
1421
  return;
1270
1422
  }
1271
1423
  }
1272
-
1273
- // If the `frameAnimation` option is used, iterate the animating track
1274
- // and execute the `frame` callback
1275
- if ( obj.options.frameAnimation ) {
1276
- while ( animIndex < animating.length ) {
1277
-
1278
- byAnimate = animating[ animIndex ];
1279
-
1280
- if ( !byAnimate._running ) {
1281
- animating.splice( animIndex, 1 );
1282
- } else {
1283
- byAnimate._natives.frame.call( obj, event, byAnimate, currentTime );
1284
- animIndex++;
1285
- }
1286
- }
1287
- }
1288
- // time bar is not moving ( video is paused )
1289
1424
  }
1290
1425
 
1291
1426
  tracks.endIndex = end;
@@ -1384,6 +1519,28 @@
1384
1519
  return this;
1385
1520
  }
1386
1521
 
1522
+ // When the "ranges" property is set and its value is an array, short-circuit
1523
+ // the pluginFn definition to recall itself with an options object generated from
1524
+ // each range object in the ranges array. (eg. { start: 15, end: 16 } )
1525
+ if ( options.ranges && Popcorn.isArray(options.ranges) ) {
1526
+ Popcorn.forEach( options.ranges, function( range ) {
1527
+ // Create a fresh object, extend with current options
1528
+ // and start/end range object's properties
1529
+ // Works with in/out as well.
1530
+ var opts = Popcorn.extend( {}, options, range );
1531
+
1532
+ // Remove the ranges property to prevent infinitely
1533
+ // entering this condition
1534
+ delete opts.ranges;
1535
+
1536
+ // Call the plugin with the newly created opts object
1537
+ this[ name ]( opts );
1538
+ }, this);
1539
+
1540
+ // Return the Popcorn instance to avoid creating an empty track event
1541
+ return this;
1542
+ }
1543
+
1387
1544
  // Storing the plugin natives
1388
1545
  var natives = options._natives = {},
1389
1546
  compose = "",
@@ -1397,17 +1554,26 @@
1397
1554
  natives.start = natives.start || natives[ "in" ];
1398
1555
  natives.end = natives.end || natives[ "out" ];
1399
1556
 
1557
+ if ( options.once ) {
1558
+ natives.end = combineFn( natives.end, function() {
1559
+ this.removeTrackEvent( options._id );
1560
+ });
1561
+ }
1562
+
1400
1563
  // extend teardown to always call end if running
1401
1564
  natives._teardown = combineFn(function() {
1402
1565
 
1403
- var args = slice.call( arguments );
1566
+ var args = slice.call( arguments ),
1567
+ runningPlugins = this.data.running[ natives.type ];
1404
1568
 
1405
1569
  // end function signature is not the same as teardown,
1406
1570
  // put null on the front of arguments for the event parameter
1407
1571
  args.unshift( null );
1408
1572
 
1409
1573
  // only call end if event is running
1410
- args[ 1 ]._running && natives.end.apply( this, args );
1574
+ args[ 1 ]._running &&
1575
+ runningPlugins.splice( runningPlugins.indexOf( options ), 1 ) &&
1576
+ natives.end.apply( this, args );
1411
1577
  }, natives._teardown );
1412
1578
 
1413
1579
  // default to an empty string if no effect exists
@@ -1470,11 +1636,16 @@
1470
1636
  options.target = manifestOpts && "target" in manifestOpts && manifestOpts.target;
1471
1637
  }
1472
1638
 
1639
+ if ( options._natives ) {
1640
+ // ensure an initial id is there before setup is called
1641
+ options._id = Popcorn.guid( options._natives.type );
1642
+ }
1643
+
1473
1644
  // Trigger _setup method if exists
1474
1645
  options._natives._setup && options._natives._setup.call( this, options );
1475
1646
 
1476
1647
  // Create new track event for this instance
1477
- Popcorn.addTrackEvent( this, Popcorn.extend( options, options ) );
1648
+ Popcorn.addTrackEvent( this, options );
1478
1649
 
1479
1650
  // Future support for plugin event definitions
1480
1651
  // for all of the native events
@@ -1495,16 +1666,56 @@
1495
1666
 
1496
1667
  // Extend Popcorn.p with new named definition
1497
1668
  // Assign new named definition
1498
- Popcorn.p[ name ] = plugin[ name ] = function( options ) {
1669
+ Popcorn.p[ name ] = plugin[ name ] = function( id, options ) {
1670
+ var length = arguments.length,
1671
+ trackEvent, defaults, mergedSetupOpts;
1672
+
1673
+ // Shift arguments based on use case
1674
+ //
1675
+ // Back compat for:
1676
+ // p.plugin( options );
1677
+ if ( id && !options ) {
1678
+ options = id;
1679
+ id = null;
1680
+ } else {
1681
+
1682
+ // Get the trackEvent that matches the given id.
1683
+ trackEvent = this.getTrackEvent( id );
1684
+
1685
+ // If the track event does not exist, ensure that the options
1686
+ // object has a proper id
1687
+ if ( !trackEvent ) {
1688
+ options.id = id;
1689
+
1690
+ // If the track event does exist, merge the updated properties
1691
+ } else {
1692
+
1693
+ options = Popcorn.extend( {}, trackEvent, options );
1694
+
1695
+ Popcorn.addTrackEvent( this, options );
1696
+
1697
+ return this;
1698
+ }
1699
+ }
1700
+
1701
+ this.data.running[ name ] = this.data.running[ name ] || [];
1499
1702
 
1500
1703
  // Merge with defaults if they exist, make sure per call is prioritized
1501
- var defaults = ( this.options.defaults && this.options.defaults[ name ] ) || {},
1502
- mergedSetupOpts = Popcorn.extend( {}, defaults, options );
1704
+ defaults = ( this.options.defaults && this.options.defaults[ name ] ) || {};
1705
+ mergedSetupOpts = Popcorn.extend( {}, defaults, options );
1503
1706
 
1504
1707
  return pluginFn.call( this, isfn ? definition.call( this, mergedSetupOpts ) : definition,
1505
1708
  mergedSetupOpts );
1506
1709
  };
1507
1710
 
1711
+ // if the manifest parameter exists we should extend it onto the definition object
1712
+ // so that it shows up when calling Popcorn.registry and Popcorn.registryByName
1713
+ if ( manifest ) {
1714
+ Popcorn.extend( definition, {
1715
+ manifest: manifest
1716
+ });
1717
+ }
1718
+
1508
1719
  // Push into the registry
1509
1720
  var entry = {
1510
1721
  fn: plugin[ name ],
@@ -1548,13 +1759,14 @@
1548
1759
 
1549
1760
  // Trigger an error that the instance can listen for
1550
1761
  // and react to
1551
- this.emit( "error", Popcorn.plugin.errors );
1762
+ this.emit( "pluginerror", Popcorn.plugin.errors );
1552
1763
  }
1553
1764
  };
1554
1765
  }
1555
1766
 
1556
1767
  // Debug-mode flag for plugin development
1557
- Popcorn.plugin.debug = false;
1768
+ // True for Popcorn development versions, false for stable/tagged versions
1769
+ Popcorn.plugin.debug = ( Popcorn.version === "@" + "VERSION" );
1558
1770
 
1559
1771
  // removePlugin( type ) removes all tracks of that from all instances of popcorn
1560
1772
  // removePlugin( obj, type ) removes all tracks of type from obj, where obj is a single instance of popcorn
@@ -1651,6 +1863,65 @@
1651
1863
 
1652
1864
  Popcorn.plugin.effect = Popcorn.effect = Popcorn.compose;
1653
1865
 
1866
+ var rnaiveExpr = /^(?:\.|#|\[)/;
1867
+
1868
+ // Basic DOM utilities and helpers API. See #1037
1869
+ Popcorn.dom = {
1870
+ debug: false,
1871
+ // Popcorn.dom.find( selector, context )
1872
+ //
1873
+ // Returns the first element that matches the specified selector
1874
+ // Optionally provide a context element, defaults to `document`
1875
+ //
1876
+ // eg.
1877
+ // Popcorn.dom.find("video") returns the first video element
1878
+ // Popcorn.dom.find("#foo") returns the first element with `id="foo"`
1879
+ // Popcorn.dom.find("foo") returns the first element with `id="foo"`
1880
+ // Note: Popcorn.dom.find("foo") is the only allowed deviation
1881
+ // from valid querySelector selector syntax
1882
+ //
1883
+ // Popcorn.dom.find(".baz") returns the first element with `class="baz"`
1884
+ // Popcorn.dom.find("[preload]") returns the first element with `preload="..."`
1885
+ // ...
1886
+ // See https://developer.mozilla.org/En/DOM/Document.querySelector
1887
+ //
1888
+ //
1889
+ find: function( selector, context ) {
1890
+ var node = null;
1891
+
1892
+ // Trim leading/trailing whitespace to avoid false negatives
1893
+ selector = selector.trim();
1894
+
1895
+ // Default context is the `document`
1896
+ context = context || document;
1897
+
1898
+ if ( selector ) {
1899
+ // If the selector does not begin with "#", "." or "[",
1900
+ // it could be either a nodeName or ID w/o "#"
1901
+ if ( !rnaiveExpr.test( selector ) ) {
1902
+
1903
+ // Try finding an element that matches by ID first
1904
+ node = document.getElementById( selector );
1905
+
1906
+ // If a match was found by ID, return the element
1907
+ if ( node !== null ) {
1908
+ return node;
1909
+ }
1910
+ }
1911
+ // Assume no elements have been found yet
1912
+ // Catch any invalid selector syntax errors and bury them.
1913
+ try {
1914
+ node = context.querySelector( selector );
1915
+ } catch ( e ) {
1916
+ if ( Popcorn.dom.debug ) {
1917
+ throw new Error(e);
1918
+ }
1919
+ }
1920
+ }
1921
+ return node;
1922
+ }
1923
+ };
1924
+
1654
1925
  // Cache references to reused RegExps
1655
1926
  var rparams = /\?/,
1656
1927
  // XHR Setup object
@@ -1760,31 +2031,53 @@
1760
2031
 
1761
2032
  var head = document.head || document.getElementsByTagName( "head" )[ 0 ] || document.documentElement,
1762
2033
  script = document.createElement( "script" ),
1763
- paramStr = url.split( "?" )[ 1 ],
1764
2034
  isFired = false,
1765
2035
  params = [],
1766
- callback, parts, callparam;
2036
+ rjsonp = /(=)\?(?=&|$)|\?\?/,
2037
+ replaceInUrl, prefix, paramStr, callback, callparam;
1767
2038
 
1768
- if ( paramStr && !isScript ) {
1769
- params = paramStr.split( "&" );
1770
- }
2039
+ if ( !isScript ) {
1771
2040
 
1772
- if ( params.length ) {
1773
- parts = params[ params.length - 1 ].split( "=" );
1774
- }
2041
+ // is there a calback already in the url
2042
+ callparam = url.match( /(callback=[^&]*)/ );
1775
2043
 
1776
- callback = params.length ? ( parts[ 1 ] ? parts[ 1 ] : parts[ 0 ] ) : "jsonp";
2044
+ if ( callparam !== null && callparam.length ) {
1777
2045
 
1778
- if ( !paramStr && !isScript ) {
1779
- url += "?callback=" + callback;
1780
- }
2046
+ prefix = callparam[ 1 ].split( "=" )[ 1 ];
2047
+
2048
+ // Since we need to support developer specified callbacks
2049
+ // and placeholders in harmony, make sure matches to "callback="
2050
+ // aren't just placeholders.
2051
+ // We coded ourselves into a corner here.
2052
+ // JSONP callbacks should never have been
2053
+ // allowed to have developer specified callbacks
2054
+ if ( prefix === "?" ) {
2055
+ prefix = "jsonp";
2056
+ }
2057
+
2058
+ // get the callback name
2059
+ callback = Popcorn.guid( prefix );
2060
+
2061
+ // replace existing callback name with unique callback name
2062
+ url = url.replace( /(callback=[^&]*)/, "callback=" + callback );
2063
+ } else {
2064
+
2065
+ callback = Popcorn.guid( "jsonp" );
2066
+
2067
+ if ( rjsonp.test( url ) ) {
2068
+ url = url.replace( rjsonp, "$1" + callback );
2069
+ }
1781
2070
 
1782
- if ( callback && !isScript ) {
2071
+ // split on first question mark,
2072
+ // this is to capture the query string
2073
+ params = url.split( /\?(.+)?/ );
1783
2074
 
1784
- // If a callback name already exists
1785
- if ( !!window[ callback ] ) {
1786
- // Create a new unique callback name
1787
- callback = Popcorn.guid( callback );
2075
+ // rebuild url with callback
2076
+ url = params[ 0 ] + "?";
2077
+ if ( params[ 1 ] ) {
2078
+ url += params[ 1 ] + "&";
2079
+ }
2080
+ url += "callback=" + callback;
1788
2081
  }
1789
2082
 
1790
2083
  // Define the JSONP success callback globally
@@ -1793,9 +2086,6 @@
1793
2086
  success && success( data );
1794
2087
  isFired = true;
1795
2088
  };
1796
-
1797
- // Replace callback param and callback name
1798
- url = url.replace( parts.join( "=" ), parts[ 0 ] + "=" + callback );
1799
2089
  }
1800
2090
 
1801
2091
  script.addEventListener( "load", function() {