|
1 (function(global, document) { |
|
2 |
|
3 // Popcorn.js does not support archaic browsers |
|
4 if ( !document.addEventListener ) { |
|
5 global.Popcorn = { |
|
6 isSupported: false |
|
7 }; |
|
8 |
|
9 var methods = ( "forEach extend effects error guid sizeOf isArray nop position disable enable destroy " + |
|
10 "addTrackEvent removeTrackEvent getTrackEvents getTrackEvent getLastTrackEventId " + |
|
11 "timeUpdate plugin removePlugin compose effect parser xhr getJSONP getScript" ).split(/\s+/); |
|
12 |
|
13 while( methods.length ) { |
|
14 global.Popcorn[ methods.shift() ] = function() {}; |
|
15 } |
|
16 return; |
|
17 } |
|
18 |
|
19 var |
|
20 |
|
21 AP = Array.prototype, |
|
22 OP = Object.prototype, |
|
23 |
|
24 forEach = AP.forEach, |
|
25 slice = AP.slice, |
|
26 hasOwn = OP.hasOwnProperty, |
|
27 toString = OP.toString, |
|
28 |
|
29 // Copy global Popcorn (may not exist) |
|
30 _Popcorn = global.Popcorn, |
|
31 |
|
32 // ID string matching |
|
33 rIdExp = /^(#([\w\-\_\.]+))$/, |
|
34 |
|
35 // Ready fn cache |
|
36 readyStack = [], |
|
37 readyBound = false, |
|
38 readyFired = false, |
|
39 |
|
40 // Non-public internal data object |
|
41 internal = { |
|
42 events: { |
|
43 hash: {}, |
|
44 apis: {} |
|
45 } |
|
46 }, |
|
47 |
|
48 // Non-public `requestAnimFrame` |
|
49 // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ |
|
50 requestAnimFrame = (function(){ |
|
51 return global.requestAnimationFrame || |
|
52 global.webkitRequestAnimationFrame || |
|
53 global.mozRequestAnimationFrame || |
|
54 global.oRequestAnimationFrame || |
|
55 global.msRequestAnimationFrame || |
|
56 function( callback, element ) { |
|
57 global.setTimeout( callback, 16 ); |
|
58 }; |
|
59 }()), |
|
60 |
|
61 // Declare constructor |
|
62 // Returns an instance object. |
|
63 Popcorn = function( entity, options ) { |
|
64 // Return new Popcorn object |
|
65 return new Popcorn.p.init( entity, options || null ); |
|
66 }; |
|
67 |
|
68 // Popcorn API version, automatically inserted via build system. |
|
69 Popcorn.version = "@VERSION"; |
|
70 |
|
71 // Boolean flag allowing a client to determine if Popcorn can be supported |
|
72 Popcorn.isSupported = true; |
|
73 |
|
74 // Instance caching |
|
75 Popcorn.instances = []; |
|
76 |
|
77 // Declare a shortcut (Popcorn.p) to and a definition of |
|
78 // the new prototype for our Popcorn constructor |
|
79 Popcorn.p = Popcorn.prototype = { |
|
80 |
|
81 init: function( entity, options ) { |
|
82 |
|
83 var matches; |
|
84 |
|
85 // Supports Popcorn(function () { /../ }) |
|
86 // Originally proposed by Daniel Brooks |
|
87 |
|
88 if ( typeof entity === "function" ) { |
|
89 |
|
90 // If document ready has already fired |
|
91 if ( document.readyState === "interactive" || document.readyState === "complete" ) { |
|
92 |
|
93 entity( document, Popcorn ); |
|
94 |
|
95 return; |
|
96 } |
|
97 // Add `entity` fn to ready stack |
|
98 readyStack.push( entity ); |
|
99 |
|
100 // This process should happen once per page load |
|
101 if ( !readyBound ) { |
|
102 |
|
103 // set readyBound flag |
|
104 readyBound = true; |
|
105 |
|
106 var DOMContentLoaded = function() { |
|
107 |
|
108 readyFired = true; |
|
109 |
|
110 // Remove global DOM ready listener |
|
111 document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); |
|
112 |
|
113 // Execute all ready function in the stack |
|
114 for ( var i = 0, readyStackLength = readyStack.length; i < readyStackLength; i++ ) { |
|
115 |
|
116 readyStack[ i ].call( document, Popcorn ); |
|
117 |
|
118 } |
|
119 // GC readyStack |
|
120 readyStack = null; |
|
121 }; |
|
122 |
|
123 // Register global DOM ready listener |
|
124 document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); |
|
125 } |
|
126 |
|
127 return; |
|
128 } |
|
129 |
|
130 // Check if entity is a valid string id |
|
131 matches = rIdExp.exec( entity ); |
|
132 |
|
133 // Get media element by id or object reference |
|
134 this.media = matches && matches.length && matches[ 2 ] ? |
|
135 document.getElementById( matches[ 2 ] ) : |
|
136 entity; |
|
137 |
|
138 // Create an audio or video element property reference |
|
139 this[ ( this.media.nodeName && this.media.nodeName.toLowerCase() ) || "video" ] = this.media; |
|
140 |
|
141 // Register new instance |
|
142 Popcorn.instances.push( this ); |
|
143 |
|
144 this.options = options || {}; |
|
145 |
|
146 this.isDestroyed = false; |
|
147 |
|
148 this.data = { |
|
149 |
|
150 // Allows disabling a plugin per instance |
|
151 disabled: [], |
|
152 |
|
153 // Stores DOM event queues by type |
|
154 events: {}, |
|
155 |
|
156 // Stores Special event hooks data |
|
157 hooks: {}, |
|
158 |
|
159 // Store track event history data |
|
160 history: [], |
|
161 |
|
162 // Stores ad-hoc state related data] |
|
163 state: { |
|
164 volume: this.media.volume |
|
165 }, |
|
166 |
|
167 // Store track event object references by trackId |
|
168 trackRefs: {}, |
|
169 |
|
170 // Playback track event queues |
|
171 trackEvents: { |
|
172 byStart: [{ |
|
173 |
|
174 start: -1, |
|
175 end: -1 |
|
176 }], |
|
177 byEnd: [{ |
|
178 start: -1, |
|
179 end: -1 |
|
180 }], |
|
181 animating: [], |
|
182 startIndex: 0, |
|
183 endIndex: 0, |
|
184 previousUpdateTime: -1 |
|
185 } |
|
186 }; |
|
187 |
|
188 // Wrap true ready check |
|
189 var isReady = function( that ) { |
|
190 |
|
191 var duration, videoDurationPlus, animate; |
|
192 |
|
193 if ( that.media.readyState >= 2 ) { |
|
194 // Adding padding to the front and end of the arrays |
|
195 // this is so we do not fall off either end |
|
196 |
|
197 duration = that.media.duration; |
|
198 // Check for no duration info (NaN) |
|
199 videoDurationPlus = duration != duration ? Number.MAX_VALUE : duration + 1; |
|
200 |
|
201 Popcorn.addTrackEvent( that, { |
|
202 start: videoDurationPlus, |
|
203 end: videoDurationPlus |
|
204 }); |
|
205 |
|
206 if ( that.options.frameAnimation ) { |
|
207 // if Popcorn is created with frameAnimation option set to true, |
|
208 // requestAnimFrame is used instead of "timeupdate" media event. |
|
209 // This is for greater frame time accuracy, theoretically up to |
|
210 // 60 frames per second as opposed to ~4 ( ~every 15-250ms) |
|
211 animate = function () { |
|
212 |
|
213 Popcorn.timeUpdate( that, {} ); |
|
214 |
|
215 that.trigger( "timeupdate" ); |
|
216 |
|
217 requestAnimFrame( animate ); |
|
218 }; |
|
219 |
|
220 requestAnimFrame( animate ); |
|
221 |
|
222 } else { |
|
223 |
|
224 that.data.timeUpdateFunction = function( event ) { |
|
225 Popcorn.timeUpdate( that, event ); |
|
226 }; |
|
227 |
|
228 if ( !that.isDestroyed ) { |
|
229 that.media.addEventListener( "timeupdate", that.data.timeUpdateFunction, false ); |
|
230 } |
|
231 } |
|
232 } else { |
|
233 global.setTimeout(function() { |
|
234 isReady( that ); |
|
235 }, 1 ); |
|
236 } |
|
237 }; |
|
238 |
|
239 isReady( this ); |
|
240 |
|
241 return this; |
|
242 } |
|
243 }; |
|
244 |
|
245 // Extend constructor prototype to instance prototype |
|
246 // Allows chaining methods to instances |
|
247 Popcorn.p.init.prototype = Popcorn.p; |
|
248 |
|
249 Popcorn.forEach = function( obj, fn, context ) { |
|
250 |
|
251 if ( !obj || !fn ) { |
|
252 return {}; |
|
253 } |
|
254 |
|
255 context = context || this; |
|
256 |
|
257 var key, len; |
|
258 |
|
259 // Use native whenever possible |
|
260 if ( forEach && obj.forEach === forEach ) { |
|
261 return obj.forEach( fn, context ); |
|
262 } |
|
263 |
|
264 if ( toString.call( obj ) === "[object NodeList]" ) { |
|
265 for ( key = 0, len = obj.length; key < len; key++ ) { |
|
266 fn.call( context, obj[ key ], key, obj ); |
|
267 } |
|
268 return obj; |
|
269 } |
|
270 |
|
271 for ( key in obj ) { |
|
272 if ( hasOwn.call( obj, key ) ) { |
|
273 fn.call( context, obj[ key ], key, obj ); |
|
274 } |
|
275 } |
|
276 return obj; |
|
277 }; |
|
278 |
|
279 Popcorn.extend = function( obj ) { |
|
280 var dest = obj, src = slice.call( arguments, 1 ); |
|
281 |
|
282 Popcorn.forEach( src, function( copy ) { |
|
283 for ( var prop in copy ) { |
|
284 dest[ prop ] = copy[ prop ]; |
|
285 } |
|
286 }); |
|
287 |
|
288 return dest; |
|
289 }; |
|
290 |
|
291 |
|
292 // A Few reusable utils, memoized onto Popcorn |
|
293 Popcorn.extend( Popcorn, { |
|
294 noConflict: function( deep ) { |
|
295 |
|
296 if ( deep ) { |
|
297 global.Popcorn = _Popcorn; |
|
298 } |
|
299 |
|
300 return Popcorn; |
|
301 }, |
|
302 error: function( msg ) { |
|
303 throw new Error( msg ); |
|
304 }, |
|
305 guid: function( prefix ) { |
|
306 Popcorn.guid.counter++; |
|
307 return ( prefix ? prefix : "" ) + ( +new Date() + Popcorn.guid.counter ); |
|
308 }, |
|
309 sizeOf: function( obj ) { |
|
310 var size = 0; |
|
311 |
|
312 for ( var prop in obj ) { |
|
313 size++; |
|
314 } |
|
315 |
|
316 return size; |
|
317 }, |
|
318 isArray: Array.isArray || function( array ) { |
|
319 return toString.call( array ) === "[object Array]"; |
|
320 }, |
|
321 |
|
322 nop: function() {}, |
|
323 |
|
324 position: function( elem ) { |
|
325 |
|
326 var clientRect = elem.getBoundingClientRect(), |
|
327 bounds = {}, |
|
328 doc = elem.ownerDocument, |
|
329 docElem = document.documentElement, |
|
330 body = document.body, |
|
331 clientTop, clientLeft, scrollTop, scrollLeft, top, left; |
|
332 |
|
333 // Determine correct clientTop/Left |
|
334 clientTop = docElem.clientTop || body.clientTop || 0; |
|
335 clientLeft = docElem.clientLeft || body.clientLeft || 0; |
|
336 |
|
337 // Determine correct scrollTop/Left |
|
338 scrollTop = ( global.pageYOffset && docElem.scrollTop || body.scrollTop ); |
|
339 scrollLeft = ( global.pageXOffset && docElem.scrollLeft || body.scrollLeft ); |
|
340 |
|
341 // Temp top/left |
|
342 top = Math.ceil( clientRect.top + scrollTop - clientTop ); |
|
343 left = Math.ceil( clientRect.left + scrollLeft - clientLeft ); |
|
344 |
|
345 for ( var p in clientRect ) { |
|
346 bounds[ p ] = Math.round( clientRect[ p ] ); |
|
347 } |
|
348 |
|
349 return Popcorn.extend({}, bounds, { top: top, left: left }); |
|
350 }, |
|
351 |
|
352 disable: function( instance, plugin ) { |
|
353 |
|
354 var disabled = instance.data.disabled; |
|
355 |
|
356 if ( disabled.indexOf( plugin ) === -1 ) { |
|
357 disabled.push( plugin ); |
|
358 } |
|
359 |
|
360 return instance; |
|
361 }, |
|
362 enable: function( instance, plugin ) { |
|
363 |
|
364 var disabled = instance.data.disabled, |
|
365 index = disabled.indexOf( plugin ); |
|
366 |
|
367 if ( index > -1 ) { |
|
368 disabled.splice( index, 1 ); |
|
369 } |
|
370 |
|
371 return instance; |
|
372 }, |
|
373 destroy: function( instance ) { |
|
374 var events = instance.data.events, |
|
375 singleEvent, item, fn; |
|
376 |
|
377 // Iterate through all events and remove them |
|
378 for ( item in events ) { |
|
379 singleEvent = events[ item ]; |
|
380 for ( fn in singleEvent ) { |
|
381 delete singleEvent[ fn ]; |
|
382 } |
|
383 events[ item ] = null; |
|
384 } |
|
385 |
|
386 if ( !instance.isDestroyed ) { |
|
387 instance.media.removeEventListener( "timeupdate", instance.data.timeUpdateFunction, false ); |
|
388 instance.isDestroyed = true; |
|
389 } |
|
390 } |
|
391 }); |
|
392 |
|
393 // Memoized GUID Counter |
|
394 Popcorn.guid.counter = 1; |
|
395 |
|
396 // Factory to implement getters, setters and controllers |
|
397 // as Popcorn instance methods. The IIFE will create and return |
|
398 // an object with defined methods |
|
399 Popcorn.extend(Popcorn.p, (function() { |
|
400 |
|
401 var methods = "load play pause currentTime playbackRate volume duration preload playbackRate " + |
|
402 "autoplay loop controls muted buffered readyState seeking paused played seekable ended", |
|
403 ret = {}; |
|
404 |
|
405 |
|
406 // Build methods, store in object that is returned and passed to extend |
|
407 Popcorn.forEach( methods.split( /\s+/g ), function( name ) { |
|
408 |
|
409 ret[ name ] = function( arg ) { |
|
410 |
|
411 if ( typeof this.media[ name ] === "function" ) { |
|
412 |
|
413 // Support for shorthanded play(n)/pause(n) jump to currentTime |
|
414 // If arg is not null or undefined and called by one of the |
|
415 // allowed shorthandable methods, then set the currentTime |
|
416 // Supports time as seconds or SMPTE |
|
417 if ( arg != null && /play|pause/.test( name ) ) { |
|
418 this.media.currentTime = Popcorn.util.toSeconds( arg ); |
|
419 } |
|
420 |
|
421 this.media[ name ](); |
|
422 |
|
423 return this; |
|
424 } |
|
425 |
|
426 |
|
427 if ( arg != null ) { |
|
428 |
|
429 this.media[ name ] = arg; |
|
430 |
|
431 return this; |
|
432 } |
|
433 |
|
434 return this.media[ name ]; |
|
435 }; |
|
436 }); |
|
437 |
|
438 return ret; |
|
439 |
|
440 })() |
|
441 ); |
|
442 |
|
443 Popcorn.forEach( "enable disable".split(" "), function( method ) { |
|
444 Popcorn.p[ method ] = function( plugin ) { |
|
445 return Popcorn[ method ]( this, plugin ); |
|
446 }; |
|
447 }); |
|
448 |
|
449 Popcorn.extend(Popcorn.p, { |
|
450 |
|
451 // Rounded currentTime |
|
452 roundTime: function() { |
|
453 return -~this.media.currentTime; |
|
454 }, |
|
455 |
|
456 // Attach an event to a single point in time |
|
457 exec: function( time, fn ) { |
|
458 |
|
459 // Creating a one second track event with an empty end |
|
460 Popcorn.addTrackEvent( this, { |
|
461 start: time, |
|
462 end: time + 1, |
|
463 _running: false, |
|
464 _natives: { |
|
465 start: fn || Popcorn.nop, |
|
466 end: Popcorn.nop, |
|
467 type: "exec" |
|
468 } |
|
469 }); |
|
470 |
|
471 return this; |
|
472 }, |
|
473 |
|
474 // Mute the calling media, optionally toggle |
|
475 mute: function( toggle ) { |
|
476 |
|
477 var event = toggle == null || toggle === true ? "muted" : "unmuted"; |
|
478 |
|
479 // If `toggle` is explicitly `false`, |
|
480 // unmute the media and restore the volume level |
|
481 if ( event === "unmuted" ) { |
|
482 this.media.muted = false; |
|
483 this.media.volume = this.data.state.volume; |
|
484 } |
|
485 |
|
486 // If `toggle` is either null or undefined, |
|
487 // save the current volume and mute the media element |
|
488 if ( event === "muted" ) { |
|
489 this.data.state.volume = this.media.volume; |
|
490 this.media.muted = true; |
|
491 } |
|
492 |
|
493 // Trigger either muted|unmuted event |
|
494 this.trigger( event ); |
|
495 |
|
496 return this; |
|
497 }, |
|
498 |
|
499 // Convenience method, unmute the calling media |
|
500 unmute: function( toggle ) { |
|
501 |
|
502 return this.mute( toggle == null ? false : !toggle ); |
|
503 }, |
|
504 |
|
505 // Get the client bounding box of an instance element |
|
506 position: function() { |
|
507 return Popcorn.position( this.media ); |
|
508 }, |
|
509 |
|
510 // Toggle a plugin's playback behaviour (on or off) per instance |
|
511 toggle: function( plugin ) { |
|
512 return Popcorn[ this.data.disabled.indexOf( plugin ) > -1 ? "enable" : "disable" ]( this, plugin ); |
|
513 }, |
|
514 |
|
515 // Set default values for plugin options objects per instance |
|
516 defaults: function( plugin, defaults ) { |
|
517 |
|
518 // If an array of default configurations is provided, |
|
519 // iterate and apply each to this instance |
|
520 if ( Popcorn.isArray( plugin ) ) { |
|
521 |
|
522 Popcorn.forEach( plugin, function( obj ) { |
|
523 for ( var name in obj ) { |
|
524 this.defaults( name, obj[ name ] ); |
|
525 } |
|
526 }, this ); |
|
527 |
|
528 return this; |
|
529 } |
|
530 |
|
531 if ( !this.options.defaults ) { |
|
532 this.options.defaults = {}; |
|
533 } |
|
534 |
|
535 if ( !this.options.defaults[ plugin ] ) { |
|
536 this.options.defaults[ plugin ] = {}; |
|
537 } |
|
538 |
|
539 Popcorn.extend( this.options.defaults[ plugin ], defaults ); |
|
540 |
|
541 return this; |
|
542 } |
|
543 }); |
|
544 |
|
545 Popcorn.Events = { |
|
546 UIEvents: "blur focus focusin focusout load resize scroll unload", |
|
547 MouseEvents: "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave click dblclick", |
|
548 Events: "loadstart progress suspend emptied stalled play pause " + |
|
549 "loadedmetadata loadeddata waiting playing canplay canplaythrough " + |
|
550 "seeking seeked timeupdate ended ratechange durationchange volumechange" |
|
551 }; |
|
552 |
|
553 Popcorn.Events.Natives = Popcorn.Events.UIEvents + " " + |
|
554 Popcorn.Events.MouseEvents + " " + |
|
555 Popcorn.Events.Events; |
|
556 |
|
557 internal.events.apiTypes = [ "UIEvents", "MouseEvents", "Events" ]; |
|
558 |
|
559 // Privately compile events table at load time |
|
560 (function( events, data ) { |
|
561 |
|
562 var apis = internal.events.apiTypes, |
|
563 eventsList = events.Natives.split( /\s+/g ), |
|
564 idx = 0, len = eventsList.length, prop; |
|
565 |
|
566 for( ; idx < len; idx++ ) { |
|
567 data.hash[ eventsList[idx] ] = true; |
|
568 } |
|
569 |
|
570 apis.forEach(function( val, idx ) { |
|
571 |
|
572 data.apis[ val ] = {}; |
|
573 |
|
574 var apiEvents = events[ val ].split( /\s+/g ), |
|
575 len = apiEvents.length, |
|
576 k = 0; |
|
577 |
|
578 for ( ; k < len; k++ ) { |
|
579 data.apis[ val ][ apiEvents[ k ] ] = true; |
|
580 } |
|
581 }); |
|
582 })( Popcorn.Events, internal.events ); |
|
583 |
|
584 Popcorn.events = { |
|
585 |
|
586 isNative: function( type ) { |
|
587 return !!internal.events.hash[ type ]; |
|
588 }, |
|
589 getInterface: function( type ) { |
|
590 |
|
591 if ( !Popcorn.events.isNative( type ) ) { |
|
592 return false; |
|
593 } |
|
594 |
|
595 var eventApi = internal.events, |
|
596 apis = eventApi.apiTypes, |
|
597 apihash = eventApi.apis, |
|
598 idx = 0, len = apis.length, api, tmp; |
|
599 |
|
600 for ( ; idx < len; idx++ ) { |
|
601 tmp = apis[ idx ]; |
|
602 |
|
603 if ( apihash[ tmp ][ type ] ) { |
|
604 api = tmp; |
|
605 break; |
|
606 } |
|
607 } |
|
608 return api; |
|
609 }, |
|
610 // Compile all native events to single array |
|
611 all: Popcorn.Events.Natives.split( /\s+/g ), |
|
612 // Defines all Event handling static functions |
|
613 fn: { |
|
614 trigger: function( type, data ) { |
|
615 |
|
616 var eventInterface, evt; |
|
617 // setup checks for custom event system |
|
618 if ( this.data.events[ type ] && Popcorn.sizeOf( this.data.events[ type ] ) ) { |
|
619 |
|
620 eventInterface = Popcorn.events.getInterface( type ); |
|
621 |
|
622 if ( eventInterface ) { |
|
623 |
|
624 evt = document.createEvent( eventInterface ); |
|
625 evt.initEvent( type, true, true, global, 1 ); |
|
626 |
|
627 this.media.dispatchEvent( evt ); |
|
628 |
|
629 return this; |
|
630 } |
|
631 |
|
632 // Custom events |
|
633 Popcorn.forEach( this.data.events[ type ], function( obj, key ) { |
|
634 |
|
635 obj.call( this, data ); |
|
636 |
|
637 }, this ); |
|
638 |
|
639 } |
|
640 |
|
641 return this; |
|
642 }, |
|
643 listen: function( type, fn ) { |
|
644 |
|
645 var self = this, |
|
646 hasEvents = true, |
|
647 eventHook = Popcorn.events.hooks[ type ], |
|
648 origType = type, |
|
649 tmp; |
|
650 |
|
651 if ( !this.data.events[ type ] ) { |
|
652 this.data.events[ type ] = {}; |
|
653 hasEvents = false; |
|
654 } |
|
655 |
|
656 // Check and setup event hooks |
|
657 if ( eventHook ) { |
|
658 |
|
659 // Execute hook add method if defined |
|
660 if ( eventHook.add ) { |
|
661 eventHook.add.call( this, {}, fn ); |
|
662 } |
|
663 |
|
664 // Reassign event type to our piggyback event type if defined |
|
665 if ( eventHook.bind ) { |
|
666 type = eventHook.bind; |
|
667 } |
|
668 |
|
669 // Reassign handler if defined |
|
670 if ( eventHook.handler ) { |
|
671 tmp = fn; |
|
672 |
|
673 fn = function wrapper( event ) { |
|
674 eventHook.handler.call( self, event, tmp ); |
|
675 }; |
|
676 } |
|
677 |
|
678 // assume the piggy back event is registered |
|
679 hasEvents = true; |
|
680 |
|
681 // Setup event registry entry |
|
682 if ( !this.data.events[ type ] ) { |
|
683 this.data.events[ type ] = {}; |
|
684 // Toggle if the previous assumption was untrue |
|
685 hasEvents = false; |
|
686 } |
|
687 } |
|
688 |
|
689 // Register event and handler |
|
690 this.data.events[ type ][ fn.name || ( fn.toString() + Popcorn.guid() ) ] = fn; |
|
691 |
|
692 // only attach one event of any type |
|
693 if ( !hasEvents && Popcorn.events.all.indexOf( type ) > -1 ) { |
|
694 |
|
695 this.media.addEventListener( type, function( event ) { |
|
696 |
|
697 Popcorn.forEach( self.data.events[ type ], function( obj, key ) { |
|
698 if ( typeof obj === "function" ) { |
|
699 obj.call( self, event ); |
|
700 } |
|
701 }); |
|
702 |
|
703 }, false); |
|
704 } |
|
705 return this; |
|
706 }, |
|
707 unlisten: function( type, fn ) { |
|
708 |
|
709 if ( this.data.events[ type ] && this.data.events[ type ][ fn ] ) { |
|
710 |
|
711 delete this.data.events[ type ][ fn ]; |
|
712 |
|
713 return this; |
|
714 } |
|
715 |
|
716 this.data.events[ type ] = null; |
|
717 |
|
718 return this; |
|
719 } |
|
720 }, |
|
721 hooks: { |
|
722 canplayall: { |
|
723 bind: "canplaythrough", |
|
724 add: function( event, callback ) { |
|
725 |
|
726 var state = false; |
|
727 |
|
728 if ( this.media.readyState ) { |
|
729 |
|
730 callback.call( this, event ); |
|
731 |
|
732 state = true; |
|
733 } |
|
734 |
|
735 this.data.hooks.canplayall = { |
|
736 fired: state |
|
737 }; |
|
738 }, |
|
739 // declare special handling instructions |
|
740 handler: function canplayall( event, callback ) { |
|
741 |
|
742 if ( !this.data.hooks.canplayall.fired ) { |
|
743 // trigger original user callback once |
|
744 callback.call( this, event ); |
|
745 |
|
746 this.data.hooks.canplayall.fired = true; |
|
747 } |
|
748 } |
|
749 } |
|
750 } |
|
751 }; |
|
752 |
|
753 // Extend Popcorn.events.fns (listen, unlisten, trigger) to all Popcorn instances |
|
754 Popcorn.forEach( [ "trigger", "listen", "unlisten" ], function( key ) { |
|
755 Popcorn.p[ key ] = Popcorn.events.fn[ key ]; |
|
756 }); |
|
757 |
|
758 // Internal Only - Adds track events to the instance object |
|
759 Popcorn.addTrackEvent = function( obj, track ) { |
|
760 |
|
761 // Determine if this track has default options set for it |
|
762 // If so, apply them to the track object |
|
763 if ( track && track._natives && track._natives.type && |
|
764 ( obj.options.defaults && obj.options.defaults[ track._natives.type ] ) ) { |
|
765 |
|
766 track = Popcorn.extend( {}, obj.options.defaults[ track._natives.type ], track ); |
|
767 } |
|
768 |
|
769 if ( track._natives ) { |
|
770 // Supports user defined track event id |
|
771 track._id = !track.id ? Popcorn.guid( track._natives.type ) : track.id; |
|
772 |
|
773 // Push track event ids into the history |
|
774 obj.data.history.push( track._id ); |
|
775 } |
|
776 |
|
777 track.start = Popcorn.util.toSeconds( track.start, obj.options.framerate ); |
|
778 track.end = Popcorn.util.toSeconds( track.end, obj.options.framerate ); |
|
779 |
|
780 // Store this definition in an array sorted by times |
|
781 var byStart = obj.data.trackEvents.byStart, |
|
782 byEnd = obj.data.trackEvents.byEnd, |
|
783 idx; |
|
784 |
|
785 for ( idx = byStart.length - 1; idx >= 0; idx-- ) { |
|
786 |
|
787 if ( track.start >= byStart[ idx ].start ) { |
|
788 byStart.splice( idx + 1, 0, track ); |
|
789 break; |
|
790 } |
|
791 } |
|
792 |
|
793 for ( idx = byEnd.length - 1; idx >= 0; idx-- ) { |
|
794 |
|
795 if ( track.end > byEnd[ idx ].end ) { |
|
796 byEnd.splice( idx + 1, 0, track ); |
|
797 break; |
|
798 } |
|
799 } |
|
800 |
|
801 this.timeUpdate( obj, null ); |
|
802 |
|
803 // Store references to user added trackevents in ref table |
|
804 if ( track._id ) { |
|
805 Popcorn.addTrackEvent.ref( obj, track ); |
|
806 } |
|
807 }; |
|
808 |
|
809 // Internal Only - Adds track event references to the instance object's trackRefs hash table |
|
810 Popcorn.addTrackEvent.ref = function( obj, track ) { |
|
811 obj.data.trackRefs[ track._id ] = track; |
|
812 |
|
813 return obj; |
|
814 }; |
|
815 |
|
816 Popcorn.removeTrackEvent = function( obj, trackId ) { |
|
817 |
|
818 var historyLen = obj.data.history.length, |
|
819 indexWasAt = 0, |
|
820 byStart = [], |
|
821 byEnd = [], |
|
822 animating = [], |
|
823 history = []; |
|
824 |
|
825 Popcorn.forEach( obj.data.trackEvents.byStart, function( o, i, context ) { |
|
826 // Preserve the original start/end trackEvents |
|
827 if ( !o._id ) { |
|
828 byStart.push( obj.data.trackEvents.byStart[i] ); |
|
829 byEnd.push( obj.data.trackEvents.byEnd[i] ); |
|
830 } |
|
831 |
|
832 // Filter for user track events (vs system track events) |
|
833 if ( o._id ) { |
|
834 |
|
835 // Filter for the trackevent to remove |
|
836 if ( o._id !== trackId ) { |
|
837 byStart.push( obj.data.trackEvents.byStart[i] ); |
|
838 byEnd.push( obj.data.trackEvents.byEnd[i] ); |
|
839 } |
|
840 |
|
841 // Capture the position of the track being removed. |
|
842 if ( o._id === trackId ) { |
|
843 indexWasAt = i; |
|
844 o._natives._teardown && o._natives._teardown.call( obj, o ); |
|
845 } |
|
846 } |
|
847 |
|
848 }); |
|
849 |
|
850 if ( obj.data.trackEvents.animating.length ) { |
|
851 Popcorn.forEach( obj.data.trackEvents.animating, function( o, i, context ) { |
|
852 // Preserve the original start/end trackEvents |
|
853 if ( !o._id ) { |
|
854 animating.push( obj.data.trackEvents.animating[i] ); |
|
855 } |
|
856 |
|
857 // Filter for user track events (vs system track events) |
|
858 if ( o._id ) { |
|
859 // Filter for the trackevent to remove |
|
860 if ( o._id !== trackId ) { |
|
861 animating.push( obj.data.trackEvents.animating[i] ); |
|
862 } |
|
863 } |
|
864 }); |
|
865 } |
|
866 |
|
867 // Update |
|
868 if ( indexWasAt <= obj.data.trackEvents.startIndex ) { |
|
869 obj.data.trackEvents.startIndex--; |
|
870 } |
|
871 |
|
872 if ( indexWasAt <= obj.data.trackEvents.endIndex ) { |
|
873 obj.data.trackEvents.endIndex--; |
|
874 } |
|
875 |
|
876 obj.data.trackEvents.byStart = byStart; |
|
877 obj.data.trackEvents.byEnd = byEnd; |
|
878 obj.data.trackEvents.animating = animating; |
|
879 |
|
880 for ( var i = 0; i < historyLen; i++ ) { |
|
881 if ( obj.data.history[ i ] !== trackId ) { |
|
882 history.push( obj.data.history[ i ] ); |
|
883 } |
|
884 } |
|
885 |
|
886 // Update ordered history array |
|
887 obj.data.history = history; |
|
888 |
|
889 // Update track event references |
|
890 Popcorn.removeTrackEvent.ref( obj, trackId ); |
|
891 }; |
|
892 |
|
893 // Internal Only - Removes track event references from instance object's trackRefs hash table |
|
894 Popcorn.removeTrackEvent.ref = function( obj, trackId ) { |
|
895 delete obj.data.trackRefs[ trackId ]; |
|
896 |
|
897 return obj; |
|
898 }; |
|
899 |
|
900 // Return an array of track events bound to this instance object |
|
901 Popcorn.getTrackEvents = function( obj ) { |
|
902 |
|
903 var trackevents = [], |
|
904 refs = obj.data.trackEvents.byStart, |
|
905 length = refs.length, |
|
906 idx = 0, |
|
907 ref; |
|
908 |
|
909 for ( ; idx < length; idx++ ) { |
|
910 ref = refs[ idx ]; |
|
911 // Return only user attributed track event references |
|
912 if ( ref._id ) { |
|
913 trackevents.push( ref ); |
|
914 } |
|
915 } |
|
916 |
|
917 return trackevents; |
|
918 }; |
|
919 |
|
920 // Internal Only - Returns an instance object's trackRefs hash table |
|
921 Popcorn.getTrackEvents.ref = function( obj ) { |
|
922 return obj.data.trackRefs; |
|
923 }; |
|
924 |
|
925 // Return a single track event bound to this instance object |
|
926 Popcorn.getTrackEvent = function( obj, trackId ) { |
|
927 return obj.data.trackRefs[ trackId ]; |
|
928 }; |
|
929 |
|
930 // Internal Only - Returns an instance object's track reference by track id |
|
931 Popcorn.getTrackEvent.ref = function( obj, trackId ) { |
|
932 return obj.data.trackRefs[ trackId ]; |
|
933 }; |
|
934 |
|
935 Popcorn.getLastTrackEventId = function( obj ) { |
|
936 return obj.data.history[ obj.data.history.length - 1 ]; |
|
937 }; |
|
938 |
|
939 Popcorn.timeUpdate = function( obj, event ) { |
|
940 |
|
941 var currentTime = obj.media.currentTime, |
|
942 previousTime = obj.data.trackEvents.previousUpdateTime, |
|
943 tracks = obj.data.trackEvents, |
|
944 animating = tracks.animating, |
|
945 end = tracks.endIndex, |
|
946 start = tracks.startIndex, |
|
947 animIndex = 0, |
|
948 |
|
949 registryByName = Popcorn.registryByName, |
|
950 |
|
951 byEnd, byStart, byAnimate, natives, type; |
|
952 |
|
953 // Playbar advancing |
|
954 if ( previousTime <= currentTime ) { |
|
955 |
|
956 while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end <= currentTime ) { |
|
957 |
|
958 byEnd = tracks.byEnd[ end ]; |
|
959 natives = byEnd._natives; |
|
960 type = natives && natives.type; |
|
961 |
|
962 // If plugin does not exist on this instance, remove it |
|
963 if ( !natives || |
|
964 ( !!registryByName[ type ] || |
|
965 !!obj[ type ] ) ) { |
|
966 |
|
967 if ( byEnd._running === true ) { |
|
968 byEnd._running = false; |
|
969 natives.end.call( obj, event, byEnd ); |
|
970 } |
|
971 |
|
972 end++; |
|
973 } else { |
|
974 // remove track event |
|
975 Popcorn.removeTrackEvent( obj, byEnd._id ); |
|
976 return; |
|
977 } |
|
978 } |
|
979 |
|
980 while ( tracks.byStart[ start ] && tracks.byStart[ start ].start <= currentTime ) { |
|
981 |
|
982 byStart = tracks.byStart[ start ]; |
|
983 natives = byStart._natives; |
|
984 type = natives && natives.type; |
|
985 |
|
986 // If plugin does not exist on this instance, remove it |
|
987 if ( !natives || |
|
988 ( !!registryByName[ type ] || |
|
989 !!obj[ type ] ) ) { |
|
990 |
|
991 if ( byStart.end > currentTime && |
|
992 byStart._running === false && |
|
993 obj.data.disabled.indexOf( type ) === -1 ) { |
|
994 |
|
995 byStart._running = true; |
|
996 natives.start.call( obj, event, byStart ); |
|
997 |
|
998 // If the `frameAnimation` option is used, |
|
999 // push the current byStart object into the `animating` cue |
|
1000 if ( obj.options.frameAnimation && |
|
1001 ( byStart && byStart._running && byStart._natives.frame ) ) { |
|
1002 |
|
1003 animating.push( byStart ); |
|
1004 } |
|
1005 } |
|
1006 start++; |
|
1007 } else { |
|
1008 // remove track event |
|
1009 Popcorn.removeTrackEvent( obj, byStart._id ); |
|
1010 return; |
|
1011 } |
|
1012 } |
|
1013 |
|
1014 // If the `frameAnimation` option is used, iterate the animating track |
|
1015 // and execute the `frame` callback |
|
1016 if ( obj.options.frameAnimation ) { |
|
1017 while ( animIndex < animating.length ) { |
|
1018 |
|
1019 byAnimate = animating[ animIndex ]; |
|
1020 |
|
1021 if ( !byAnimate._running ) { |
|
1022 animating.splice( animIndex, 1 ); |
|
1023 } else { |
|
1024 byAnimate._natives.frame.call( obj, event, byAnimate, currentTime ); |
|
1025 animIndex++; |
|
1026 } |
|
1027 } |
|
1028 } |
|
1029 |
|
1030 // Playbar receding |
|
1031 } else if ( previousTime > currentTime ) { |
|
1032 |
|
1033 while ( tracks.byStart[ start ] && tracks.byStart[ start ].start > currentTime ) { |
|
1034 |
|
1035 byStart = tracks.byStart[ start ]; |
|
1036 natives = byStart._natives; |
|
1037 type = natives && natives.type; |
|
1038 |
|
1039 // if plugin does not exist on this instance, remove it |
|
1040 if ( !natives || |
|
1041 ( !!registryByName[ type ] || |
|
1042 !!obj[ type ] ) ) { |
|
1043 |
|
1044 if ( byStart._running === true ) { |
|
1045 byStart._running = false; |
|
1046 natives.end.call( obj, event, byStart ); |
|
1047 } |
|
1048 start--; |
|
1049 } else { |
|
1050 // remove track event |
|
1051 Popcorn.removeTrackEvent( obj, byStart._id ); |
|
1052 return; |
|
1053 } |
|
1054 } |
|
1055 |
|
1056 while ( tracks.byEnd[ end ] && tracks.byEnd[ end ].end > currentTime ) { |
|
1057 |
|
1058 byEnd = tracks.byEnd[ end ]; |
|
1059 natives = byEnd._natives; |
|
1060 type = natives && natives.type; |
|
1061 |
|
1062 // if plugin does not exist on this instance, remove it |
|
1063 if ( !natives || |
|
1064 ( !!registryByName[ type ] || |
|
1065 !!obj[ type ] ) ) { |
|
1066 |
|
1067 if ( byEnd.start <= currentTime && |
|
1068 byEnd._running === false && |
|
1069 obj.data.disabled.indexOf( type ) === -1 ) { |
|
1070 |
|
1071 byEnd._running = true; |
|
1072 natives.start.call( obj, event, byEnd ); |
|
1073 |
|
1074 // If the `frameAnimation` option is used, |
|
1075 // push the current byEnd object into the `animating` cue |
|
1076 if ( obj.options.frameAnimation && |
|
1077 ( byEnd && byEnd._running && byEnd._natives.frame ) ) { |
|
1078 |
|
1079 animating.push( byEnd ); |
|
1080 } |
|
1081 } |
|
1082 end--; |
|
1083 } else { |
|
1084 // remove track event |
|
1085 Popcorn.removeTrackEvent( obj, byEnd._id ); |
|
1086 return; |
|
1087 } |
|
1088 } |
|
1089 |
|
1090 // If the `frameAnimation` option is used, iterate the animating track |
|
1091 // and execute the `frame` callback |
|
1092 if ( obj.options.frameAnimation ) { |
|
1093 while ( animIndex < animating.length ) { |
|
1094 |
|
1095 byAnimate = animating[ animIndex ]; |
|
1096 |
|
1097 if ( !byAnimate._running ) { |
|
1098 animating.splice( animIndex, 1 ); |
|
1099 } else { |
|
1100 byAnimate._natives.frame.call( obj, event, byAnimate, currentTime ); |
|
1101 animIndex++; |
|
1102 } |
|
1103 } |
|
1104 } |
|
1105 // time bar is not moving ( video is paused ) |
|
1106 } |
|
1107 |
|
1108 tracks.endIndex = end; |
|
1109 tracks.startIndex = start; |
|
1110 tracks.previousUpdateTime = currentTime; |
|
1111 }; |
|
1112 |
|
1113 // Map and Extend TrackEvent functions to all Popcorn instances |
|
1114 Popcorn.extend( Popcorn.p, { |
|
1115 |
|
1116 getTrackEvents: function() { |
|
1117 return Popcorn.getTrackEvents.call( null, this ); |
|
1118 }, |
|
1119 |
|
1120 getTrackEvent: function( id ) { |
|
1121 return Popcorn.getTrackEvent.call( null, this, id ); |
|
1122 }, |
|
1123 |
|
1124 getLastTrackEventId: function() { |
|
1125 return Popcorn.getLastTrackEventId.call( null, this ); |
|
1126 }, |
|
1127 |
|
1128 removeTrackEvent: function( id ) { |
|
1129 |
|
1130 Popcorn.removeTrackEvent.call( null, this, id ); |
|
1131 return this; |
|
1132 }, |
|
1133 |
|
1134 removePlugin: function( name ) { |
|
1135 Popcorn.removePlugin.call( null, this, name ); |
|
1136 return this; |
|
1137 }, |
|
1138 |
|
1139 timeUpdate: function( event ) { |
|
1140 Popcorn.timeUpdate.call( null, this, event ); |
|
1141 return this; |
|
1142 }, |
|
1143 |
|
1144 destroy: function() { |
|
1145 Popcorn.destroy.call( null, this ); |
|
1146 return this; |
|
1147 } |
|
1148 }); |
|
1149 |
|
1150 // Plugin manifests |
|
1151 Popcorn.manifest = {}; |
|
1152 // Plugins are registered |
|
1153 Popcorn.registry = []; |
|
1154 Popcorn.registryByName = {}; |
|
1155 // An interface for extending Popcorn |
|
1156 // with plugin functionality |
|
1157 Popcorn.plugin = function( name, definition, manifest ) { |
|
1158 |
|
1159 if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) { |
|
1160 Popcorn.error( "'" + name + "' is a protected function name" ); |
|
1161 return; |
|
1162 } |
|
1163 |
|
1164 // Provides some sugar, but ultimately extends |
|
1165 // the definition into Popcorn.p |
|
1166 var reserved = [ "start", "end" ], |
|
1167 plugin = {}, |
|
1168 setup, |
|
1169 isfn = typeof definition === "function", |
|
1170 methods = [ "_setup", "_teardown", "start", "end", "frame" ]; |
|
1171 |
|
1172 // combines calls of two function calls into one |
|
1173 var combineFn = function( first, second ) { |
|
1174 |
|
1175 first = first || Popcorn.nop; |
|
1176 second = second || Popcorn.nop; |
|
1177 |
|
1178 return function() { |
|
1179 first.apply( this, arguments ); |
|
1180 second.apply( this, arguments ); |
|
1181 }; |
|
1182 }; |
|
1183 |
|
1184 // If `manifest` arg is undefined, check for manifest within the `definition` object |
|
1185 // If no `definition.manifest`, an empty object is a sufficient fallback |
|
1186 Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {}; |
|
1187 |
|
1188 // apply safe, and empty default functions |
|
1189 methods.forEach(function( method ) { |
|
1190 definition[ method ] = safeTry( definition[ method ] || Popcorn.nop, name ); |
|
1191 }); |
|
1192 |
|
1193 var pluginFn = function( setup, options ) { |
|
1194 |
|
1195 if ( !options ) { |
|
1196 return this; |
|
1197 } |
|
1198 |
|
1199 // Storing the plugin natives |
|
1200 var natives = options._natives = {}, |
|
1201 compose = "", |
|
1202 defaults, originalOpts, manifestOpts, mergedSetupOpts; |
|
1203 |
|
1204 Popcorn.extend( natives, setup ); |
|
1205 |
|
1206 options._natives.type = name; |
|
1207 options._running = false; |
|
1208 |
|
1209 natives.start = natives.start || natives[ "in" ]; |
|
1210 natives.end = natives.end || natives[ "out" ]; |
|
1211 |
|
1212 // Check for previously set default options |
|
1213 defaults = this.options.defaults && this.options.defaults[ options._natives && options._natives.type ]; |
|
1214 |
|
1215 // default to an empty string if no effect exists |
|
1216 // split string into an array of effects |
|
1217 options.compose = options.compose && options.compose.split( " " ) || []; |
|
1218 options.effect = options.effect && options.effect.split( " " ) || []; |
|
1219 |
|
1220 // join the two arrays together |
|
1221 options.compose = options.compose.concat( options.effect ); |
|
1222 |
|
1223 options.compose.forEach(function( composeOption ) { |
|
1224 |
|
1225 // if the requested compose is garbage, throw it away |
|
1226 compose = Popcorn.compositions[ composeOption ] || {}; |
|
1227 |
|
1228 // extends previous functions with compose function |
|
1229 methods.forEach(function( method ) { |
|
1230 natives[ method ] = combineFn( natives[ method ], compose[ method ] ); |
|
1231 }); |
|
1232 }); |
|
1233 |
|
1234 // Ensure a manifest object, an empty object is a sufficient fallback |
|
1235 options._natives.manifest = manifest; |
|
1236 |
|
1237 // Checks for expected properties |
|
1238 if ( !( "start" in options ) ) { |
|
1239 options.start = options[ "in" ] || 0; |
|
1240 } |
|
1241 |
|
1242 if ( !( "end" in options ) ) { |
|
1243 options.end = options[ "out" ] || this.duration() || Number.MAX_VALUE; |
|
1244 } |
|
1245 |
|
1246 // Merge with defaults if they exist, make sure per call is prioritized |
|
1247 mergedSetupOpts = defaults ? Popcorn.extend( {}, defaults, options ) : |
|
1248 options; |
|
1249 |
|
1250 // Resolves 239, 241, 242 |
|
1251 if ( !mergedSetupOpts.target ) { |
|
1252 |
|
1253 // Sometimes the manifest may be missing entirely |
|
1254 // or it has an options object that doesn't have a `target` property |
|
1255 manifestOpts = "options" in manifest && manifest.options; |
|
1256 |
|
1257 mergedSetupOpts.target = manifestOpts && "target" in manifestOpts && manifestOpts.target; |
|
1258 } |
|
1259 |
|
1260 // Trigger _setup method if exists |
|
1261 options._natives._setup && options._natives._setup.call( this, mergedSetupOpts ); |
|
1262 |
|
1263 // Create new track event for this instance |
|
1264 Popcorn.addTrackEvent( this, Popcorn.extend( mergedSetupOpts, options ) ); |
|
1265 |
|
1266 // Future support for plugin event definitions |
|
1267 // for all of the native events |
|
1268 Popcorn.forEach( setup, function( callback, type ) { |
|
1269 |
|
1270 if ( type !== "type" ) { |
|
1271 |
|
1272 if ( reserved.indexOf( type ) === -1 ) { |
|
1273 |
|
1274 this.listen( type, callback ); |
|
1275 } |
|
1276 } |
|
1277 |
|
1278 }, this ); |
|
1279 |
|
1280 return this; |
|
1281 }; |
|
1282 |
|
1283 // Assign new named definition |
|
1284 plugin[ name ] = function( options ) { |
|
1285 return pluginFn.call( this, isfn ? definition.call( this, options ) : definition, |
|
1286 options ); |
|
1287 }; |
|
1288 |
|
1289 // Extend Popcorn.p with new named definition |
|
1290 Popcorn.extend( Popcorn.p, plugin ); |
|
1291 |
|
1292 // Push into the registry |
|
1293 var entry = { |
|
1294 fn: plugin[ name ], |
|
1295 definition: definition, |
|
1296 base: definition, |
|
1297 parents: [], |
|
1298 name: name |
|
1299 }; |
|
1300 Popcorn.registry.push( |
|
1301 Popcorn.extend( plugin, entry, { |
|
1302 type: name |
|
1303 }) |
|
1304 ); |
|
1305 Popcorn.registryByName[ name ] = entry; |
|
1306 |
|
1307 return plugin; |
|
1308 }; |
|
1309 |
|
1310 // Storage for plugin function errors |
|
1311 Popcorn.plugin.errors = []; |
|
1312 |
|
1313 // Returns wrapped plugin function |
|
1314 function safeTry( fn, pluginName ) { |
|
1315 return function() { |
|
1316 try { |
|
1317 return fn.apply( this, arguments ); |
|
1318 } catch ( ex ) { |
|
1319 // Push plugin function errors into logging queue |
|
1320 Popcorn.plugin.errors.push({ |
|
1321 plugin: pluginName, |
|
1322 thrown: ex, |
|
1323 source: fn.toString() |
|
1324 }); |
|
1325 |
|
1326 // Trigger an error that the instance can listen for |
|
1327 // and react to |
|
1328 this.trigger( "error", Popcorn.plugin.errors ); |
|
1329 } |
|
1330 }; |
|
1331 } |
|
1332 |
|
1333 // Debug-mode flag for plugin development |
|
1334 Popcorn.plugin.debug = false; |
|
1335 |
|
1336 // removePlugin( type ) removes all tracks of that from all instances of popcorn |
|
1337 // removePlugin( obj, type ) removes all tracks of type from obj, where obj is a single instance of popcorn |
|
1338 Popcorn.removePlugin = function( obj, name ) { |
|
1339 |
|
1340 // Check if we are removing plugin from an instance or from all of Popcorn |
|
1341 if ( !name ) { |
|
1342 |
|
1343 // Fix the order |
|
1344 name = obj; |
|
1345 obj = Popcorn.p; |
|
1346 |
|
1347 if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) { |
|
1348 Popcorn.error( "'" + name + "' is a protected function name" ); |
|
1349 return; |
|
1350 } |
|
1351 |
|
1352 var registryLen = Popcorn.registry.length, |
|
1353 registryIdx; |
|
1354 |
|
1355 // remove plugin reference from registry |
|
1356 for ( registryIdx = 0; registryIdx < registryLen; registryIdx++ ) { |
|
1357 if ( Popcorn.registry[ registryIdx ].name === name ) { |
|
1358 Popcorn.registry.splice( registryIdx, 1 ); |
|
1359 delete Popcorn.registryByName[ name ]; |
|
1360 |
|
1361 // delete the plugin |
|
1362 delete obj[ name ]; |
|
1363 |
|
1364 // plugin found and removed, stop checking, we are done |
|
1365 return; |
|
1366 } |
|
1367 } |
|
1368 |
|
1369 } |
|
1370 |
|
1371 var byStart = obj.data.trackEvents.byStart, |
|
1372 byEnd = obj.data.trackEvents.byEnd, |
|
1373 animating = obj.data.trackEvents.animating, |
|
1374 idx, sl; |
|
1375 |
|
1376 // remove all trackEvents |
|
1377 for ( idx = 0, sl = byStart.length; idx < sl; idx++ ) { |
|
1378 |
|
1379 if ( ( byStart[ idx ] && byStart[ idx ]._natives && byStart[ idx ]._natives.type === name ) && |
|
1380 ( byEnd[ idx ] && byEnd[ idx ]._natives && byEnd[ idx ]._natives.type === name ) ) { |
|
1381 |
|
1382 byStart[ idx ]._natives._teardown && byStart[ idx ]._natives._teardown.call( obj, byStart[ idx ] ); |
|
1383 |
|
1384 byStart.splice( idx, 1 ); |
|
1385 byEnd.splice( idx, 1 ); |
|
1386 |
|
1387 // update for loop if something removed, but keep checking |
|
1388 idx--; sl--; |
|
1389 if ( obj.data.trackEvents.startIndex <= idx ) { |
|
1390 obj.data.trackEvents.startIndex--; |
|
1391 obj.data.trackEvents.endIndex--; |
|
1392 } |
|
1393 } |
|
1394 } |
|
1395 |
|
1396 //remove all animating events |
|
1397 for ( idx = 0, sl = animating.length; idx < sl; idx++ ) { |
|
1398 |
|
1399 if ( animating[ idx ] && animating[ idx ]._natives && animating[ idx ]._natives.type === name ) { |
|
1400 |
|
1401 animating.splice( idx, 1 ); |
|
1402 |
|
1403 // update for loop if something removed, but keep checking |
|
1404 idx--; sl--; |
|
1405 } |
|
1406 } |
|
1407 |
|
1408 }; |
|
1409 |
|
1410 Popcorn.compositions = {}; |
|
1411 |
|
1412 // Plugin inheritance |
|
1413 Popcorn.compose = function( name, definition, manifest ) { |
|
1414 |
|
1415 // If `manifest` arg is undefined, check for manifest within the `definition` object |
|
1416 // If no `definition.manifest`, an empty object is a sufficient fallback |
|
1417 Popcorn.manifest[ name ] = manifest = manifest || definition.manifest || {}; |
|
1418 |
|
1419 // register the effect by name |
|
1420 Popcorn.compositions[ name ] = definition; |
|
1421 }; |
|
1422 |
|
1423 Popcorn.plugin.effect = Popcorn.effect = Popcorn.compose; |
|
1424 |
|
1425 // stores parsers keyed on filetype |
|
1426 Popcorn.parsers = {}; |
|
1427 |
|
1428 // An interface for extending Popcorn |
|
1429 // with parser functionality |
|
1430 Popcorn.parser = function( name, type, definition ) { |
|
1431 |
|
1432 if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) { |
|
1433 Popcorn.error( "'" + name + "' is a protected function name" ); |
|
1434 return; |
|
1435 } |
|
1436 |
|
1437 // fixes parameters for overloaded function call |
|
1438 if ( typeof type === "function" && !definition ) { |
|
1439 definition = type; |
|
1440 type = ""; |
|
1441 } |
|
1442 |
|
1443 if ( typeof definition !== "function" || typeof type !== "string" ) { |
|
1444 return; |
|
1445 } |
|
1446 |
|
1447 // Provides some sugar, but ultimately extends |
|
1448 // the definition into Popcorn.p |
|
1449 |
|
1450 var natives = Popcorn.events.all, |
|
1451 parseFn, |
|
1452 parser = {}; |
|
1453 |
|
1454 parseFn = function( filename, callback ) { |
|
1455 |
|
1456 if ( !filename ) { |
|
1457 return this; |
|
1458 } |
|
1459 |
|
1460 var that = this; |
|
1461 |
|
1462 Popcorn.xhr({ |
|
1463 url: filename, |
|
1464 dataType: type, |
|
1465 success: function( data ) { |
|
1466 |
|
1467 var tracksObject = definition( data ), |
|
1468 tracksData, |
|
1469 tracksDataLen, |
|
1470 tracksDef, |
|
1471 idx = 0; |
|
1472 |
|
1473 tracksData = tracksObject.data || []; |
|
1474 tracksDataLen = tracksData.length; |
|
1475 tracksDef = null; |
|
1476 |
|
1477 // If no tracks to process, return immediately |
|
1478 if ( !tracksDataLen ) { |
|
1479 return; |
|
1480 } |
|
1481 |
|
1482 // Create tracks out of parsed object |
|
1483 for ( ; idx < tracksDataLen; idx++ ) { |
|
1484 |
|
1485 tracksDef = tracksData[ idx ]; |
|
1486 |
|
1487 for ( var key in tracksDef ) { |
|
1488 |
|
1489 if ( hasOwn.call( tracksDef, key ) && !!that[ key ] ) { |
|
1490 |
|
1491 that[ key ]( tracksDef[ key ] ); |
|
1492 } |
|
1493 } |
|
1494 } |
|
1495 if ( callback ) { |
|
1496 callback(); |
|
1497 } |
|
1498 } |
|
1499 }); |
|
1500 |
|
1501 return this; |
|
1502 }; |
|
1503 |
|
1504 // Assign new named definition |
|
1505 parser[ name ] = parseFn; |
|
1506 |
|
1507 // Extend Popcorn.p with new named definition |
|
1508 Popcorn.extend( Popcorn.p, parser ); |
|
1509 |
|
1510 // keys the function name by filetype extension |
|
1511 //Popcorn.parsers[ name ] = true; |
|
1512 |
|
1513 return parser; |
|
1514 }; |
|
1515 |
|
1516 Popcorn.player = function( name, player ) { |
|
1517 |
|
1518 player = player || {}; |
|
1519 |
|
1520 var playerFn = function( target, src, options ) { |
|
1521 |
|
1522 options = options || {}; |
|
1523 |
|
1524 // List of events |
|
1525 var date = new Date() / 1000, |
|
1526 baselineTime = date, |
|
1527 currentTime = 0, |
|
1528 events = {}, |
|
1529 |
|
1530 // The container div of the resource |
|
1531 container = document.getElementById( rIdExp.exec( target ) && rIdExp.exec( target )[ 2 ] ) || |
|
1532 document.getElementById( target ) || |
|
1533 target, |
|
1534 basePlayer = {}, |
|
1535 timeout, |
|
1536 popcorn; |
|
1537 |
|
1538 // copies a div into the media object |
|
1539 for( var val in container ) { |
|
1540 |
|
1541 if ( typeof container[ val ] === "object" ) { |
|
1542 |
|
1543 basePlayer[ val ] = container[ val ]; |
|
1544 } else if ( typeof container[ val ] === "function" ) { |
|
1545 |
|
1546 basePlayer[ val ] = (function( value ) { |
|
1547 |
|
1548 return function() { |
|
1549 |
|
1550 return container[ value ].apply( container, arguments ); |
|
1551 }; |
|
1552 }( val )); |
|
1553 } else { |
|
1554 |
|
1555 Popcorn.player.defineProperty( basePlayer, val, { |
|
1556 get: (function( value ) { |
|
1557 |
|
1558 return function() { |
|
1559 |
|
1560 return container[ value ]; |
|
1561 }; |
|
1562 }( val )), |
|
1563 set: Popcorn.nop, |
|
1564 configurable: true |
|
1565 }); |
|
1566 } |
|
1567 } |
|
1568 |
|
1569 var timeupdate = function() { |
|
1570 |
|
1571 date = new Date() / 1000; |
|
1572 |
|
1573 if ( !basePlayer.paused ) { |
|
1574 |
|
1575 basePlayer.currentTime = basePlayer.currentTime + ( date - baselineTime ); |
|
1576 basePlayer.dispatchEvent( "timeupdate" ); |
|
1577 timeout = setTimeout( timeupdate, 10 ); |
|
1578 } |
|
1579 |
|
1580 baselineTime = date; |
|
1581 }; |
|
1582 |
|
1583 basePlayer.play = function() { |
|
1584 |
|
1585 this.paused = false; |
|
1586 |
|
1587 if ( basePlayer.readyState >= 4 ) { |
|
1588 |
|
1589 baselineTime = new Date() / 1000; |
|
1590 basePlayer.dispatchEvent( "play" ); |
|
1591 timeupdate(); |
|
1592 } |
|
1593 }; |
|
1594 |
|
1595 basePlayer.pause = function() { |
|
1596 |
|
1597 this.paused = true; |
|
1598 basePlayer.dispatchEvent( "pause" ); |
|
1599 }; |
|
1600 |
|
1601 Popcorn.player.defineProperty( basePlayer, "currentTime", { |
|
1602 get: function() { |
|
1603 |
|
1604 return currentTime; |
|
1605 }, |
|
1606 set: function( val ) { |
|
1607 |
|
1608 // make sure val is a number |
|
1609 currentTime = +val; |
|
1610 basePlayer.dispatchEvent( "timeupdate" ); |
|
1611 return currentTime; |
|
1612 }, |
|
1613 configurable: true |
|
1614 }); |
|
1615 |
|
1616 // Adds an event listener to the object |
|
1617 basePlayer.addEventListener = function( evtName, fn ) { |
|
1618 |
|
1619 if ( !events[ evtName ] ) { |
|
1620 |
|
1621 events[ evtName ] = []; |
|
1622 } |
|
1623 |
|
1624 events[ evtName ].push( fn ); |
|
1625 return fn; |
|
1626 }; |
|
1627 |
|
1628 // Can take event object or simple string |
|
1629 basePlayer.dispatchEvent = function( oEvent ) { |
|
1630 |
|
1631 var evt, |
|
1632 self = this, |
|
1633 eventInterface, |
|
1634 eventName = oEvent.type; |
|
1635 |
|
1636 // A string was passed, create event object |
|
1637 if ( !eventName ) { |
|
1638 |
|
1639 eventName = oEvent; |
|
1640 eventInterface = Popcorn.events.getInterface( eventName ); |
|
1641 |
|
1642 if ( eventInterface ) { |
|
1643 |
|
1644 evt = document.createEvent( eventInterface ); |
|
1645 evt.initEvent( eventName, true, true, window, 1 ); |
|
1646 } |
|
1647 } |
|
1648 |
|
1649 Popcorn.forEach( events[ eventName ], function( val ) { |
|
1650 |
|
1651 val.call( self, evt, self ); |
|
1652 }); |
|
1653 }; |
|
1654 |
|
1655 // Attempt to get src from playerFn parameter |
|
1656 basePlayer.src = src || ""; |
|
1657 basePlayer.readyState = 0; |
|
1658 basePlayer.duration = 0; |
|
1659 basePlayer.paused = true; |
|
1660 basePlayer.ended = 0; |
|
1661 |
|
1662 // basePlayer has no concept of sound |
|
1663 basePlayer.volume = 1; |
|
1664 basePlayer.muted = false; |
|
1665 |
|
1666 if ( player._setup ) { |
|
1667 |
|
1668 player._setup.call( basePlayer, options ); |
|
1669 } else { |
|
1670 |
|
1671 // there is no setup, which means there is nothing to load |
|
1672 basePlayer.readyState = 4; |
|
1673 basePlayer.dispatchEvent( 'load' ); |
|
1674 } |
|
1675 |
|
1676 popcorn = new Popcorn.p.init( basePlayer, options ); |
|
1677 |
|
1678 return popcorn; |
|
1679 }; |
|
1680 |
|
1681 Popcorn[ name ] = Popcorn[ name ] || playerFn; |
|
1682 }; |
|
1683 |
|
1684 Popcorn.player.defineProperty = Object.defineProperty || function( object, description, options ) { |
|
1685 |
|
1686 object.__defineGetter__( description, options.get || Popcorn.nop ); |
|
1687 object.__defineSetter__( description, options.set || Popcorn.nop ); |
|
1688 }; |
|
1689 |
|
1690 // Cache references to reused RegExps |
|
1691 var rparams = /\?/, |
|
1692 // XHR Setup object |
|
1693 setup = { |
|
1694 url: "", |
|
1695 data: "", |
|
1696 dataType: "", |
|
1697 success: Popcorn.nop, |
|
1698 type: "GET", |
|
1699 async: true, |
|
1700 xhr: function() { |
|
1701 return new global.XMLHttpRequest(); |
|
1702 } |
|
1703 }; |
|
1704 |
|
1705 Popcorn.xhr = function( options ) { |
|
1706 |
|
1707 options.dataType = options.dataType && options.dataType.toLowerCase() || null; |
|
1708 |
|
1709 if ( options.dataType && |
|
1710 ( options.dataType === "jsonp" || options.dataType === "script" ) ) { |
|
1711 |
|
1712 Popcorn.xhr.getJSONP( |
|
1713 options.url, |
|
1714 options.success, |
|
1715 options.dataType === "script" |
|
1716 ); |
|
1717 return; |
|
1718 } |
|
1719 |
|
1720 var settings = Popcorn.extend( {}, setup, options ); |
|
1721 |
|
1722 // Create new XMLHttpRequest object |
|
1723 settings.ajax = settings.xhr(); |
|
1724 |
|
1725 if ( settings.ajax ) { |
|
1726 |
|
1727 if ( settings.type === "GET" && settings.data ) { |
|
1728 |
|
1729 // append query string |
|
1730 settings.url += ( rparams.test( settings.url ) ? "&" : "?" ) + settings.data; |
|
1731 |
|
1732 // Garbage collect and reset settings.data |
|
1733 settings.data = null; |
|
1734 } |
|
1735 |
|
1736 |
|
1737 settings.ajax.open( settings.type, settings.url, settings.async ); |
|
1738 settings.ajax.send( settings.data || null ); |
|
1739 |
|
1740 return Popcorn.xhr.httpData( settings ); |
|
1741 } |
|
1742 }; |
|
1743 |
|
1744 |
|
1745 Popcorn.xhr.httpData = function( settings ) { |
|
1746 |
|
1747 var data, json = null; |
|
1748 |
|
1749 settings.ajax.onreadystatechange = function() { |
|
1750 |
|
1751 if ( settings.ajax.readyState === 4 ) { |
|
1752 |
|
1753 try { |
|
1754 json = JSON.parse( settings.ajax.responseText ); |
|
1755 } catch( e ) { |
|
1756 //suppress |
|
1757 } |
|
1758 |
|
1759 data = { |
|
1760 xml: settings.ajax.responseXML, |
|
1761 text: settings.ajax.responseText, |
|
1762 json: json |
|
1763 }; |
|
1764 |
|
1765 // If a dataType was specified, return that type of data |
|
1766 if ( settings.dataType ) { |
|
1767 data = data[ settings.dataType ]; |
|
1768 } |
|
1769 |
|
1770 |
|
1771 settings.success.call( settings.ajax, data ); |
|
1772 |
|
1773 } |
|
1774 }; |
|
1775 return data; |
|
1776 }; |
|
1777 |
|
1778 Popcorn.xhr.getJSONP = function( url, success, isScript ) { |
|
1779 |
|
1780 var head = document.head || document.getElementsByTagName( "head" )[ 0 ] || document.documentElement, |
|
1781 script = document.createElement( "script" ), |
|
1782 paramStr = url.split( "?" )[ 1 ], |
|
1783 isFired = false, |
|
1784 params = [], |
|
1785 callback, parts, callparam; |
|
1786 |
|
1787 if ( paramStr && !isScript ) { |
|
1788 params = paramStr.split( "&" ); |
|
1789 } |
|
1790 |
|
1791 if ( params.length ) { |
|
1792 parts = params[ params.length - 1 ].split( "=" ); |
|
1793 } |
|
1794 |
|
1795 callback = params.length ? ( parts[ 1 ] ? parts[ 1 ] : parts[ 0 ] ) : "jsonp"; |
|
1796 |
|
1797 if ( !paramStr && !isScript ) { |
|
1798 url += "?callback=" + callback; |
|
1799 } |
|
1800 |
|
1801 if ( callback && !isScript ) { |
|
1802 |
|
1803 // If a callback name already exists |
|
1804 if ( !!window[ callback ] ) { |
|
1805 // Create a new unique callback name |
|
1806 callback = Popcorn.guid( callback ); |
|
1807 } |
|
1808 |
|
1809 // Define the JSONP success callback globally |
|
1810 window[ callback ] = function( data ) { |
|
1811 // Fire success callbacks |
|
1812 success && success( data ); |
|
1813 isFired = true; |
|
1814 }; |
|
1815 |
|
1816 // Replace callback param and callback name |
|
1817 url = url.replace( parts.join( "=" ), parts[ 0 ] + "=" + callback ); |
|
1818 } |
|
1819 |
|
1820 script.onload = function() { |
|
1821 |
|
1822 // Handling remote script loading callbacks |
|
1823 if ( isScript ) { |
|
1824 // getScript |
|
1825 success && success(); |
|
1826 } |
|
1827 |
|
1828 // Executing for JSONP requests |
|
1829 if ( isFired ) { |
|
1830 // Garbage collect the callback |
|
1831 delete window[ callback ]; |
|
1832 } |
|
1833 // Garbage collect the script resource |
|
1834 head.removeChild( script ); |
|
1835 }; |
|
1836 |
|
1837 script.src = url; |
|
1838 |
|
1839 head.insertBefore( script, head.firstChild ); |
|
1840 |
|
1841 return; |
|
1842 }; |
|
1843 |
|
1844 Popcorn.getJSONP = Popcorn.xhr.getJSONP; |
|
1845 |
|
1846 Popcorn.getScript = Popcorn.xhr.getScript = function( url, success ) { |
|
1847 |
|
1848 return Popcorn.xhr.getJSONP( url, success, true ); |
|
1849 }; |
|
1850 |
|
1851 Popcorn.util = { |
|
1852 // Simple function to parse a timestamp into seconds |
|
1853 // Acceptable formats are: |
|
1854 // HH:MM:SS.MMM |
|
1855 // HH:MM:SS;FF |
|
1856 // Hours and minutes are optional. They default to 0 |
|
1857 toSeconds: function( timeStr, framerate ) { |
|
1858 // Hours and minutes are optional |
|
1859 // Seconds must be specified |
|
1860 // Seconds can be followed by milliseconds OR by the frame information |
|
1861 var validTimeFormat = /^([0-9]+:){0,2}[0-9]+([.;][0-9]+)?$/, |
|
1862 errorMessage = "Invalid time format", |
|
1863 digitPairs, lastIndex, lastPair, firstPair, |
|
1864 frameInfo, frameTime; |
|
1865 |
|
1866 if ( typeof timeStr === "number" ) { |
|
1867 return timeStr; |
|
1868 } |
|
1869 |
|
1870 if ( typeof timeStr === "string" && |
|
1871 !validTimeFormat.test( timeStr ) ) { |
|
1872 Popcorn.error( errorMessage ); |
|
1873 } |
|
1874 |
|
1875 digitPairs = timeStr.split( ":" ); |
|
1876 lastIndex = digitPairs.length - 1; |
|
1877 lastPair = digitPairs[ lastIndex ]; |
|
1878 |
|
1879 // Fix last element: |
|
1880 if ( lastPair.indexOf( ";" ) > -1 ) { |
|
1881 |
|
1882 frameInfo = lastPair.split( ";" ); |
|
1883 frameTime = 0; |
|
1884 |
|
1885 if ( framerate && ( typeof framerate === "number" ) ) { |
|
1886 frameTime = parseFloat( frameInfo[ 1 ], 10 ) / framerate; |
|
1887 } |
|
1888 |
|
1889 digitPairs[ lastIndex ] = parseInt( frameInfo[ 0 ], 10 ) + frameTime; |
|
1890 } |
|
1891 |
|
1892 firstPair = digitPairs[ 0 ]; |
|
1893 |
|
1894 return { |
|
1895 |
|
1896 1: parseFloat( firstPair, 10 ), |
|
1897 |
|
1898 2: ( parseInt( firstPair, 10 ) * 60 ) + |
|
1899 parseFloat( digitPairs[ 1 ], 10 ), |
|
1900 |
|
1901 3: ( parseInt( firstPair, 10 ) * 3600 ) + |
|
1902 ( parseInt( digitPairs[ 1 ], 10 ) * 60 ) + |
|
1903 parseFloat( digitPairs[ 2 ], 10 ) |
|
1904 |
|
1905 }[ digitPairs.length || 1 ]; |
|
1906 } |
|
1907 }; |
|
1908 |
|
1909 |
|
1910 // Initialize locale data |
|
1911 // Based on http://en.wikipedia.org/wiki/Language_localisation#Language_tags_and_codes |
|
1912 function initLocale( arg ) { |
|
1913 |
|
1914 var locale = typeof arg === "string" ? arg : [ arg.language, arg.region ].join( "-" ), |
|
1915 parts = locale.split( "-" ); |
|
1916 |
|
1917 // Setup locale data table |
|
1918 return { |
|
1919 iso6391: locale, |
|
1920 language: parts[ 0 ] || "", |
|
1921 region: parts[ 1 ] || "" |
|
1922 }; |
|
1923 } |
|
1924 |
|
1925 // Declare locale data table |
|
1926 var localeData = initLocale( global.navigator.userLanguage || global.navigator.language ); |
|
1927 |
|
1928 Popcorn.locale = { |
|
1929 |
|
1930 // Popcorn.locale.get() |
|
1931 // returns reference to privately |
|
1932 // defined localeData |
|
1933 get: function() { |
|
1934 return localeData; |
|
1935 }, |
|
1936 |
|
1937 // Popcorn.locale.set( string|object ); |
|
1938 set: function( arg ) { |
|
1939 |
|
1940 localeData = initLocale( arg ); |
|
1941 |
|
1942 Popcorn.locale.broadcast(); |
|
1943 |
|
1944 return localeData; |
|
1945 }, |
|
1946 |
|
1947 // Popcorn.locale.broadcast( type ) |
|
1948 // Sends events to all popcorn media instances that are |
|
1949 // listening for locale events |
|
1950 broadcast: function( type ) { |
|
1951 |
|
1952 var instances = Popcorn.instances, |
|
1953 length = instances.length, |
|
1954 idx = 0, |
|
1955 instance; |
|
1956 |
|
1957 type = type || "locale:changed"; |
|
1958 |
|
1959 // Iterate all current instances |
|
1960 for ( ; idx < length; idx++ ) { |
|
1961 instance = instances[ idx ]; |
|
1962 |
|
1963 // For those instances with locale event listeners, |
|
1964 // trigger a locale change event |
|
1965 if ( type in instance.data.events ) { |
|
1966 instance.trigger( type ); |
|
1967 } |
|
1968 } |
|
1969 } |
|
1970 }; |
|
1971 |
|
1972 // alias for exec function |
|
1973 Popcorn.p.cue = Popcorn.p.exec; |
|
1974 |
|
1975 // Protected API methods |
|
1976 Popcorn.protect = { |
|
1977 natives: Object.keys( Popcorn.p ).join( "," ).toLowerCase().split( "," ) |
|
1978 }; |
|
1979 |
|
1980 // Exposes Popcorn to global context |
|
1981 global.Popcorn = Popcorn; |
|
1982 |
|
1983 })(window, window.document); |