1(function( Popcorn ) {
2
3  // combines calls of two function calls into one
4  var combineFn = function( first, second ) {
5
6    first = first || Popcorn.nop;
7    second = second || Popcorn.nop;
8
9    return function() {
10
11      first.apply( this, arguments );
12      second.apply( this, arguments );
13    };
14  };
15
16  //  ID string matching
17  var rIdExp  = /^(#([\w\-\_\.]+))$/;
18
19  Popcorn.player = function( name, player ) {
20
21    // return early if a player already exists under this name
22    if ( Popcorn[ name ] ) {
23
24      return;
25    }
26
27    player = player || {};
28
29    var playerFn = function( target, src, options ) {
30
31      options = options || {};
32
33      // List of events
34      var date = new Date() / 1000,
35          baselineTime = date,
36          currentTime = 0,
37          readyState = 0,
38          volume = 1,
39          muted = false,
40          events = {},
41
42          // The container div of the resource
43          container = typeof target === "string" ? Popcorn.dom.find( target ) : target,
44          basePlayer = {},
45          timeout,
46          popcorn;
47
48      if ( !Object.prototype.__defineGetter__ ) {
49
50        basePlayer = container || document.createElement( "div" );
51      }
52
53      // copies a div into the media object
54      for( var val in container ) {
55
56        // don't copy properties if using container as baseplayer
57        if ( val in basePlayer ) {
58
59          continue;
60        }
61
62        if ( typeof container[ val ] === "object" ) {
63
64          basePlayer[ val ] = container[ val ];
65        } else if ( typeof container[ val ] === "function" ) {
66
67          basePlayer[ val ] = (function( value ) {
68
69            // this is a stupid ugly kludgy hack in honour of Safari
70            // in Safari a NodeList is a function, not an object
71            if ( "length" in container[ value ] && !container[ value ].call ) {
72
73              return container[ value ];
74            } else {
75
76              return function() {
77
78                return container[ value ].apply( container, arguments );
79              };
80            }
81          }( val ));
82        } else {
83
84          Popcorn.player.defineProperty( basePlayer, val, {
85            get: (function( value ) {
86
87              return function() {
88
89                return container[ value ];
90              };
91            }( val )),
92            set: Popcorn.nop,
93            configurable: true
94          });
95        }
96      }
97
98      var timeupdate = function() {
99
100        date = new Date() / 1000;
101
102        if ( !basePlayer.paused ) {
103
104          basePlayer.currentTime = basePlayer.currentTime + ( date - baselineTime );
105          basePlayer.dispatchEvent( "timeupdate" );
106          timeout = setTimeout( timeupdate, 10 );
107        }
108
109        baselineTime = date;
110      };
111
112      basePlayer.play = function() {
113
114        this.paused = false;
115
116        if ( basePlayer.readyState >= 4 ) {
117
118          baselineTime = new Date() / 1000;
119          basePlayer.dispatchEvent( "play" );
120          timeupdate();
121        }
122      };
123
124      basePlayer.pause = function() {
125
126        this.paused = true;
127        basePlayer.dispatchEvent( "pause" );
128      };
129
130      Popcorn.player.defineProperty( basePlayer, "currentTime", {
131        get: function() {
132
133          return currentTime;
134        },
135        set: function( val ) {
136
137          // make sure val is a number
138          currentTime = +val;
139          basePlayer.dispatchEvent( "timeupdate" );
140
141          return currentTime;
142        },
143        configurable: true
144      });
145
146      Popcorn.player.defineProperty( basePlayer, "volume", {
147        get: function() {
148
149          return volume;
150        },
151        set: function( val ) {
152
153          // make sure val is a number
154          volume = +val;
155          basePlayer.dispatchEvent( "volumechange" );
156          return volume;
157        },
158        configurable: true
159      });
160
161      Popcorn.player.defineProperty( basePlayer, "muted", {
162        get: function() {
163
164          return muted;
165        },
166        set: function( val ) {
167
168          // make sure val is a number
169          muted = +val;
170          basePlayer.dispatchEvent( "volumechange" );
171          return muted;
172        },
173        configurable: true
174      });
175
176      Popcorn.player.defineProperty( basePlayer, "readyState", {
177        get: function() {
178
179          return readyState;
180        },
181        set: function( val ) {
182
183          readyState = val;
184          return readyState;
185        },
186        configurable: true
187      });
188
189      // Adds an event listener to the object
190      basePlayer.addEventListener = function( evtName, fn ) {
191
192        if ( !events[ evtName ] ) {
193
194          events[ evtName ] = [];
195        }
196
197        events[ evtName ].push( fn );
198        return fn;
199      };
200
201      // Removes an event listener from the object
202      basePlayer.removeEventListener = function( evtName, fn ) {
203
204        var i,
205            listeners = events[ evtName ];
206
207        if ( !listeners ){
208
209          return;
210        }
211
212        // walk backwards so we can safely splice
213        for ( i = events[ evtName ].length - 1; i >= 0; i-- ) {
214
215          if( fn === listeners[ i ] ) {
216
217            listeners.splice(i, 1);
218          }
219        }
220
221        return fn;
222      };
223
224      // Can take event object or simple string
225      basePlayer.dispatchEvent = function( oEvent ) {
226
227        var evt,
228            self = this,
229            eventInterface,
230            eventName = oEvent.type;
231
232        // A string was passed, create event object
233        if ( !eventName ) {
234
235          eventName = oEvent;
236          eventInterface  = Popcorn.events.getInterface( eventName );
237
238          if ( eventInterface ) {
239
240            evt = document.createEvent( eventInterface );
241            evt.initEvent( eventName, true, true, window, 1 );
242          }
243        }
244
245        if ( events[ eventName ] ) {
246
247          for ( var i = events[ eventName ].length - 1; i >= 0; i-- ) {
248
249            events[ eventName ][ i ].call( self, evt, self );
250          }
251        }
252      };
253
254      // Attempt to get src from playerFn parameter
255      basePlayer.src = src || "";
256      basePlayer.duration = 0;
257      basePlayer.paused = true;
258      basePlayer.ended = 0;
259
260      options && options.events && Popcorn.forEach( options.events, function( val, key ) {
261
262        basePlayer.addEventListener( key, val, false );
263      });
264
265      // true and undefined returns on canPlayType means we should attempt to use it,
266      // false means we cannot play this type
267      if ( player._canPlayType( container.nodeName, src ) !== false ) {
268
269        if ( player._setup ) {
270
271          player._setup.call( basePlayer, options );
272        } else {
273
274          // there is no setup, which means there is nothing to load
275          basePlayer.readyState = 4;
276          basePlayer.dispatchEvent( "loadedmetadata" );
277          basePlayer.dispatchEvent( "loadeddata" );
278          basePlayer.dispatchEvent( "canplaythrough" );
279        }
280      } else {
281
282        // Asynchronous so that users can catch this event
283        setTimeout( function() {
284          basePlayer.dispatchEvent( "error" );
285        }, 0 );
286      }
287
288      popcorn = new Popcorn.p.init( basePlayer, options );
289
290      if ( player._teardown ) {
291
292        popcorn.destroy = combineFn( popcorn.destroy, function() {
293
294          player._teardown.call( basePlayer, options );
295        });
296      }
297
298      return popcorn;
299    };
300
301    playerFn.canPlayType = player._canPlayType = player._canPlayType || Popcorn.nop;
302
303    Popcorn[ name ] = Popcorn.player.registry[ name ] = playerFn;
304  };
305
306  Popcorn.player.registry = {};
307
308  Popcorn.player.defineProperty = Object.defineProperty || function( object, description, options ) {
309
310    object.__defineGetter__( description, options.get || Popcorn.nop );
311    object.__defineSetter__( description, options.set || Popcorn.nop );
312  };
313
314  // player queue is to help players queue things like play and pause
315  // HTML5 video's play and pause are asynch, but do fire in sequence
316  // play() should really mean "requestPlay()" or "queuePlay()" and
317  // stash a callback that will play the media resource when it's ready to be played
318  Popcorn.player.playerQueue = function() {
319
320    var _queue = [],
321        _running = false;
322
323    return {
324      next: function() {
325
326        _running = false;
327        _queue.shift();
328        _queue[ 0 ] && _queue[ 0 ]();
329      },
330      add: function( callback ) {
331
332        _queue.push(function() {
333
334          _running = true;
335          callback && callback();
336        });
337
338        // if there is only one item on the queue, start it
339        !_running && _queue[ 0 ]();
340      }
341    };
342  };
343
344  // Popcorn.smart will attempt to find you a wrapper or player. If it can't do that,
345  // it will default to using an HTML5 video in the target.
346  Popcorn.smart = function( target, src, options ) {
347    var node = typeof target === "string" ? Popcorn.dom.find( target ) : target,
348        i, srci, j, media, mediaWrapper, popcorn, srcLength,
349        // We leave HTMLVideoElement and HTMLAudioElement wrappers out
350        // of the mix, since we'll default to HTML5 video if nothing
351        // else works.  Waiting on #1254 before we add YouTube to this.
352        wrappers = "HTMLYouTubeVideoElement HTMLVimeoVideoElement HTMLSoundCloudAudioElement HTMLNullVideoElement".split(" ");
353
354    if ( !node ) {
355      Popcorn.error( "Specified target `" + target + "` was not found." );
356      return;
357    }
358
359    // If our src is not an array, create an array of one.
360    src = typeof src === "string" ? [ src ] : src;
361
362    // Loop through each src, and find the first playable.
363    for ( i = 0, srcLength = src.length; i < srcLength; i++ ) {
364      srci = src[ i ];
365
366      // See if we can use a wrapper directly, if not, try players.
367      for ( j = 0; j < wrappers.length; j++ ) {
368        mediaWrapper = Popcorn[ wrappers[ j ] ];
369        if ( mediaWrapper && mediaWrapper._canPlaySrc( srci ) === "probably" ) {
370          media = mediaWrapper( node );
371          popcorn = Popcorn( media, options );
372          // Set src, but not until after we return the media so the caller
373          // can get error events, if any.
374          setTimeout( function() {
375            media.src = srci;
376          }, 0 );
377          return popcorn;
378        }
379      }
380
381      // No wrapper can play this, check players.
382      for ( var key in Popcorn.player.registry ) {
383        if ( Popcorn.player.registry.hasOwnProperty( key ) ) {
384          if ( Popcorn.player.registry[ key ].canPlayType( node.nodeName, srci ) ) {
385            // Popcorn.smart( player, src, /* options */ )
386            return Popcorn[ key ]( node, srci, options );
387          }
388        }
389      }
390    }
391
392    // If we don't have any players or wrappers that can handle this,
393    // Default to using HTML5 video.  Similar to the HTMLVideoElement
394    // wrapper, we put a video in the div passed to us via:
395    // Popcorn.smart( div, src, options )
396    var videoHTML,
397        videoElement,
398        videoID = Popcorn.guid( "popcorn-video-" ),
399        videoHTMLContainer = document.createElement( "div" );
400
401    videoHTMLContainer.style.width = "100%";
402    videoHTMLContainer.style.height = "100%";
403
404    // If we only have one source, do not bother with source elements.
405    // This means we don't have the IE9 hack,
406    // and we can properly listen to error events.
407    // That way an error event can be told to backup to Flash if it fails.
408    if ( src.length === 1 ) {
409      videoElement = document.createElement( "video" );
410      videoElement.id = videoID;
411      node.appendChild( videoElement );
412      setTimeout( function() {
413        // Hack to decode html characters like &amp; to &
414        var decodeDiv = document.createElement( "div" );
415        decodeDiv.innerHTML = src[ 0 ];
416
417        videoElement.src = decodeDiv.firstChild.nodeValue;
418      }, 0 );
419      return Popcorn( '#' + videoID, options );
420    }
421
422    node.appendChild( videoHTMLContainer );
423    // IE9 doesn't like dynamic creation of source elements on <video>
424    // so we do it in one shot via innerHTML.
425    videoHTML = '<video id="' +  videoID + '" preload=auto autobuffer>';
426    for ( i = 0, srcLength = src.length; i < srcLength; i++ ) {
427      videoHTML += '<source src="' + src[ i ] + '">';
428    }
429    videoHTML += "</video>";
430    videoHTMLContainer.innerHTML = videoHTML;
431
432    if ( options && options.events && options.events.error ) {
433      node.addEventListener( "error", options.events.error, false );
434    }
435    return Popcorn( '#' + videoID, options );
436  };
437})( Popcorn );
438