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
@@ -206,8 +206,7 @@
206
206
  isSupported: false
207
207
  };
208
208
 
209
- var methods = ( "removeInstance addInstance getInstanceById removeInstanceById " +
210
- "forEach extend effects error guid sizeOf isArray nop position disable enable destroy" +
209
+ var methods = ( "byId forEach extend effects error guid sizeOf isArray nop position disable enable destroy" +
211
210
  "addTrackEvent removeTrackEvent getTrackEvents getTrackEvent getLastTrackEventId " +
212
211
  "timeUpdate plugin removePlugin compose effect xhr getJSONP getScript" ).split(/\s+/);
213
212
 
@@ -230,9 +229,6 @@
230
229
  // Copy global Popcorn (may not exist)
231
230
  _Popcorn = global.Popcorn,
232
231
 
233
- // ID string matching
234
- rIdExp = /^(#([\w\-\_\.]+))$/,
235
-
236
232
  // Ready fn cache
237
233
  readyStack = [],
238
234
  readyBound = false,
@@ -274,61 +270,6 @@
274
270
  })( obj );
275
271
  },
276
272
 
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
273
  // Declare constructor
333
274
  // Returns an instance object.
334
275
  Popcorn = function( entity, options ) {
@@ -337,7 +278,7 @@
337
278
  };
338
279
 
339
280
  // Popcorn API version, automatically inserted via build system.
340
- Popcorn.version = "1.2";
281
+ Popcorn.version = "1.3";
341
282
 
342
283
  // Boolean flag allowing a client to determine if Popcorn can be supported
343
284
  Popcorn.isSupported = true;
@@ -351,7 +292,7 @@
351
292
 
352
293
  init: function( entity, options ) {
353
294
 
354
- var matches,
295
+ var matches, nodeName,
355
296
  self = this;
356
297
 
357
298
  // Supports Popcorn(function () { /../ })
@@ -399,31 +340,47 @@
399
340
  return;
400
341
  }
401
342
 
402
- // Check if entity is a valid string id
403
- matches = rIdExp.exec( entity );
343
+ if ( typeof entity === "string" ) {
344
+ try {
345
+ matches = document.querySelector( entity );
346
+ } catch( e ) {
347
+ throw new Error( "Popcorn.js Error: Invalid media element selector: " + entity );
348
+ }
349
+ }
404
350
 
405
351
  // Get media element by id or object reference
406
- this.media = matches && matches.length && matches[ 2 ] ?
407
- document.getElementById( matches[ 2 ] ) :
408
- entity;
352
+ this.media = matches || entity;
409
353
 
410
- // Create an audio or video element property reference
411
- this[ ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video" ] = this.media;
354
+ // inner reference to this media element's nodeName string value
355
+ nodeName = ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video";
412
356
 
413
- // Register new instance
414
- Popcorn.instances.push( this );
357
+ // Create an audio or video element property reference
358
+ this[ nodeName ] = this.media;
415
359
 
416
360
  this.options = options || {};
417
361
 
362
+ // Resolve custom ID or default prefixed ID
363
+ this.id = this.options.id || Popcorn.guid( nodeName );
364
+
365
+ // Throw if an attempt is made to use an ID that already exists
366
+ if ( Popcorn.byId( this.id ) ) {
367
+ throw new Error( "Popcorn.js Error: Cannot use duplicate ID (" + this.id + ")" );
368
+ }
369
+
418
370
  this.isDestroyed = false;
419
371
 
420
372
  this.data = {
421
373
 
374
+ // data structure of all
375
+ running: {
376
+ cue: []
377
+ },
378
+
422
379
  // Executed by either timeupdate event or in rAF loop
423
380
  timeUpdate: Popcorn.nop,
424
381
 
425
382
  // Allows disabling a plugin per instance
426
- disabled: [],
383
+ disabled: {},
427
384
 
428
385
  // Stores DOM event queues by type
429
386
  events: {},
@@ -460,12 +417,26 @@
460
417
  }
461
418
  };
462
419
 
420
+ // Register new instance
421
+ Popcorn.instances.push( this );
422
+
463
423
  // function to fire when video is ready
464
424
  var isReady = function() {
465
425
 
426
+ // chrome bug: http://code.google.com/p/chromium/issues/detail?id=119598
427
+ // it is possible the video's time is less than 0
428
+ // this has the potential to call track events more than once, when they should not
429
+ // start: 0, end: 1 will start, end, start again, when it should just start
430
+ // just setting it to 0 if it is below 0 fixes this issue
431
+ if ( self.media.currentTime < 0 ) {
432
+
433
+ self.media.currentTime = 0;
434
+ }
435
+
466
436
  self.media.removeEventListener( "loadeddata", isReady, false );
467
437
 
468
- var duration, videoDurationPlus;
438
+ var duration, videoDurationPlus,
439
+ runningPlugins, runningPlugin, rpLength, rpNatives;
469
440
 
470
441
  // Adding padding to the front and end of the arrays
471
442
  // this is so we do not fall off either end
@@ -480,6 +451,7 @@
480
451
  });
481
452
 
482
453
  if ( self.options.frameAnimation ) {
454
+
483
455
  // if Popcorn is created with frameAnimation option set to true,
484
456
  // requestAnimFrame is used instead of "timeupdate" media event.
485
457
  // This is for greater frame time accuracy, theoretically up to
@@ -488,6 +460,25 @@
488
460
 
489
461
  Popcorn.timeUpdate( self, {} );
490
462
 
463
+ // fire frame for each enabled active plugin of every type
464
+ Popcorn.forEach( Popcorn.manifest, function( key, val ) {
465
+
466
+ runningPlugins = self.data.running[ val ];
467
+
468
+ // ensure there are running plugins on this type on this instance
469
+ if ( runningPlugins ) {
470
+
471
+ rpLength = runningPlugins.length;
472
+ for ( var i = 0; i < rpLength; i++ ) {
473
+
474
+ runningPlugin = runningPlugins[ i ];
475
+ rpNatives = runningPlugin._natives;
476
+ rpNatives && rpNatives.frame &&
477
+ rpNatives.frame.call( self, {}, runningPlugin, self.currentTime() );
478
+ }
479
+ }
480
+ });
481
+
491
482
  self.emit( "timeupdate" );
492
483
 
493
484
  !self.isDestroyed && requestAnimFrame( self.data.timeUpdate );
@@ -507,6 +498,13 @@
507
498
  }
508
499
  };
509
500
 
501
+ Object.defineProperty( this, "error", {
502
+ get: function() {
503
+
504
+ return self.media.error;
505
+ }
506
+ });
507
+
510
508
  if ( self.media.readyState >= 2 ) {
511
509
 
512
510
  isReady();
@@ -523,6 +521,20 @@
523
521
  // Allows chaining methods to instances
524
522
  Popcorn.p.init.prototype = Popcorn.p;
525
523
 
524
+ Popcorn.byId = function( str ) {
525
+ var instances = Popcorn.instances,
526
+ length = instances.length,
527
+ i = 0;
528
+
529
+ for ( ; i < length; i++ ) {
530
+ if ( instances[ i ].id === str ) {
531
+ return instances[ i ];
532
+ }
533
+ }
534
+
535
+ return null;
536
+ };
537
+
526
538
  Popcorn.forEach = function( obj, fn, context ) {
527
539
 
528
540
  if ( !obj || !fn ) {
@@ -628,32 +640,38 @@
628
640
 
629
641
  disable: function( instance, plugin ) {
630
642
 
631
- var disabled = instance.data.disabled;
643
+ if ( !instance.data.disabled[ plugin ] ) {
632
644
 
633
- if ( disabled.indexOf( plugin ) === -1 ) {
634
- disabled.push( plugin );
635
- }
645
+ instance.data.disabled[ plugin ] = true;
636
646
 
637
- refresh( instance );
647
+ for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) {
648
+
649
+ event = instance.data.running[ plugin ][ i ];
650
+ event._natives.end.call( instance, null, event );
651
+ }
652
+ }
638
653
 
639
654
  return instance;
640
655
  },
641
656
  enable: function( instance, plugin ) {
642
657
 
643
- var disabled = instance.data.disabled,
644
- index = disabled.indexOf( plugin );
658
+ if ( instance.data.disabled[ plugin ] ) {
645
659
 
646
- if ( index > -1 ) {
647
- disabled.splice( index, 1 );
648
- }
660
+ instance.data.disabled[ plugin ] = false;
649
661
 
650
- refresh( instance );
662
+ for ( var i = instance.data.running[ plugin ].length - 1, event; i >= 0; i-- ) {
663
+
664
+ event = instance.data.running[ plugin ][ i ];
665
+ event._natives.start.call( instance, null, event );
666
+ }
667
+ }
651
668
 
652
669
  return instance;
653
670
  },
654
671
  destroy: function( instance ) {
655
672
  var events = instance.data.events,
656
- singleEvent, item, fn;
673
+ trackEvents = instance.data.trackEvents,
674
+ singleEvent, item, fn, plugin;
657
675
 
658
676
  // Iterate through all events and remove them
659
677
  for ( item in events ) {
@@ -664,6 +682,15 @@
664
682
  events[ item ] = null;
665
683
  }
666
684
 
685
+ // remove all plugins off the given instance
686
+ for ( plugin in Popcorn.registryByName ) {
687
+ Popcorn.removePlugin( instance, plugin );
688
+ }
689
+
690
+ // Remove all data.trackEvents #1178
691
+ trackEvents.byStart.length = 0;
692
+ trackEvents.byEnd.length = 0;
693
+
667
694
  if ( !instance.isDestroyed ) {
668
695
  instance.data.timeUpdate && instance.media.removeEventListener( "timeupdate", instance.data.timeUpdate, false );
669
696
  instance.isDestroyed = true;
@@ -688,6 +715,7 @@
688
715
  Popcorn.forEach( methods.split( /\s+/g ), function( name ) {
689
716
 
690
717
  ret[ name ] = function( arg ) {
718
+ var previous;
691
719
 
692
720
  if ( typeof this.media[ name ] === "function" ) {
693
721
 
@@ -704,11 +732,22 @@
704
732
  return this;
705
733
  }
706
734
 
707
-
708
735
  if ( arg != null ) {
736
+ // Capture the current value of the attribute property
737
+ previous = this.media[ name ];
709
738
 
739
+ // Set the attribute property with the new value
710
740
  this.media[ name ] = arg;
711
741
 
742
+ // If the new value is not the same as the old value
743
+ // emit an "attrchanged event"
744
+ if ( previous !== arg ) {
745
+ this.emit( "attrchange", {
746
+ attribute: name,
747
+ previousValue: previous,
748
+ currentValue: arg
749
+ });
750
+ }
712
751
  return this;
713
752
  }
714
753
 
@@ -731,14 +770,103 @@
731
770
 
732
771
  // Rounded currentTime
733
772
  roundTime: function() {
734
- return -~this.media.currentTime;
773
+ return Math.round( this.media.currentTime );
735
774
  },
736
775
 
737
776
  // Attach an event to a single point in time
738
- exec: function( time, fn ) {
777
+ exec: function( id, time, fn ) {
778
+ var length = arguments.length,
779
+ trackEvent, sec;
780
+
781
+ // Check if first could possibly be a SMPTE string
782
+ // p.cue( "smpte string", fn );
783
+ // try/catch avoid awful throw in Popcorn.util.toSeconds
784
+ // TODO: Get rid of that, replace with NaN return?
785
+ try {
786
+ sec = Popcorn.util.toSeconds( id );
787
+ } catch ( e ) {}
788
+
789
+ // If it can be converted into a number then
790
+ // it's safe to assume that the string was SMPTE
791
+ if ( typeof sec === "number" ) {
792
+ id = sec;
793
+ }
794
+
795
+ // Shift arguments based on use case
796
+ //
797
+ // Back compat for:
798
+ // p.cue( time, fn );
799
+ if ( typeof id === "number" && length === 2 ) {
800
+ fn = time;
801
+ time = id;
802
+ id = Popcorn.guid( "cue" );
803
+ } else {
804
+ // Support for new forms
805
+
806
+ // p.cue( "empty-cue" );
807
+ if ( length === 1 ) {
808
+ // Set a time for an empty cue. It's not important what
809
+ // the time actually is, because the cue is a no-op
810
+ time = -1;
811
+
812
+ } else {
813
+
814
+ // Get the trackEvent that matches the given id.
815
+ trackEvent = this.getTrackEvent( id );
816
+
817
+ if ( trackEvent ) {
818
+
819
+ // p.cue( "my-id", 12 );
820
+ // p.cue( "my-id", function() { ... });
821
+ if ( typeof id === "string" && length === 2 ) {
822
+
823
+ // p.cue( "my-id", 12 );
824
+ // The path will update the cue time.
825
+ if ( typeof time === "number" ) {
826
+ // Re-use existing trackEvent start callback
827
+ fn = trackEvent._natives.start;
828
+ }
829
+
830
+ // p.cue( "my-id", function() { ... });
831
+ // The path will update the cue function
832
+ if ( typeof time === "function" ) {
833
+ fn = time;
834
+ // Re-use existing trackEvent start time
835
+ time = trackEvent.start;
836
+ }
837
+ }
838
+ } else {
839
+
840
+ if ( length >= 2 ) {
841
+
842
+ // p.cue( "a", "00:00:00");
843
+ if ( typeof time === "string" ) {
844
+ try {
845
+ sec = Popcorn.util.toSeconds( time );
846
+ } catch ( e ) {}
847
+
848
+ time = sec;
849
+ }
850
+
851
+ // p.cue( "b", 11 );
852
+ if ( typeof time === "number" ) {
853
+ fn = Popcorn.nop();
854
+ }
855
+
856
+ // p.cue( "c", function() {});
857
+ if ( typeof time === "function" ) {
858
+ fn = time;
859
+ time = -1;
860
+ }
861
+ }
862
+ }
863
+ }
864
+ }
739
865
 
740
866
  // Creating a one second track event with an empty end
867
+ // Or update an existing track event with new values
741
868
  Popcorn.addTrackEvent( this, {
869
+ id: id,
742
870
  start: time,
743
871
  end: time + 1,
744
872
  _running: false,
@@ -790,7 +918,7 @@
790
918
 
791
919
  // Toggle a plugin's playback behaviour (on or off) per instance
792
920
  toggle: function( plugin ) {
793
- return Popcorn[ this.data.disabled.indexOf( plugin ) > -1 ? "enable" : "disable" ]( this, plugin );
921
+ return Popcorn[ this.data.disabled[ plugin ] ? "enable" : "disable" ]( this, plugin );
794
922
  },
795
923
 
796
924
  // Set default values for plugin options objects per instance
@@ -826,7 +954,7 @@
826
954
  Popcorn.Events = {
827
955
  UIEvents: "blur focus focusin focusout load resize scroll unload",
828
956
  MouseEvents: "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave click dblclick",
829
- Events: "loadstart progress suspend emptied stalled play pause " +
957
+ Events: "loadstart progress suspend emptied stalled play pause error " +
830
958
  "loadedmetadata loadeddata waiting playing canplay canplaythrough " +
831
959
  "seeking seeked timeupdate ended ratechange durationchange volumechange"
832
960
  };
@@ -1039,6 +1167,23 @@
1039
1167
 
1040
1168
  // Internal Only - Adds track events to the instance object
1041
1169
  Popcorn.addTrackEvent = function( obj, track ) {
1170
+ var trackEvent, isUpdate, eventType;
1171
+
1172
+ // Do a lookup for existing trackevents with this id
1173
+ if ( track.id ) {
1174
+ trackEvent = obj.getTrackEvent( track.id );
1175
+ }
1176
+
1177
+ // If a track event by this id currently exists, modify it
1178
+ if ( trackEvent ) {
1179
+ isUpdate = true;
1180
+ // Create a new object with the existing trackEvent
1181
+ // Extend with new track properties
1182
+ track = Popcorn.extend( {}, trackEvent, track );
1183
+
1184
+ // Remove the existing track from the instance
1185
+ obj.removeTrackEvent( track.id );
1186
+ }
1042
1187
 
1043
1188
  // Determine if this track has default options set for it
1044
1189
  // If so, apply them to the track object
@@ -1050,7 +1195,7 @@
1050
1195
 
1051
1196
  if ( track._natives ) {
1052
1197
  // Supports user defined track event id
1053
- track._id = !track.id ? Popcorn.guid( track._natives.type ) : track.id;
1198
+ track._id = track.id || track._id || Popcorn.guid( track._natives.type );
1054
1199
 
1055
1200
  // Push track event ids into the history
1056
1201
  obj.data.history.push( track._id );
@@ -1062,8 +1207,7 @@
1062
1207
  // Store this definition in an array sorted by times
1063
1208
  var byStart = obj.data.trackEvents.byStart,
1064
1209
  byEnd = obj.data.trackEvents.byEnd,
1065
- startIndex, endIndex,
1066
- currentTime;
1210
+ startIndex, endIndex;
1067
1211
 
1068
1212
  for ( startIndex = byStart.length - 1; startIndex >= 0; startIndex-- ) {
1069
1213
 
@@ -1082,23 +1226,15 @@
1082
1226
  }
1083
1227
 
1084
1228
  // Display track event immediately if it's enabled and current
1085
- if ( track._natives &&
1086
- ( !!Popcorn.registryByName[ track._natives.type ] || !!obj[ track._natives.type ] ) ) {
1229
+ if ( track.end > obj.media.currentTime &&
1230
+ track.start <= obj.media.currentTime ) {
1087
1231
 
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 );
1232
+ track._running = true;
1233
+ obj.data.running[ track._natives.type ].push( track );
1095
1234
 
1096
- if ( obj.options.frameAnimation &&
1097
- track._natives.frame ) {
1235
+ if ( !obj.data.disabled[ track._natives.type ] ) {
1098
1236
 
1099
- obj.data.trackEvents.animating.push( track );
1100
- track._natives.frame.call( obj, null, track, currentTime );
1101
- }
1237
+ track._natives.start.call( obj, null, track );
1102
1238
  }
1103
1239
  }
1104
1240
 
@@ -1121,6 +1257,33 @@
1121
1257
  if ( track._id ) {
1122
1258
  Popcorn.addTrackEvent.ref( obj, track );
1123
1259
  }
1260
+
1261
+ // If the call to addTrackEvent was an update/modify call, fire an event
1262
+ if ( isUpdate ) {
1263
+
1264
+ // Determine appropriate event type to trigger
1265
+ // they are identical in function, but the naming
1266
+ // adds some level of intuition for the end developer
1267
+ // to rely on
1268
+ if ( track._natives.type === "cue" ) {
1269
+ eventType = "cuechange";
1270
+ } else {
1271
+ eventType = "trackchange";
1272
+ }
1273
+
1274
+ // Fire an event with change information
1275
+ obj.emit( eventType, {
1276
+ id: track.id,
1277
+ previousValue: {
1278
+ time: trackEvent.start,
1279
+ fn: trackEvent._natives.start
1280
+ },
1281
+ currentValue: {
1282
+ time: track.start,
1283
+ fn: track._natives.start
1284
+ }
1285
+ });
1286
+ }
1124
1287
  };
1125
1288
 
1126
1289
  // Internal Only - Adds track event references to the instance object's trackRefs hash table
@@ -1286,17 +1449,15 @@
1286
1449
  var currentTime = obj.media.currentTime,
1287
1450
  previousTime = obj.data.trackEvents.previousUpdateTime,
1288
1451
  tracks = obj.data.trackEvents,
1289
- animating = tracks.animating,
1290
1452
  end = tracks.endIndex,
1291
1453
  start = tracks.startIndex,
1292
- animIndex = 0,
1293
1454
  byStartLen = tracks.byStart.length,
1294
1455
  byEndLen = tracks.byEnd.length,
1295
1456
  registryByName = Popcorn.registryByName,
1296
1457
  trackstart = "trackstart",
1297
1458
  trackend = "trackend",
1298
1459
 
1299
- byEnd, byStart, byAnimate, natives, type;
1460
+ byEnd, byStart, byAnimate, natives, type, runningPlugins;
1300
1461
 
1301
1462
  // Playbar advancing
1302
1463
  if ( previousTime <= currentTime ) {
@@ -1313,15 +1474,22 @@
1313
1474
  !!obj[ type ] ) ) {
1314
1475
 
1315
1476
  if ( byEnd._running === true ) {
1477
+
1316
1478
  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
- );
1479
+ runningPlugins = obj.data.running[ type ];
1480
+ runningPlugins.splice( runningPlugins.indexOf( byEnd ), 1 );
1481
+
1482
+ if ( !obj.data.disabled[ type ] ) {
1483
+
1484
+ natives.end.call( obj, event, byEnd );
1485
+
1486
+ obj.emit( trackend,
1487
+ Popcorn.extend({}, byEnd, {
1488
+ plugin: type,
1489
+ type: trackend
1490
+ })
1491
+ );
1492
+ }
1325
1493
  }
1326
1494
 
1327
1495
  end++;
@@ -1344,25 +1512,21 @@
1344
1512
  !!obj[ type ] ) ) {
1345
1513
 
1346
1514
  if ( byStart.end > currentTime &&
1347
- byStart._running === false &&
1348
- obj.data.disabled.indexOf( type ) === -1 ) {
1515
+ byStart._running === false ) {
1349
1516
 
1350
1517
  byStart._running = true;
1351
- natives.start.call( obj, event, byStart );
1518
+ obj.data.running[ type ].push( byStart );
1352
1519
 
1353
- obj.emit( trackstart,
1354
- Popcorn.extend({}, byStart, {
1355
- plugin: type,
1356
- type: trackstart
1357
- })
1358
- );
1520
+ if ( !obj.data.disabled[ type ] ) {
1359
1521
 
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 ) ) {
1522
+ natives.start.call( obj, event, byStart );
1364
1523
 
1365
- animating.push( byStart );
1524
+ obj.emit( trackstart,
1525
+ Popcorn.extend({}, byStart, {
1526
+ plugin: type,
1527
+ type: trackstart
1528
+ })
1529
+ );
1366
1530
  }
1367
1531
  }
1368
1532
  start++;
@@ -1373,22 +1537,6 @@
1373
1537
  }
1374
1538
  }
1375
1539
 
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
1540
  // Playbar receding
1393
1541
  } else if ( previousTime > currentTime ) {
1394
1542
 
@@ -1404,15 +1552,22 @@
1404
1552
  !!obj[ type ] ) ) {
1405
1553
 
1406
1554
  if ( byStart._running === true ) {
1555
+
1407
1556
  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
- );
1557
+ runningPlugins = obj.data.running[ type ];
1558
+ runningPlugins.splice( runningPlugins.indexOf( byStart ), 1 );
1559
+
1560
+ if ( !obj.data.disabled[ type ] ) {
1561
+
1562
+ natives.end.call( obj, event, byStart );
1563
+
1564
+ obj.emit( trackend,
1565
+ Popcorn.extend({}, byStart, {
1566
+ plugin: type,
1567
+ type: trackend
1568
+ })
1569
+ );
1570
+ }
1416
1571
  }
1417
1572
  start--;
1418
1573
  } else {
@@ -1434,24 +1589,21 @@
1434
1589
  !!obj[ type ] ) ) {
1435
1590
 
1436
1591
  if ( byEnd.start <= currentTime &&
1437
- byEnd._running === false &&
1438
- obj.data.disabled.indexOf( type ) === -1 ) {
1592
+ byEnd._running === false ) {
1439
1593
 
1440
1594
  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 );
1595
+ obj.data.running[ type ].push( byEnd );
1596
+
1597
+ if ( !obj.data.disabled[ type ] ) {
1598
+
1599
+ natives.start.call( obj, event, byEnd );
1600
+
1601
+ obj.emit( trackstart,
1602
+ Popcorn.extend({}, byEnd, {
1603
+ plugin: type,
1604
+ type: trackstart
1605
+ })
1606
+ );
1455
1607
  }
1456
1608
  }
1457
1609
  end--;
@@ -1461,23 +1613,6 @@
1461
1613
  return;
1462
1614
  }
1463
1615
  }
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
1616
  }
1482
1617
 
1483
1618
  tracks.endIndex = end;
@@ -1576,6 +1711,28 @@
1576
1711
  return this;
1577
1712
  }
1578
1713
 
1714
+ // When the "ranges" property is set and its value is an array, short-circuit
1715
+ // the pluginFn definition to recall itself with an options object generated from
1716
+ // each range object in the ranges array. (eg. { start: 15, end: 16 } )
1717
+ if ( options.ranges && Popcorn.isArray(options.ranges) ) {
1718
+ Popcorn.forEach( options.ranges, function( range ) {
1719
+ // Create a fresh object, extend with current options
1720
+ // and start/end range object's properties
1721
+ // Works with in/out as well.
1722
+ var opts = Popcorn.extend( {}, options, range );
1723
+
1724
+ // Remove the ranges property to prevent infinitely
1725
+ // entering this condition
1726
+ delete opts.ranges;
1727
+
1728
+ // Call the plugin with the newly created opts object
1729
+ this[ name ]( opts );
1730
+ }, this);
1731
+
1732
+ // Return the Popcorn instance to avoid creating an empty track event
1733
+ return this;
1734
+ }
1735
+
1579
1736
  // Storing the plugin natives
1580
1737
  var natives = options._natives = {},
1581
1738
  compose = "",
@@ -1589,17 +1746,26 @@
1589
1746
  natives.start = natives.start || natives[ "in" ];
1590
1747
  natives.end = natives.end || natives[ "out" ];
1591
1748
 
1749
+ if ( options.once ) {
1750
+ natives.end = combineFn( natives.end, function() {
1751
+ this.removeTrackEvent( options._id );
1752
+ });
1753
+ }
1754
+
1592
1755
  // extend teardown to always call end if running
1593
1756
  natives._teardown = combineFn(function() {
1594
1757
 
1595
- var args = slice.call( arguments );
1758
+ var args = slice.call( arguments ),
1759
+ runningPlugins = this.data.running[ natives.type ];
1596
1760
 
1597
1761
  // end function signature is not the same as teardown,
1598
1762
  // put null on the front of arguments for the event parameter
1599
1763
  args.unshift( null );
1600
1764
 
1601
1765
  // only call end if event is running
1602
- args[ 1 ]._running && natives.end.apply( this, args );
1766
+ args[ 1 ]._running &&
1767
+ runningPlugins.splice( runningPlugins.indexOf( options ), 1 ) &&
1768
+ natives.end.apply( this, args );
1603
1769
  }, natives._teardown );
1604
1770
 
1605
1771
  // default to an empty string if no effect exists
@@ -1662,11 +1828,16 @@
1662
1828
  options.target = manifestOpts && "target" in manifestOpts && manifestOpts.target;
1663
1829
  }
1664
1830
 
1831
+ if ( options._natives ) {
1832
+ // ensure an initial id is there before setup is called
1833
+ options._id = Popcorn.guid( options._natives.type );
1834
+ }
1835
+
1665
1836
  // Trigger _setup method if exists
1666
1837
  options._natives._setup && options._natives._setup.call( this, options );
1667
1838
 
1668
1839
  // Create new track event for this instance
1669
- Popcorn.addTrackEvent( this, Popcorn.extend( options, options ) );
1840
+ Popcorn.addTrackEvent( this, options );
1670
1841
 
1671
1842
  // Future support for plugin event definitions
1672
1843
  // for all of the native events
@@ -1687,16 +1858,56 @@
1687
1858
 
1688
1859
  // Extend Popcorn.p with new named definition
1689
1860
  // Assign new named definition
1690
- Popcorn.p[ name ] = plugin[ name ] = function( options ) {
1861
+ Popcorn.p[ name ] = plugin[ name ] = function( id, options ) {
1862
+ var length = arguments.length,
1863
+ trackEvent, defaults, mergedSetupOpts;
1864
+
1865
+ // Shift arguments based on use case
1866
+ //
1867
+ // Back compat for:
1868
+ // p.plugin( options );
1869
+ if ( id && !options ) {
1870
+ options = id;
1871
+ id = null;
1872
+ } else {
1873
+
1874
+ // Get the trackEvent that matches the given id.
1875
+ trackEvent = this.getTrackEvent( id );
1876
+
1877
+ // If the track event does not exist, ensure that the options
1878
+ // object has a proper id
1879
+ if ( !trackEvent ) {
1880
+ options.id = id;
1881
+
1882
+ // If the track event does exist, merge the updated properties
1883
+ } else {
1884
+
1885
+ options = Popcorn.extend( {}, trackEvent, options );
1886
+
1887
+ Popcorn.addTrackEvent( this, options );
1888
+
1889
+ return this;
1890
+ }
1891
+ }
1892
+
1893
+ this.data.running[ name ] = this.data.running[ name ] || [];
1691
1894
 
1692
1895
  // 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 );
1896
+ defaults = ( this.options.defaults && this.options.defaults[ name ] ) || {};
1897
+ mergedSetupOpts = Popcorn.extend( {}, defaults, options );
1695
1898
 
1696
1899
  return pluginFn.call( this, isfn ? definition.call( this, mergedSetupOpts ) : definition,
1697
1900
  mergedSetupOpts );
1698
1901
  };
1699
1902
 
1903
+ // if the manifest parameter exists we should extend it onto the definition object
1904
+ // so that it shows up when calling Popcorn.registry and Popcorn.registryByName
1905
+ if ( manifest ) {
1906
+ Popcorn.extend( definition, {
1907
+ manifest: manifest
1908
+ });
1909
+ }
1910
+
1700
1911
  // Push into the registry
1701
1912
  var entry = {
1702
1913
  fn: plugin[ name ],
@@ -1740,13 +1951,14 @@
1740
1951
 
1741
1952
  // Trigger an error that the instance can listen for
1742
1953
  // and react to
1743
- this.emit( "error", Popcorn.plugin.errors );
1954
+ this.emit( "pluginerror", Popcorn.plugin.errors );
1744
1955
  }
1745
1956
  };
1746
1957
  }
1747
1958
 
1748
1959
  // Debug-mode flag for plugin development
1749
- Popcorn.plugin.debug = false;
1960
+ // True for Popcorn development versions, false for stable/tagged versions
1961
+ Popcorn.plugin.debug = ( Popcorn.version === "@" + "VERSION" );
1750
1962
 
1751
1963
  // removePlugin( type ) removes all tracks of that from all instances of popcorn
1752
1964
  // removePlugin( obj, type ) removes all tracks of type from obj, where obj is a single instance of popcorn
@@ -1843,6 +2055,65 @@
1843
2055
 
1844
2056
  Popcorn.plugin.effect = Popcorn.effect = Popcorn.compose;
1845
2057
 
2058
+ var rnaiveExpr = /^(?:\.|#|\[)/;
2059
+
2060
+ // Basic DOM utilities and helpers API. See #1037
2061
+ Popcorn.dom = {
2062
+ debug: false,
2063
+ // Popcorn.dom.find( selector, context )
2064
+ //
2065
+ // Returns the first element that matches the specified selector
2066
+ // Optionally provide a context element, defaults to `document`
2067
+ //
2068
+ // eg.
2069
+ // Popcorn.dom.find("video") returns the first video element
2070
+ // Popcorn.dom.find("#foo") returns the first element with `id="foo"`
2071
+ // Popcorn.dom.find("foo") returns the first element with `id="foo"`
2072
+ // Note: Popcorn.dom.find("foo") is the only allowed deviation
2073
+ // from valid querySelector selector syntax
2074
+ //
2075
+ // Popcorn.dom.find(".baz") returns the first element with `class="baz"`
2076
+ // Popcorn.dom.find("[preload]") returns the first element with `preload="..."`
2077
+ // ...
2078
+ // See https://developer.mozilla.org/En/DOM/Document.querySelector
2079
+ //
2080
+ //
2081
+ find: function( selector, context ) {
2082
+ var node = null;
2083
+
2084
+ // Trim leading/trailing whitespace to avoid false negatives
2085
+ selector = selector.trim();
2086
+
2087
+ // Default context is the `document`
2088
+ context = context || document;
2089
+
2090
+ if ( selector ) {
2091
+ // If the selector does not begin with "#", "." or "[",
2092
+ // it could be either a nodeName or ID w/o "#"
2093
+ if ( !rnaiveExpr.test( selector ) ) {
2094
+
2095
+ // Try finding an element that matches by ID first
2096
+ node = document.getElementById( selector );
2097
+
2098
+ // If a match was found by ID, return the element
2099
+ if ( node !== null ) {
2100
+ return node;
2101
+ }
2102
+ }
2103
+ // Assume no elements have been found yet
2104
+ // Catch any invalid selector syntax errors and bury them.
2105
+ try {
2106
+ node = context.querySelector( selector );
2107
+ } catch ( e ) {
2108
+ if ( Popcorn.dom.debug ) {
2109
+ throw new Error(e);
2110
+ }
2111
+ }
2112
+ }
2113
+ return node;
2114
+ }
2115
+ };
2116
+
1846
2117
  // Cache references to reused RegExps
1847
2118
  var rparams = /\?/,
1848
2119
  // XHR Setup object
@@ -1952,31 +2223,53 @@
1952
2223
 
1953
2224
  var head = document.head || document.getElementsByTagName( "head" )[ 0 ] || document.documentElement,
1954
2225
  script = document.createElement( "script" ),
1955
- paramStr = url.split( "?" )[ 1 ],
1956
2226
  isFired = false,
1957
2227
  params = [],
1958
- callback, parts, callparam;
2228
+ rjsonp = /(=)\?(?=&|$)|\?\?/,
2229
+ replaceInUrl, prefix, paramStr, callback, callparam;
1959
2230
 
1960
- if ( paramStr && !isScript ) {
1961
- params = paramStr.split( "&" );
1962
- }
2231
+ if ( !isScript ) {
1963
2232
 
1964
- if ( params.length ) {
1965
- parts = params[ params.length - 1 ].split( "=" );
1966
- }
2233
+ // is there a calback already in the url
2234
+ callparam = url.match( /(callback=[^&]*)/ );
1967
2235
 
1968
- callback = params.length ? ( parts[ 1 ] ? parts[ 1 ] : parts[ 0 ] ) : "jsonp";
2236
+ if ( callparam !== null && callparam.length ) {
1969
2237
 
1970
- if ( !paramStr && !isScript ) {
1971
- url += "?callback=" + callback;
1972
- }
2238
+ prefix = callparam[ 1 ].split( "=" )[ 1 ];
2239
+
2240
+ // Since we need to support developer specified callbacks
2241
+ // and placeholders in harmony, make sure matches to "callback="
2242
+ // aren't just placeholders.
2243
+ // We coded ourselves into a corner here.
2244
+ // JSONP callbacks should never have been
2245
+ // allowed to have developer specified callbacks
2246
+ if ( prefix === "?" ) {
2247
+ prefix = "jsonp";
2248
+ }
2249
+
2250
+ // get the callback name
2251
+ callback = Popcorn.guid( prefix );
2252
+
2253
+ // replace existing callback name with unique callback name
2254
+ url = url.replace( /(callback=[^&]*)/, "callback=" + callback );
2255
+ } else {
2256
+
2257
+ callback = Popcorn.guid( "jsonp" );
2258
+
2259
+ if ( rjsonp.test( url ) ) {
2260
+ url = url.replace( rjsonp, "$1" + callback );
2261
+ }
1973
2262
 
1974
- if ( callback && !isScript ) {
2263
+ // split on first question mark,
2264
+ // this is to capture the query string
2265
+ params = url.split( /\?(.+)?/ );
1975
2266
 
1976
- // If a callback name already exists
1977
- if ( !!window[ callback ] ) {
1978
- // Create a new unique callback name
1979
- callback = Popcorn.guid( callback );
2267
+ // rebuild url with callback
2268
+ url = params[ 0 ] + "?";
2269
+ if ( params[ 1 ] ) {
2270
+ url += params[ 1 ] + "&";
2271
+ }
2272
+ url += "callback=" + callback;
1980
2273
  }
1981
2274
 
1982
2275
  // Define the JSONP success callback globally
@@ -1985,9 +2278,6 @@
1985
2278
  success && success( data );
1986
2279
  isFired = true;
1987
2280
  };
1988
-
1989
- // Replace callback param and callback name
1990
- url = url.replace( parts.join( "=" ), parts[ 0 ] + "=" + callback );
1991
2281
  }
1992
2282
 
1993
2283
  script.addEventListener( "load", function() {
@@ -2138,6 +2428,13 @@
2138
2428
  // ID string matching
2139
2429
  var rIdExp = /^(#([\w\-\_\.]+))$/;
2140
2430
 
2431
+ var audioExtensions = "ogg|oga|aac|mp3|wav",
2432
+ videoExtensions = "ogg|ogv|mp4|webm",
2433
+ mediaExtensions = audioExtensions + "|" + videoExtensions;
2434
+
2435
+ var audioExtensionRegexp = new RegExp( "^.*\\.(" + audioExtensions + ")($|\\?)" ),
2436
+ mediaExtensionRegexp = new RegExp( "^.*\\.(" + mediaExtensions + ")($|\\?)" );
2437
+
2141
2438
  Popcorn.player = function( name, player ) {
2142
2439
 
2143
2440
  // return early if a player already exists under this name
@@ -2162,9 +2459,7 @@
2162
2459
  events = {},
2163
2460
 
2164
2461
  // The container div of the resource
2165
- container = document.getElementById( rIdExp.exec( target ) && rIdExp.exec( target )[ 2 ] ) ||
2166
- document.getElementById( target ) ||
2167
- target,
2462
+ container = typeof target === "string" ? Popcorn.dom.find( target ) : target,
2168
2463
  basePlayer = {},
2169
2464
  timeout,
2170
2465
  popcorn;
@@ -2309,7 +2604,7 @@
2309
2604
  },
2310
2605
  configurable: true
2311
2606
  });
2312
-
2607
+
2313
2608
  // Adds an event listener to the object
2314
2609
  basePlayer.addEventListener = function( evtName, fn ) {
2315
2610
 
@@ -2403,32 +2698,18 @@
2403
2698
  }
2404
2699
  } else {
2405
2700
 
2406
- basePlayer.dispatchEvent( "error" );
2701
+ // Asynchronous so that users can catch this event
2702
+ setTimeout( function() {
2703
+ basePlayer.dispatchEvent( "error" );
2704
+ }, 0 );
2407
2705
  }
2408
2706
 
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
2707
  popcorn = new Popcorn.p.init( basePlayer, options );
2427
2708
 
2428
2709
  if ( player._teardown ) {
2429
2710
 
2430
2711
  popcorn.destroy = combineFn( popcorn.destroy, function() {
2431
-
2712
+
2432
2713
  player._teardown.call( basePlayer, options );
2433
2714
  });
2434
2715
  }
@@ -2449,19 +2730,81 @@
2449
2730
  object.__defineSetter__( description, options.set || Popcorn.nop );
2450
2731
  };
2451
2732
 
2733
+ // player queue is to help players queue things like play and pause
2734
+ // HTML5 video's play and pause are asynch, but do fire in sequence
2735
+ // play() should really mean "requestPlay()" or "queuePlay()" and
2736
+ // stash a callback that will play the media resource when it's ready to be played
2737
+ Popcorn.player.playerQueue = function() {
2738
+
2739
+ var _queue = [],
2740
+ _running = false;
2741
+
2742
+ return {
2743
+ next: function() {
2744
+
2745
+ _running = false;
2746
+ _queue.shift();
2747
+ _queue[ 0 ] && _queue[ 0 ]();
2748
+ },
2749
+ add: function( callback ) {
2750
+
2751
+ _queue.push(function() {
2752
+
2753
+ _running = true;
2754
+ callback && callback();
2755
+ });
2756
+
2757
+ // if there is only one item on the queue, start it
2758
+ !_running && _queue[ 0 ]();
2759
+ }
2760
+ };
2761
+ };
2762
+
2452
2763
  // smart will attempt to find you a match, if it does not find a match,
2453
2764
  // it will attempt to create a video element with the source,
2454
2765
  // if that failed, it will throw.
2455
2766
  Popcorn.smart = function( target, src, options ) {
2767
+ var playerType,
2768
+ elementTypes = [ "AUDIO", "VIDEO" ],
2769
+ sourceNode,
2770
+ firstSrc,
2771
+ node = Popcorn.dom.find( target ),
2772
+ i, srcResult,
2773
+ canPlayTypeTester = document.createElement( "video" ),
2774
+ canPlayTypes = {
2775
+ "ogg": "video/ogg",
2776
+ "ogv": "video/ogg",
2777
+ "oga": "audio/ogg",
2778
+ "webm": "video/webm",
2779
+ "mp4": "video/mp4",
2780
+ "mp3": "audio/mp3"
2781
+ };
2782
+
2783
+ var canPlayType = function( type ) {
2456
2784
 
2457
- var nodeId = rIdExp.exec( target ),
2458
- playerType,
2459
- node = nodeId && nodeId.length && nodeId[ 2 ] ?
2460
- document.getElementById( nodeId[ 2 ] ) :
2461
- target;
2785
+ return canPlayTypeTester.canPlayType( canPlayTypes[ type ] );
2786
+ };
2462
2787
 
2463
- // Popcorn.smart( video, /* options */ )
2464
- if ( node.nodeType === "VIDEO" && !src ) {
2788
+ var canPlaySrc = function( src ) {
2789
+
2790
+ srcResult = mediaExtensionRegexp.exec( src );
2791
+
2792
+ if ( !srcResult || !srcResult[ 1 ] ) {
2793
+ return false;
2794
+ }
2795
+
2796
+ return canPlayType( srcResult[ 1 ] );
2797
+ };
2798
+
2799
+ if ( !node ) {
2800
+
2801
+ Popcorn.error( "Specified target " + target + " was not found." );
2802
+ return;
2803
+ }
2804
+
2805
+ // For when no src is defined.
2806
+ // Usually this is a video element with a src already on it.
2807
+ if ( elementTypes.indexOf( node.nodeName ) > -1 && !src ) {
2465
2808
 
2466
2809
  if ( typeof src === "object" ) {
2467
2810
 
@@ -2472,24 +2815,49 @@
2472
2815
  return Popcorn( node, options );
2473
2816
  }
2474
2817
 
2475
- // for now we loop through and use the first valid player we find.
2476
- for ( var key in Popcorn.player.registry ) {
2818
+ // if our src is not an array, create an array of one.
2819
+ if ( typeof( src ) === "string" ) {
2820
+
2821
+ src = [ src ];
2822
+ }
2823
+
2824
+ // go through each src, and find the first playable.
2825
+ // this only covers player sources popcorn knows of,
2826
+ // and not things like a youtube src that is private.
2827
+ // it will still consider a private youtube video to be playable.
2828
+ for ( i = 0, srcLength = src.length; i < srcLength; i++ ) {
2477
2829
 
2478
- if ( Popcorn.player.registry.hasOwnProperty( key ) ) {
2830
+ // src is a playable HTML5 video, we don't need to check custom players.
2831
+ if ( canPlaySrc( src[ i ] ) ) {
2479
2832
 
2480
- if ( Popcorn.player.registry[ key ].canPlayType( node.nodeName, src ) ) {
2833
+ src = src[ i ];
2834
+ break;
2835
+ }
2836
+
2837
+ // for now we loop through and use the first valid player we find.
2838
+ for ( var key in Popcorn.player.registry ) {
2839
+
2840
+ if ( Popcorn.player.registry.hasOwnProperty( key ) ) {
2481
2841
 
2482
- // Popcorn.smart( player, src, /* options */ )
2483
- return Popcorn[ key ]( target, src, options );
2842
+ if ( Popcorn.player.registry[ key ].canPlayType( node.nodeName, src[ i ] ) ) {
2843
+
2844
+ // Popcorn.smart( player, src, /* options */ )
2845
+ return Popcorn[ key ]( node, src[ i ], options );
2846
+ }
2484
2847
  }
2485
2848
  }
2486
2849
  }
2487
2850
 
2488
2851
  // Popcorn.smart( div, src, /* options */ )
2489
2852
  // attempting to create a video in a container
2490
- if ( node.nodeType !== "VIDEO" ) {
2853
+ if ( elementTypes.indexOf( node.nodeName ) === -1 ) {
2854
+
2855
+ firstSrc = typeof( src ) === "string" ? src : src.length ? src[ 0 ] : src;
2856
+
2857
+ target = document.createElement( !!audioExtensionRegexp.exec( firstSrc ) ? elementTypes[ 0 ] : elementTypes[ 1 ] );
2491
2858
 
2492
- target = document.createElement( "video" );
2859
+ // Controls are defaulted to being present
2860
+ target.controls = true;
2493
2861
 
2494
2862
  node.appendChild( target );
2495
2863
  node = target;
@@ -2499,351 +2867,384 @@
2499
2867
  node.src = src;
2500
2868
 
2501
2869
  return Popcorn( node, options );
2502
- };
2503
2870
 
2871
+ };
2504
2872
  })( Popcorn );
2505
- // A global callback for youtube... that makes me angry
2506
- var onYouTubePlayerReady = function( containerId ) {
2873
+ (function( window, Popcorn ) {
2874
+ // A global callback for youtube... that makes me angry
2875
+ window.onYouTubePlayerAPIReady = function() {
2507
2876
 
2508
- onYouTubePlayerReady[ containerId ] && onYouTubePlayerReady[ containerId ]();
2509
- };
2510
- onYouTubePlayerReady.stateChangeEventHandler = {};
2511
- onYouTubePlayerReady.onErrorEventHandler = {};
2877
+ onYouTubePlayerAPIReady.ready = true;
2878
+ for ( var i = 0; i < onYouTubePlayerAPIReady.waiting.length; i++ ) {
2879
+ onYouTubePlayerAPIReady.waiting[ i ]();
2880
+ }
2881
+ };
2512
2882
 
2513
- Popcorn.player( "youtube", {
2514
- _canPlayType: function( nodeName, url ) {
2883
+ // existing youtube references can break us.
2884
+ // remove it and use the one we can trust.
2885
+ if ( window.YT ) {
2886
+ window.quarantineYT = window.YT;
2887
+ window.YT = null;
2888
+ }
2515
2889
 
2516
- return (/(?:http:\/\/www\.|http:\/\/|www\.|\.|^)(youtu)/).test( url ) && nodeName.toLowerCase() !== "video";
2517
- },
2518
- _setup: function( options ) {
2890
+ onYouTubePlayerAPIReady.waiting = [];
2519
2891
 
2520
- var media = this,
2521
- autoPlay = false,
2522
- container = document.createElement( "div" ),
2523
- currentTime = 0,
2524
- seekTime = 0,
2525
- firstGo = true,
2526
- seeking = false,
2892
+ var _loading = false;
2527
2893
 
2528
- // state code for volume changed polling
2529
- volumeChanged = false,
2530
- lastMuted = false,
2531
- lastVolume = 100;
2894
+ Popcorn.player( "youtube", {
2895
+ _canPlayType: function( nodeName, url ) {
2532
2896
 
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();
2897
+ return typeof url === "string" && (/(?:http:\/\/www\.|http:\/\/|www\.|\.|^)(youtu)/).test( url ) && nodeName.toLowerCase() !== "video";
2898
+ },
2899
+ _setup: function( options ) {
2900
+ if ( !window.YT && !_loading ) {
2901
+ _loading = true;
2902
+ Popcorn.getScript( "//youtube.com/player_api" );
2903
+ }
2536
2904
 
2537
- options._container = container;
2905
+ var media = this,
2906
+ autoPlay = false,
2907
+ container = document.createElement( "div" ),
2908
+ currentTime = 0,
2909
+ paused = true,
2910
+ seekTime = 0,
2911
+ firstGo = true,
2912
+ seeking = false,
2913
+ fragmentStart = 0,
2538
2914
 
2539
- media.appendChild( container );
2915
+ // state code for volume changed polling
2916
+ lastMuted = false,
2917
+ lastVolume = 100,
2918
+ playerQueue = Popcorn.player.playerQueue();
2540
2919
 
2541
- var youtubeInit = function() {
2920
+ var createProperties = function() {
2542
2921
 
2543
- var flashvars,
2544
- params,
2545
- attributes,
2546
- src,
2547
- width,
2548
- height,
2549
- query;
2922
+ Popcorn.player.defineProperty( media, "currentTime", {
2923
+ set: function( val ) {
2550
2924
 
2551
- // expose a callback to this scope, that is called from the global callback youtube calls
2552
- onYouTubePlayerReady[ container.id ] = function() {
2925
+ if ( options.destroyed ) {
2926
+ return;
2927
+ }
2553
2928
 
2554
- options.youtubeObject = document.getElementById( container.id );
2929
+ seeking = true;
2930
+ // make sure val is a number
2931
+ currentTime = Math.round( +val * 100 ) / 100;
2932
+ },
2933
+ get: function() {
2555
2934
 
2556
- // more youtube callback nonsense
2557
- onYouTubePlayerReady.stateChangeEventHandler[ container.id ] = function( state ) {
2935
+ return currentTime;
2936
+ }
2937
+ });
2558
2938
 
2559
- if ( options.destroyed ) {
2939
+ Popcorn.player.defineProperty( media, "paused", {
2940
+ get: function() {
2560
2941
 
2561
- return;
2942
+ return paused;
2562
2943
  }
2944
+ });
2563
2945
 
2564
- // youtube fires paused events while seeking
2565
- // this is the only way to get seeking events
2566
- if ( state === 2 ) {
2946
+ Popcorn.player.defineProperty( media, "muted", {
2947
+ set: function( val ) {
2567
2948
 
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() ) {
2949
+ if ( options.destroyed ) {
2572
2950
 
2573
- seeking = false;
2574
- options.youtubeObject.seekTo( currentTime );
2575
- return;
2951
+ return val;
2576
2952
  }
2577
2953
 
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 ) {
2954
+ if ( options.youtubeObject.isMuted() !== val ) {
2604
2955
 
2605
- media.pause();
2606
- } else if ( media.paused === false ) {
2956
+ if ( val ) {
2607
2957
 
2608
- media.play();
2609
- } else if ( autoPlay ) {
2958
+ options.youtubeObject.mute();
2959
+ } else {
2610
2960
 
2611
- media.play();
2612
- } else if ( !autoPlay ) {
2961
+ options.youtubeObject.unMute();
2962
+ }
2613
2963
 
2614
- media.pause();
2964
+ lastMuted = options.youtubeObject.isMuted();
2965
+ media.dispatchEvent( "volumechange" );
2615
2966
  }
2616
2967
 
2617
- media.duration = options.youtubeObject.getDuration();
2618
-
2619
- media.dispatchEvent( "durationchange" );
2620
- volumeupdate();
2968
+ return options.youtubeObject.isMuted();
2969
+ },
2970
+ get: function() {
2621
2971
 
2622
- media.dispatchEvent( "loadedmetadata" );
2623
- media.dispatchEvent( "loadeddata" );
2972
+ if ( options.destroyed ) {
2624
2973
 
2625
- media.readyState = 4;
2626
- media.dispatchEvent( "canplaythrough" );
2974
+ return 0;
2975
+ }
2627
2976
 
2628
- return;
2629
- } else if ( state === 0 ) {
2630
- media.dispatchEvent( "ended" );
2977
+ return options.youtubeObject.isMuted();
2631
2978
  }
2632
- };
2979
+ });
2633
2980
 
2634
- onYouTubePlayerReady.onErrorEventHandler[ container.id ] = function( errorCode ) {
2635
- if ( [ 2, 100, 101, 150 ].indexOf( errorCode ) !== -1 ) {
2636
- media.dispatchEvent( "error" );
2637
- }
2638
- };
2981
+ Popcorn.player.defineProperty( media, "volume", {
2982
+ set: function( val ) {
2639
2983
 
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 );
2984
+ if ( options.destroyed ) {
2642
2985
 
2643
- options.youtubeObject.addEventListener( "onError", "onYouTubePlayerReady.onErrorEventHandler." + container.id );
2986
+ return val;
2987
+ }
2644
2988
 
2645
- var timeupdate = function() {
2989
+ if ( options.youtubeObject.getVolume() / 100 !== val ) {
2646
2990
 
2647
- if ( options.destroyed ) {
2991
+ options.youtubeObject.setVolume( val * 100 );
2992
+ lastVolume = options.youtubeObject.getVolume();
2993
+ media.dispatchEvent( "volumechange" );
2994
+ }
2648
2995
 
2649
- return;
2650
- }
2996
+ return options.youtubeObject.getVolume() / 100;
2997
+ },
2998
+ get: function() {
2651
2999
 
2652
- if ( !media.paused ) {
3000
+ if ( options.destroyed ) {
2653
3001
 
2654
- currentTime = options.youtubeObject.getCurrentTime();
2655
- media.dispatchEvent( "timeupdate" );
2656
- setTimeout( timeupdate, 10 );
3002
+ return 0;
3003
+ }
3004
+
3005
+ return options.youtubeObject.getVolume() / 100;
2657
3006
  }
2658
- };
3007
+ });
2659
3008
 
2660
- var volumeupdate = function() {
3009
+ media.play = function() {
2661
3010
 
2662
3011
  if ( options.destroyed ) {
2663
3012
 
2664
3013
  return;
2665
3014
  }
2666
3015
 
2667
- if ( lastMuted !== options.youtubeObject.isMuted() ) {
3016
+ paused = false;
3017
+ playerQueue.add(function() {
2668
3018
 
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
- }
3019
+ if ( options.youtubeObject.getPlayerState() !== 1 ) {
2678
3020
 
2679
- setTimeout( volumeupdate, 250 );
3021
+ seeking = false;
3022
+ options.youtubeObject.playVideo();
3023
+ } else {
3024
+ playerQueue.next();
3025
+ }
3026
+ });
2680
3027
  };
2681
3028
 
2682
- media.play = function() {
3029
+ media.pause = function() {
2683
3030
 
2684
3031
  if ( options.destroyed ) {
2685
3032
 
2686
3033
  return;
2687
3034
  }
2688
3035
 
2689
- if ( media.paused !== false || options.youtubeObject.getPlayerState() !== 1 ) {
2690
-
2691
- media.paused = false;
2692
- media.dispatchEvent( "play" );
3036
+ paused = true;
3037
+ playerQueue.add(function() {
2693
3038
 
2694
- media.dispatchEvent( "playing" );
2695
- }
3039
+ if ( options.youtubeObject.getPlayerState() !== 2 ) {
2696
3040
 
2697
- timeupdate();
2698
- options.youtubeObject.playVideo();
3041
+ options.youtubeObject.pauseVideo();
3042
+ } else {
3043
+ playerQueue.next();
3044
+ }
3045
+ });
2699
3046
  };
3047
+ };
2700
3048
 
2701
- media.pause = function() {
3049
+ container.id = media.id + Popcorn.guid();
3050
+ options._container = container;
3051
+ media.appendChild( container );
2702
3052
 
2703
- if ( options.destroyed ) {
3053
+ var youtubeInit = function() {
2704
3054
 
2705
- return;
2706
- }
3055
+ var src, query, params, playerVars, queryStringItem, firstPlay = true;
2707
3056
 
2708
- if ( media.paused !== true || options.youtubeObject.getPlayerState() !== 2 ) {
3057
+ var timeUpdate = function() {
2709
3058
 
2710
- media.paused = true;
2711
- media.dispatchEvent( "pause" );
2712
- options.youtubeObject.pauseVideo();
3059
+ if ( options.destroyed ) {
3060
+ return;
2713
3061
  }
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
3062
 
2725
- return currentTime;
2726
- }
3063
+ if ( !seeking ) {
3064
+ currentTime = options.youtubeObject.getCurrentTime();
3065
+ media.dispatchEvent( "timeupdate" );
3066
+ } else if ( currentTime === options.youtubeObject.getCurrentTime() ) {
2727
3067
 
3068
+ seeking = false;
2728
3069
  media.dispatchEvent( "seeked" );
2729
3070
  media.dispatchEvent( "timeupdate" );
3071
+ } else {
2730
3072
 
3073
+ // keep trying the seek until it is right.
2731
3074
  options.youtubeObject.seekTo( currentTime );
3075
+ }
3076
+ setTimeout( timeUpdate, 250 );
3077
+ };
2732
3078
 
2733
- return currentTime;
2734
- },
2735
- get: function() {
3079
+ // delay is in seconds
3080
+ var fetchDuration = function( delay ) {
3081
+ var ytDuration = options.youtubeObject.getDuration();
2736
3082
 
2737
- return currentTime;
3083
+ if ( isNaN( ytDuration ) || ytDuration === 0 ) {
3084
+ setTimeout( function() {
3085
+ fetchDuration( delay * 2 );
3086
+ }, delay*1000 );
3087
+ } else {
3088
+ // set duration and dispatch ready events
3089
+ media.duration = ytDuration;
3090
+ media.dispatchEvent( "durationchange" );
3091
+
3092
+ media.dispatchEvent( "loadedmetadata" );
3093
+ media.dispatchEvent( "loadeddata" );
3094
+
3095
+ media.readyState = 4;
3096
+
3097
+ timeUpdate();
3098
+ media.dispatchEvent( "canplaythrough" );
2738
3099
  }
2739
- });
3100
+ };
2740
3101
 
2741
- Popcorn.player.defineProperty( media, "muted", {
2742
- set: function( val ) {
3102
+ options.controls = +options.controls === 0 || +options.controls === 1 ? options.controls : 1;
3103
+ options.annotations = +options.annotations === 1 || +options.annotations === 3 ? options.annotations : 1;
2743
3104
 
2744
- if ( options.destroyed ) {
3105
+ src = /^.*(?:\/|v=)(.{11})/.exec( media.src )[ 1 ];
2745
3106
 
2746
- return val;
2747
- }
3107
+ query = ( media.src.split( "?" )[ 1 ] || "" )
3108
+ .replace( /v=.{11}/, "" );
3109
+ query = query.replace( /&t=(?:(\d+)m)?(?:(\d+)s)?/, function( all, minutes, seconds ) {
2748
3110
 
2749
- if ( options.youtubeObject.isMuted() !== val ) {
3111
+ // Make sure we have real zeros
3112
+ minutes = minutes | 0; // bit-wise OR
3113
+ seconds = seconds | 0; // bit-wise OR
2750
3114
 
2751
- if ( val ) {
3115
+ fragmentStart = ( +seconds + ( minutes * 60 ) );
3116
+ return "";
3117
+ });
3118
+ query = query.replace( /&start=(\d+)?/, function( all, seconds ) {
2752
3119
 
2753
- options.youtubeObject.mute();
2754
- } else {
3120
+ // Make sure we have real zeros
3121
+ seconds = seconds | 0; // bit-wise OR
2755
3122
 
2756
- options.youtubeObject.unMute();
2757
- }
3123
+ fragmentStart = seconds;
3124
+ return "";
3125
+ });
2758
3126
 
2759
- lastMuted = options.youtubeObject.isMuted();
2760
- media.dispatchEvent( "volumechange" );
2761
- }
3127
+ autoPlay = ( /autoplay=1/.test( query ) );
2762
3128
 
2763
- return options.youtubeObject.isMuted();
2764
- },
2765
- get: function() {
3129
+ params = query.split( /[\&\?]/g );
3130
+ playerVars = { wmode: "transparent" };
2766
3131
 
2767
- if ( options.destroyed ) {
3132
+ for( var i = 0; i < params.length; i++ ) {
3133
+ queryStringItem = params[ i ].split( "=" );
3134
+ playerVars[ queryStringItem[ 0 ] ] = queryStringItem[ 1 ];
3135
+ }
3136
+
3137
+ options.youtubeObject = new YT.Player( container.id, {
3138
+ height: "100%",
3139
+ width: "100%",
3140
+ wmode: "transparent",
3141
+ playerVars: playerVars,
3142
+ videoId: src,
3143
+ events: {
3144
+ "onReady": function(){
3145
+
3146
+ // pulling initial volume states form baseplayer
3147
+ lastVolume = media.volume;
3148
+ lastMuted = media.muted;
3149
+
3150
+ volumeupdate();
3151
+
3152
+ paused = media.paused;
3153
+ createProperties();
3154
+ options.youtubeObject.playVideo();
3155
+
3156
+ media.currentTime = fragmentStart;
3157
+ // wait to dispatch ready events until we get a duration
3158
+ },
3159
+ "onStateChange": function( state ){
3160
+
3161
+ if ( options.destroyed || state.data === -1 ) {
3162
+ return;
3163
+ }
2768
3164
 
2769
- return 0;
3165
+ // state.data === 2 is for pause events
3166
+ // state.data === 1 is for play events
3167
+ if ( state.data === 2 ) {
3168
+ paused = true;
3169
+ media.dispatchEvent( "pause" );
3170
+ playerQueue.next();
3171
+ } else if ( state.data === 1 && !firstPlay ) {
3172
+ paused = false;
3173
+ media.dispatchEvent( "play" );
3174
+ media.dispatchEvent( "playing" );
3175
+ playerQueue.next();
3176
+ } else if ( state.data === 0 ) {
3177
+ media.dispatchEvent( "ended" );
3178
+ } else if ( state.data === 1 && firstPlay ) {
3179
+ firstPlay = false;
3180
+
3181
+ // pulling initial paused state from autoplay or the baseplayer
3182
+ // also need to explicitly set to paused otherwise.
3183
+ if ( autoPlay || !media.paused ) {
3184
+ paused = false;
3185
+ }
3186
+
3187
+ if ( paused ) {
3188
+ options.youtubeObject.pauseVideo();
3189
+ }
3190
+
3191
+ fetchDuration( 0.025 );
3192
+ }
3193
+ },
3194
+ "onError": function( error ) {
3195
+
3196
+ if ( [ 2, 100, 101, 150 ].indexOf( error.data ) !== -1 ) {
3197
+ media.error = {
3198
+ customCode: error.data
3199
+ };
3200
+ media.dispatchEvent( "error" );
3201
+ }
2770
3202
  }
2771
-
2772
- return options.youtubeObject.isMuted();
2773
3203
  }
2774
3204
  });
2775
3205
 
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
- }
3206
+ var volumeupdate = function() {
2790
3207
 
2791
- return options.youtubeObject.getVolume() / 100;
2792
- },
2793
- get: function() {
3208
+ if ( options.destroyed ) {
2794
3209
 
2795
- if ( options.destroyed ) {
3210
+ return;
3211
+ }
2796
3212
 
2797
- return 0;
2798
- }
3213
+ if ( lastMuted !== options.youtubeObject.isMuted() ) {
2799
3214
 
2800
- return options.youtubeObject.getVolume() / 100;
3215
+ lastMuted = options.youtubeObject.isMuted();
3216
+ media.dispatchEvent( "volumechange" );
2801
3217
  }
2802
- });
2803
- };
2804
3218
 
2805
- options.controls = +options.controls === 0 || +options.controls === 1 ? options.controls : 1;
2806
- options.annotations = +options.annotations === 1 || +options.annotations === 3 ? options.annotations : 1;
3219
+ if ( lastVolume !== options.youtubeObject.getVolume() ) {
2807
3220
 
2808
- flashvars = {
2809
- playerapiid: container.id
2810
- };
3221
+ lastVolume = options.youtubeObject.getVolume();
3222
+ media.dispatchEvent( "volumechange" );
3223
+ }
2811
3224
 
2812
- params = {
2813
- wmode: "transparent",
2814
- allowScriptAccess: "always"
3225
+ setTimeout( volumeupdate, 250 );
3226
+ };
2815
3227
  };
2816
3228
 
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";
3229
+ if ( onYouTubePlayerAPIReady.ready ) {
2825
3230
 
2826
- attributes = {
2827
- id: container.id,
2828
- "data-youtube-player": "//www.youtube.com/e/" + src + "?" + query + "&enablejsapi=1&playerapiid=" + container.id + "&version=3"
2829
- };
3231
+ youtubeInit();
3232
+ } else {
2830
3233
 
2831
- swfobject.embedSWF( attributes[ "data-youtube-player" ], container.id, width, height, "8", undefined, flashvars, params, attributes );
2832
- };
3234
+ onYouTubePlayerAPIReady.waiting.push( youtubeInit );
3235
+ }
3236
+ },
3237
+ _teardown: function( options ) {
2833
3238
 
2834
- if ( !window.swfobject ) {
3239
+ options.destroyed = true;
2835
3240
 
2836
- Popcorn.getScript( "//ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js", youtubeInit );
2837
- } else {
3241
+ var youtubeObject = options.youtubeObject;
3242
+ if( youtubeObject ){
3243
+ youtubeObject.stopVideo();
3244
+ youtubeObject.clearVideo && youtubeObject.clearVideo();
3245
+ }
2838
3246
 
2839
- youtubeInit();
3247
+ this.removeChild( document.getElementById( options._container.id ) );
2840
3248
  }
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
- });
3249
+ });
3250
+ }( window, Popcorn ));