]> git.mxchange.org Git - friendica.git/blob - library/video-js/video.js
basic video playback support using VideoJS
[friendica.git] / library / video-js / video.js
1 /*!
2 Video.js - HTML5 Video Player
3 Version 3.2.0
4
5 LGPL v3 LICENSE INFO
6 This file is part of Video.js. Copyright 2011 Zencoder, Inc.
7
8 Video.js is free software: you can redistribute it and/or modify
9 it under the terms of the GNU Lesser General Public License as published by
10 the Free Software Foundation, either version 3 of the License, or
11 (at your option) any later version.
12
13 Video.js is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 GNU Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public License
19 along with Video.js.  If not, see <http://www.gnu.org/licenses/>.
20 */
21
22 // Self-executing function to prevent global vars and help with minification
23 ;(function(window, undefined){
24   var document = window.document;// HTML5 Shiv. Must be in <head> to support older browsers.
25 document.createElement("video");document.createElement("audio");
26
27 var VideoJS = function(id, addOptions, ready){
28   var tag; // Element of ID
29
30   // Allow for element or ID to be passed in
31   // String ID
32   if (typeof id == "string") {
33
34     // Adjust for jQuery ID syntax
35     if (id.indexOf("#") === 0) {
36       id = id.slice(1);
37     }
38
39     // If a player instance has already been created for this ID return it.
40     if (_V_.players[id]) {
41       return _V_.players[id];
42
43     // Otherwise get element for ID
44     } else {
45       tag = _V_.el(id)
46     }
47
48   // ID is a media element
49   } else {
50     tag = id;
51   }
52
53   // Check for a useable element
54   if (!tag || !tag.nodeName) { // re: nodeName, could be a box div also
55     throw new TypeError("The element or ID supplied is not valid. (VideoJS)"); // Returns
56   }
57
58   // Element may have a player attr referring to an already created player instance.
59   // If not, set up a new player and return the instance.
60   return tag.player || new _V_.Player(tag, addOptions, ready);
61 },
62
63 // Shortcut
64 _V_ = VideoJS,
65
66 // CDN Version. Used to target right flash swf.
67 CDN_VERSION = "3.2";
68
69 VideoJS.players = {};
70
71 VideoJS.options = {
72
73   // Default order of fallback technology
74   techOrder: ["html5","flash"],
75   // techOrder: ["flash","html5"],
76
77   html5: {},
78   flash: { swf: "http://vjs.zencdn.net/c/video-js.swf" },
79
80   // Default of web browser is 300x150. Should rely on source width/height.
81   width: "auto",
82   height: "auto",
83   
84   // defaultVolume: 0.85,
85   defaultVolume: 0.00, // The freakin seaguls are driving me crazy!
86
87   // Included control sets
88   components: {
89     "posterImage": {},
90     "textTrackDisplay": {},
91     "loadingSpinner": {},
92     "bigPlayButton": {},
93     "controlBar": {}
94   }
95
96   // components: [
97   //   "poster",
98   //   "loadingSpinner",
99   //   "bigPlayButton",
100   //   { name: "controlBar", options: {
101   //     components: [
102   //       "playToggle",
103   //       "fullscreenToggle",
104   //       "currentTimeDisplay",
105   //       "timeDivider",
106   //       "durationDisplay",
107   //       "remainingTimeDisplay",
108   //       { name: "progressControl", options: {
109   //         components: [
110   //           { name: "seekBar", options: {
111   //             components: [
112   //               "loadProgressBar",
113   //               "playProgressBar",
114   //               "seekHandle"
115   //             ]}
116   //           }
117   //         ]}
118   //       },
119   //       { name: "volumeControl", options: {
120   //         components: [
121   //           { name: "volumeBar", options: {
122   //             components: [
123   //               "volumeLevel",
124   //               "volumeHandle"
125   //             ]}
126   //           }
127   //         ]}
128   //       },
129   //       "muteToggle"
130   //     ]
131   //   }},
132   //   "subtitlesDisplay"/*, "replay"*/
133   // ]
134 };
135
136 // Set CDN Version of swf
137 if (CDN_VERSION != "GENERATED_CDN_VSN") {
138   _V_.options.flash.swf = "http://vjs.zencdn.net/"+CDN_VERSION+"/video-js.swf"
139 }_V_.merge = function(obj1, obj2, safe){
140   // Make sure second object exists
141   if (!obj2) { obj2 = {}; };
142
143   for (var attrname in obj2){
144     if (obj2.hasOwnProperty(attrname) && (!safe || !obj1.hasOwnProperty(attrname))) { obj1[attrname]=obj2[attrname]; }
145   }
146   return obj1;
147 };
148 _V_.extend = function(obj){ this.merge(this, obj, true); };
149
150 _V_.extend({
151   tech: {}, // Holder for playback technology settings
152   controlSets: {}, // Holder for control set definitions
153
154   // Device Checks
155   isIE: function(){ return !+"\v1"; },
156   isFF: function(){ return !!_V_.ua.match("Firefox") },
157   isIPad: function(){ return navigator.userAgent.match(/iPad/i) !== null; },
158   isIPhone: function(){ return navigator.userAgent.match(/iPhone/i) !== null; },
159   isIOS: function(){ return VideoJS.isIPhone() || VideoJS.isIPad(); },
160   iOSVersion: function() {
161     var match = navigator.userAgent.match(/OS (\d+)_/i);
162     if (match && match[1]) { return match[1]; }
163   },
164   isAndroid: function(){ return navigator.userAgent.match(/Android.*AppleWebKit/i) !== null; },
165   androidVersion: function() {
166     var match = navigator.userAgent.match(/Android (\d+)\./i);
167     if (match && match[1]) { return match[1]; }
168   },
169
170   testVid: document.createElement("video"),
171   ua: navigator.userAgent,
172   support: {},
173
174   each: function(arr, fn){
175     if (!arr || arr.length === 0) { return; }
176     for (var i=0,j=arr.length; i<j; i++) {
177       fn.call(this, arr[i], i);
178     }
179   },
180
181   eachProp: function(obj, fn){
182     if (!obj) { return; }
183     for (var name in obj) {
184       if (obj.hasOwnProperty(name)) {
185         fn.call(this, name, obj[name]);
186       }
187     }
188   },
189
190   el: function(id){ return document.getElementById(id); },
191   createElement: function(tagName, attributes){
192     var el = document.createElement(tagName),
193         attrname;
194     for (attrname in attributes){
195       if (attributes.hasOwnProperty(attrname)) {
196         if (attrname.indexOf("-") !== -1) {
197           el.setAttribute(attrname, attributes[attrname]);
198         } else {
199           el[attrname] = attributes[attrname];
200         }
201       }
202     }
203     return el;
204   },
205
206   insertFirst: function(node, parent){
207     if (parent.firstChild) {
208       parent.insertBefore(node, parent.firstChild);
209     } else {
210       parent.appendChild(node);
211     }
212   },
213
214   addClass: function(element, classToAdd){
215     if ((" "+element.className+" ").indexOf(" "+classToAdd+" ") == -1) {
216       element.className = element.className === "" ? classToAdd : element.className + " " + classToAdd;
217     }
218   },
219
220   removeClass: function(element, classToRemove){
221     if (element.className.indexOf(classToRemove) == -1) { return; }
222     var classNames = element.className.split(" ");
223     classNames.splice(classNames.indexOf(classToRemove),1);
224     element.className = classNames.join(" ");
225   },
226   
227   remove: function(item, array){
228     if (!array) return;
229     var i = array.indexOf(item);
230     if (i != -1) { 
231       return array.splice(i, 1)
232     };
233   },
234
235   // Attempt to block the ability to select text while dragging controls
236   blockTextSelection: function(){
237     document.body.focus();
238     document.onselectstart = function () { return false; };
239   },
240   // Turn off text selection blocking
241   unblockTextSelection: function(){ document.onselectstart = function () { return true; }; },
242
243   // Return seconds as H:MM:SS or M:SS
244   // Supplying a guide (in seconds) will include enough leading zeros to cover the length of the guide
245   formatTime: function(seconds, guide) {
246     guide = guide || seconds; // Default to using seconds as guide
247     var s = Math.floor(seconds % 60),
248         m = Math.floor(seconds / 60 % 60),
249         h = Math.floor(seconds / 3600),
250         gm = Math.floor(guide / 60 % 60),
251         gh = Math.floor(guide / 3600);
252
253     // Check if we need to show hours
254     h = (h > 0 || gh > 0) ? h + ":" : "";
255
256     // If hours are showing, we may need to add a leading zero.
257     // Always show at least one digit of minutes.
258     m = (((h || gm >= 10) && m < 10) ? "0" + m : m) + ":";
259
260     // Check if leading zero is need for seconds
261     s = (s < 10) ? "0" + s : s;
262
263     return h + m + s;
264   },
265
266   uc: function(string){
267     return string.charAt(0).toUpperCase() + string.slice(1);
268   },
269
270   // Return the relative horizonal position of an event as a value from 0-1
271   getRelativePosition: function(x, relativeElement){
272     return Math.max(0, Math.min(1, (x - _V_.findPosX(relativeElement)) / relativeElement.offsetWidth));
273   },
274   
275   getComputedStyleValue: function(element, style){
276     return window.getComputedStyle(element, null).getPropertyValue(style);
277   },
278
279   trim: function(string){ return string.toString().replace(/^\s+/, "").replace(/\s+$/, ""); },
280   round: function(num, dec) {
281     if (!dec) { dec = 0; }
282     return Math.round(num*Math.pow(10,dec))/Math.pow(10,dec);
283   },
284
285   isEmpty: function(object) {
286     for (var prop in object) {
287       return false;
288     }
289     return true;
290   },
291
292   // Mimic HTML5 TimeRange Spec.
293   createTimeRange: function(start, end){
294     return {
295       length: 1,
296       start: function() { return start; },
297       end: function() { return end; }
298     };
299   },
300
301   /* Element Data Store. Allows for binding data to an element without putting it directly on the element.
302      Ex. Event listneres are stored here.
303      (also from jsninja.com)
304   ================================================================================ */
305   cache: {}, // Where the data is stored
306   guid: 1, // Unique ID for the element
307   expando: "vdata" + (new Date).getTime(), // Unique attribute to store element's guid in
308
309   // Returns the cache object where data for the element is stored
310   getData: function(elem){
311     var id = elem[_V_.expando];
312     if (!id) {
313       id = elem[_V_.expando] = _V_.guid++;
314       _V_.cache[id] = {};
315     }
316     return _V_.cache[id];
317   },
318   // Delete data for the element from the cache and the guid attr from element
319   removeData: function(elem){
320     var id = elem[_V_.expando];
321     if (!id) { return; }
322     // Remove all stored data
323     delete _V_.cache[id];
324     // Remove the expando property from the DOM node
325     try {
326       delete elem[_V_.expando];
327     } catch(e) {
328       if (elem.removeAttribute) {
329         elem.removeAttribute(_V_.expando);
330       } else {
331         // IE doesn't appear to support removeAttribute on the document element
332         elem[_V_.expando] = null;
333       }
334     }
335   },
336
337   /* Proxy (a.k.a Bind or Context). A simple method for changing the context of a function
338      It also stores a unique id on the function so it can be easily removed from events
339   ================================================================================ */
340   proxy: function(context, fn, uid) {
341     // Make sure the function has a unique ID
342     if (!fn.guid) { fn.guid = _V_.guid++; }
343
344     // Create the new function that changes the context
345     var ret = function() {
346       return fn.apply(context, arguments);
347     }
348
349     // Allow for the ability to individualize this function
350     // Needed in the case where multiple objects might share the same prototype
351     // IF both items add an event listener with the same function, then you try to remove just one
352     // it will remove both because they both have the same guid.
353     // when using this, you need to use the proxy method when you remove the listener as well.
354     ret.guid = (uid) ? uid + "_" + fn.guid : fn.guid;
355
356     return ret;
357   },
358
359   get: function(url, onSuccess, onError){
360     // if (netscape.security.PrivilegeManager.enablePrivilege) {
361     //   netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
362     // }
363
364     var local = (url.indexOf("file:") == 0 || (window.location.href.indexOf("file:") == 0 && url.indexOf("http:") == -1));
365
366     if (typeof XMLHttpRequest == "undefined") {
367       XMLHttpRequest = function () {
368         try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e) {}
369         try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (f) {}
370         try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (g) {}
371         throw new Error("This browser does not support XMLHttpRequest.");
372       };
373     }
374
375     var request = new XMLHttpRequest();
376
377     try {
378       request.open("GET", url);
379     } catch(e) {
380       _V_.log("VideoJS XMLHttpRequest (open)", e);
381       // onError(e);
382       return false;
383     }
384
385     request.onreadystatechange = _V_.proxy(this, function() {
386       if (request.readyState == 4) {
387         if (request.status == 200 || local && request.status == 0) {
388           onSuccess(request.responseText);
389         } else {
390           if (onError) {
391             onError();
392           }
393         }
394       }
395     });
396
397     try {
398       request.send();
399     } catch(e) {
400       _V_.log("VideoJS XMLHttpRequest (send)", e);
401       if (onError) {
402         onError(e);
403       }
404     }
405   },
406
407   /* Local Storage
408   ================================================================================ */
409   setLocalStorage: function(key, value){
410     // IE was throwing errors referencing the var anywhere without this
411     var localStorage = window.localStorage || false;
412     if (!localStorage) { return; }
413     try {
414       localStorage[key] = value;
415     } catch(e) {
416       if (e.code == 22 || e.code == 1014) { // Webkit == 22 / Firefox == 1014
417         _V_.log("LocalStorage Full (VideoJS)", e);
418       } else {
419         _V_.log("LocalStorage Error (VideoJS)", e);
420       }
421     }
422   },
423
424   // Get abosolute version of relative URL. Used to tell flash correct URL.
425   // http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
426   getAbsoluteURL: function(url){
427
428     // Check if absolute URL
429     if (!url.match(/^https?:\/\//)) {
430       // Convert to absolute URL. Flash hosted off-site needs an absolute URL.
431       url = _V_.createElement('div', {
432         innerHTML: '<a href="'+url+'">x</a>'
433       }).firstChild.href;
434     }
435
436     return url;
437   }
438
439 });
440
441 // usage: log('inside coolFunc', this, arguments);
442 // paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/
443 _V_.log = function(){
444   _V_.log.history = _V_.log.history || [];// store logs to an array for reference
445   _V_.log.history.push(arguments);
446   if(window.console) {
447     arguments.callee = arguments.callee.caller;
448     var newarr = [].slice.call(arguments);
449     (typeof console.log === 'object' ? _V_.log.apply.call(console.log, console, newarr) : console.log.apply(console, newarr));
450   }
451 };
452
453 // make it safe to use console.log always
454 (function(b){function c(){}for(var d="assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,timeStamp,profile,profileEnd,time,timeEnd,trace,warn".split(","),a;a=d.pop();){b[a]=b[a]||c}})((function(){try
455 {console.log();return window.console;}catch(err){return window.console={};}})());
456
457 // Offset Left
458 // getBoundingClientRect technique from John Resig http://ejohn.org/blog/getboundingclientrect-is-awesome/
459 if ("getBoundingClientRect" in document.documentElement) {
460   _V_.findPosX = function(el) {
461     var box;
462
463     try {
464       box = el.getBoundingClientRect();
465     } catch(e) {}
466
467     if (!box) { return 0; }
468
469     var docEl = document.documentElement,
470         body = document.body,
471         clientLeft = docEl.clientLeft || body.clientLeft || 0,
472         scrollLeft = window.pageXOffset || body.scrollLeft,
473         left = box.left + scrollLeft - clientLeft;
474
475     return left;
476   };
477 } else {
478   _V_.findPosX = function(el) {
479     var curleft = el.offsetLeft;
480     // _V_.log(obj.className, obj.offsetLeft)
481     while(el = obj.offsetParent) {
482       if (el.className.indexOf("video-js") == -1) {
483         // _V_.log(el.offsetParent, "OFFSETLEFT", el.offsetLeft)
484         // _V_.log("-webkit-full-screen", el.webkitMatchesSelector("-webkit-full-screen"));
485         // _V_.log("-webkit-full-screen", el.querySelectorAll(".video-js:-webkit-full-screen"));
486       } else {
487       }
488       curleft += el.offsetLeft;
489     }
490     return curleft;
491   };
492 }// Using John Resig's Class implementation http://ejohn.org/blog/simple-javascript-inheritance/
493 // (function(){var initializing=false, fnTest=/xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; _V_.Class = function(){}; _V_.Class.extend = function(prop) { var _super = this.prototype; initializing = true; var prototype = new this(); initializing = false; for (var name in prop) { prototype[name] = typeof prop[name] == "function" && typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn){ return function() { var tmp = this._super; this._super = _super[name]; var ret = fn.apply(this, arguments); this._super = tmp; return ret; }; })(name, prop[name]) : prop[name]; } function Class() { if ( !initializing && this.init ) this.init.apply(this, arguments); } Class.prototype = prototype; Class.constructor = Class; Class.extend = arguments.callee; return Class;};})();
494 (function(){
495   var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
496   _V_.Class = function(){};
497   _V_.Class.extend = function(prop) {
498     var _super = this.prototype;
499     initializing = true;
500     var prototype = new this();
501     initializing = false;
502     for (var name in prop) {
503       prototype[name] = typeof prop[name] == "function" &&
504         typeof _super[name] == "function" && fnTest.test(prop[name]) ?
505         (function(name, fn){
506           return function() {
507             var tmp = this._super;
508             this._super = _super[name];
509             var ret = fn.apply(this, arguments);
510             this._super = tmp;
511             return ret;
512           };
513         })(name, prop[name]) :
514         prop[name];
515     }
516     function Class() {
517       if ( !initializing && this.init ) {
518         return this.init.apply(this, arguments);
519
520       // Attempting to recreate accessing function form of class.
521       } else if (!initializing) {
522         return arguments.callee.prototype.init()
523       }
524     }
525     Class.prototype = prototype;
526     Class.constructor = Class;
527     Class.extend = arguments.callee;
528     return Class;
529   };
530 })();
531
532 /* Player Component- Base class for all UI objects
533 ================================================================================ */
534 _V_.Component = _V_.Class.extend({
535
536   init: function(player, options){
537     this.player = player;
538
539     // Allow for overridding default component options
540     options = this.options = _V_.merge(this.options || {}, options);
541
542     // Create element if one wasn't provided in options
543     if (options.el) {
544       this.el = options.el;
545     } else {
546       this.el = this.createElement();
547     }
548
549     // Add any components in options
550     this.initComponents();
551   },
552
553   destroy: function(){},
554
555   createElement: function(type, attrs){
556     return _V_.createElement(type || "div", attrs);
557   },
558
559   buildCSSClass: function(){
560     // Child classes can include a function that does:
561     // return "CLASS NAME" + this._super();
562     return "";
563   },
564
565   initComponents: function(){
566     var options = this.options;
567     if (options && options.components) {
568       // Loop through components and add them to the player
569       this.eachProp(options.components, function(name, opts){
570
571         // Allow waiting to add components until a specific event is called
572         var tempAdd = this.proxy(function(){
573           // Set property name on player. Could cause conflicts with other prop names, but it's worth making refs easy.
574           this[name] = this.addComponent(name, opts);
575         });
576
577         if (opts.loadEvent) {
578           this.one(opts.loadEvent, tempAdd)
579         } else {
580           tempAdd();
581         }
582       });
583     }
584   },
585
586   // Add child components to this component.
587   // Will generate a new child component and then append child component's element to this component's element.
588   // Takes either the name of the UI component class, or an object that contains a name, UI Class, and options.
589   addComponent: function(name, options){
590     var component, componentClass;
591
592     // If string, create new component with options
593     if (typeof name == "string") {
594
595       // Make sure options is at least an empty object to protect against errors
596       options = options || {};
597
598       // Assume name of set is a lowercased name of the UI Class (PlayButton, etc.)
599       componentClass = options.componentClass || _V_.uc(name);
600
601       // Create a new object & element for this controls set
602       // If there's no .player, this is a player
603       component = new _V_[componentClass](this.player || this, options);
604
605     } else {
606       component = name;
607     }
608
609     // Add the UI object's element to the container div (box)
610     this.el.appendChild(component.el);
611
612     // Return so it can stored on parent object if desired.
613     return component;
614   },
615
616   removeComponent: function(component){
617     this.el.removeChild(component.el);
618   },
619
620   /* Display
621   ================================================================================ */
622   show: function(){
623     this.el.style.display = "block";
624   },
625
626   hide: function(){
627     this.el.style.display = "none";
628   },
629   
630   fadeIn: function(){
631     this.removeClass("vjs-fade-out");
632     this.addClass("vjs-fade-in");
633   },
634
635   fadeOut: function(){
636     this.removeClass("vjs-fade-in");
637     this.addClass("vjs-fade-out");
638   },
639
640   lockShowing: function(){
641     var style = this.el.style;
642     style.display = "block";
643     style.opacity = 1;
644     style.visiblity = "visible";
645   },
646
647   unlockShowing: function(){
648     var style = this.el.style;
649     style.display = "";
650     style.opacity = "";
651     style.visiblity = "";
652   },
653
654   addClass: function(classToAdd){
655     _V_.addClass(this.el, classToAdd);
656   },
657
658   removeClass: function(classToRemove){
659     _V_.removeClass(this.el, classToRemove);
660   },
661
662   /* Events
663   ================================================================================ */
664   addEvent: function(type, fn, uid){
665     return _V_.addEvent(this.el, type, _V_.proxy(this, fn));
666   },
667   removeEvent: function(type, fn){
668     return _V_.removeEvent(this.el, type, fn);
669   },
670   triggerEvent: function(type, e){
671     return _V_.triggerEvent(this.el, type, e);
672   },
673   one: function(type, fn) {
674     _V_.one(this.el, type, _V_.proxy(this, fn));
675   },
676
677   /* Ready - Trigger functions when component is ready
678   ================================================================================ */
679   ready: function(fn){
680     if (!fn) return this;
681
682     if (this.isReady) {
683       fn.call(this);
684     } else {
685       if (this.readyQueue === undefined) {
686         this.readyQueue = [];
687       }
688       this.readyQueue.push(fn);
689     }
690
691     return this;
692   },
693
694   triggerReady: function(){
695     this.isReady = true;
696     if (this.readyQueue && this.readyQueue.length > 0) {
697       // Call all functions in ready queue
698       this.each(this.readyQueue, function(fn){
699         fn.call(this);
700       });
701
702       // Reset Ready Queue
703       this.readyQueue = [];
704
705       // Allow for using event listeners also, in case you want to do something everytime a source is ready.
706       this.triggerEvent("ready");
707     }
708   },
709
710   /* Utility
711   ================================================================================ */
712   each: function(arr, fn){ _V_.each.call(this, arr, fn); },
713
714   eachProp: function(obj, fn){ _V_.eachProp.call(this, obj, fn); },
715
716   extend: function(obj){ _V_.merge(this, obj) },
717
718   // More easily attach 'this' to functions
719   proxy: function(fn, uid){  return _V_.proxy(this, fn, uid); }
720
721 });/* Control - Base class for all control elements
722 ================================================================================ */
723 _V_.Control = _V_.Component.extend({
724
725   buildCSSClass: function(){
726     return "vjs-control " + this._super();
727   }
728
729 });
730
731 /* Control Bar
732 ================================================================================ */
733 _V_.ControlBar = _V_.Component.extend({
734
735   options: {
736     loadEvent: "play",
737     components: {
738       "playToggle": {},
739       "fullscreenToggle": {},
740       "currentTimeDisplay": {},
741       "timeDivider": {},
742       "durationDisplay": {},
743       "remainingTimeDisplay": {},
744       "progressControl": {},
745       "volumeControl": {},
746       "muteToggle": {}
747     }
748   },
749
750   init: function(player, options){
751     this._super(player, options);
752
753     player.addEvent("play", this.proxy(function(){
754       this.fadeIn();
755       this.player.addEvent("mouseover", this.proxy(this.fadeIn));
756       this.player.addEvent("mouseout", this.proxy(this.fadeOut));
757     }));
758
759   },
760
761   createElement: function(){
762     return _V_.createElement("div", {
763       className: "vjs-controls"
764     });
765   },
766
767   fadeIn: function(){
768     this._super();
769     this.player.triggerEvent("controlsvisible");
770   },
771
772   fadeOut: function(){
773     this._super();
774     this.player.triggerEvent("controlshidden");
775   },
776
777   lockShowing: function(){
778     this.el.style.opacity = "1";
779   }
780
781 });
782
783 /* Button - Base class for all buttons
784 ================================================================================ */
785 _V_.Button = _V_.Control.extend({
786
787   init: function(player, options){
788     this._super(player, options);
789
790     this.addEvent("click", this.onClick);
791     this.addEvent("focus", this.onFocus);
792     this.addEvent("blur", this.onBlur);
793   },
794
795   createElement: function(type, attrs){
796     // Add standard Aria and Tabindex info
797     attrs = _V_.merge({
798       className: this.buildCSSClass(),
799       innerHTML: '<div><span class="vjs-control-text">' + (this.buttonText || "Need Text") + '</span></div>',
800       role: "button",
801       tabIndex: 0
802     }, attrs);
803
804     return this._super(type, attrs);
805   },
806
807   // Click - Override with specific functionality for button
808   onClick: function(){},
809
810   // Focus - Add keyboard functionality to element
811   onFocus: function(){
812     _V_.addEvent(document, "keyup", _V_.proxy(this, this.onKeyPress));
813   },
814
815   // KeyPress (document level) - Trigger click when keys are pressed
816   onKeyPress: function(event){
817     // Check for space bar (32) or enter (13) keys
818     if (event.which == 32 || event.which == 13) {
819       event.preventDefault();
820       this.onClick();
821     }
822   },
823
824   // Blur - Remove keyboard triggers
825   onBlur: function(){
826     _V_.removeEvent(document, "keyup", _V_.proxy(this, this.onKeyPress));
827   }
828
829 });
830
831 /* Play Button
832 ================================================================================ */
833 _V_.PlayButton = _V_.Button.extend({
834
835   buttonText: "Play",
836
837   buildCSSClass: function(){
838     return "vjs-play-button " + this._super();
839   },
840
841   onClick: function(){
842     this.player.play();
843   }
844
845 });
846
847 /* Pause Button
848 ================================================================================ */
849 _V_.PauseButton = _V_.Button.extend({
850
851   buttonText: "Pause",
852
853   buildCSSClass: function(){
854     return "vjs-pause-button " + this._super();
855   },
856
857   onClick: function(){
858     this.player.pause();
859   }
860
861 });
862
863 /* Play Toggle - Play or Pause Media
864 ================================================================================ */
865 _V_.PlayToggle = _V_.Button.extend({
866
867   buttonText: "Play",
868
869   init: function(player, options){
870     this._super(player, options);
871
872     player.addEvent("play", _V_.proxy(this, this.onPlay));
873     player.addEvent("pause", _V_.proxy(this, this.onPause));
874   },
875
876   buildCSSClass: function(){
877     return "vjs-play-control " + this._super();
878   },
879
880   // OnClick - Toggle between play and pause
881   onClick: function(){
882     if (this.player.paused()) {
883       this.player.play();
884     } else {
885       this.player.pause();
886     }
887   },
888
889   // OnPlay - Add the vjs-playing class to the element so it can change appearance
890   onPlay: function(){
891     _V_.removeClass(this.el, "vjs-paused");
892     _V_.addClass(this.el, "vjs-playing");
893   },
894
895   // OnPause - Add the vjs-paused class to the element so it can change appearance
896   onPause: function(){
897     _V_.removeClass(this.el, "vjs-playing");
898     _V_.addClass(this.el, "vjs-paused");
899   }
900
901 });
902
903
904 /* Fullscreen Toggle Behaviors
905 ================================================================================ */
906 _V_.FullscreenToggle = _V_.Button.extend({
907
908   buttonText: "Fullscreen",
909
910   buildCSSClass: function(){
911     return "vjs-fullscreen-control " + this._super();
912   },
913
914   onClick: function(){
915     if (!this.player.isFullScreen) {
916       this.player.requestFullScreen();
917     } else {
918       this.player.cancelFullScreen();
919     }
920   }
921
922 });
923
924 /* Big Play Button
925 ================================================================================ */
926 _V_.BigPlayButton = _V_.Button.extend({
927   init: function(player, options){
928     this._super(player, options);
929
930     player.addEvent("play", _V_.proxy(this, this.hide));
931     player.addEvent("ended", _V_.proxy(this, this.show));
932   },
933
934   createElement: function(){
935     return this._super("div", {
936       className: "vjs-big-play-button",
937       innerHTML: "<span></span>"
938     });
939   },
940
941   onClick: function(){
942     // Go back to the beginning if big play button is showing at the end.
943     // Have to check for current time otherwise it might throw a 'not ready' error.
944     if(this.player.currentTime()) {
945       this.player.currentTime(0);
946     }
947     this.player.play();
948   }
949 });
950
951 /* Loading Spinner
952 ================================================================================ */
953 _V_.LoadingSpinner = _V_.Component.extend({
954   init: function(player, options){
955     this._super(player, options);
956
957     player.addEvent("canplay", _V_.proxy(this, this.hide));
958     player.addEvent("canplaythrough", _V_.proxy(this, this.hide));
959     player.addEvent("playing", _V_.proxy(this, this.hide));
960
961     player.addEvent("seeking", _V_.proxy(this, this.show));
962     player.addEvent("error", _V_.proxy(this, this.show));
963
964     // Not showing spinner on stalled any more. Browsers may stall and then not trigger any events that would remove the spinner.
965     // Checked in Chrome 16 and Safari 5.1.2. http://help.videojs.com/discussions/problems/883-why-is-the-download-progress-showing
966     // player.addEvent("stalled", _V_.proxy(this, this.show));
967
968     player.addEvent("waiting", _V_.proxy(this, this.show));
969   },
970
971   createElement: function(){
972
973     var classNameSpinner, innerHtmlSpinner;
974
975     if ( typeof this.player.el.style.WebkitBorderRadius == "string"
976          || typeof this.player.el.style.MozBorderRadius == "string"
977          || typeof this.player.el.style.KhtmlBorderRadius == "string"
978          || typeof this.player.el.style.borderRadius == "string")
979       {
980         classNameSpinner = "vjs-loading-spinner";
981         innerHtmlSpinner = "<div class='ball1'></div><div class='ball2'></div><div class='ball3'></div><div class='ball4'></div><div class='ball5'></div><div class='ball6'></div><div class='ball7'></div><div class='ball8'></div>";
982       } else {
983         classNameSpinner = "vjs-loading-spinner-fallback";
984         innerHtmlSpinner = "";
985       }
986
987     return this._super("div", {
988       className: classNameSpinner,
989       innerHTML: innerHtmlSpinner
990     });
991   }
992 });
993
994 /* Time
995 ================================================================================ */
996 _V_.CurrentTimeDisplay = _V_.Component.extend({
997
998   init: function(player, options){
999     this._super(player, options);
1000
1001     player.addEvent("timeupdate", _V_.proxy(this, this.updateContent));
1002   },
1003
1004   createElement: function(){
1005     var el = this._super("div", {
1006       className: "vjs-current-time vjs-time-controls vjs-control"
1007     });
1008
1009     this.content = _V_.createElement("div", {
1010       className: "vjs-current-time-display",
1011       innerHTML: '0:00'
1012     });
1013
1014     el.appendChild(_V_.createElement("div").appendChild(this.content));
1015     return el;
1016   },
1017
1018   updateContent: function(){
1019     // Allows for smooth scrubbing, when player can't keep up.
1020     var time = (this.player.scrubbing) ? this.player.values.currentTime : this.player.currentTime();
1021     this.content.innerHTML = _V_.formatTime(time, this.player.duration());
1022   }
1023
1024 });
1025
1026 _V_.DurationDisplay = _V_.Component.extend({
1027
1028   init: function(player, options){
1029     this._super(player, options);
1030
1031     player.addEvent("timeupdate", _V_.proxy(this, this.updateContent));
1032   },
1033
1034   createElement: function(){
1035     var el = this._super("div", {
1036       className: "vjs-duration vjs-time-controls vjs-control"
1037     });
1038
1039     this.content = _V_.createElement("div", {
1040       className: "vjs-duration-display",
1041       innerHTML: '0:00'
1042     });
1043
1044     el.appendChild(_V_.createElement("div").appendChild(this.content));
1045     return el;
1046   },
1047
1048   updateContent: function(){
1049     if (this.player.duration()) { this.content.innerHTML = _V_.formatTime(this.player.duration()); }
1050   }
1051
1052 });
1053
1054 // Time Separator (Not used in main skin, but still available, and could be used as a 'spare element')
1055 _V_.TimeDivider = _V_.Component.extend({
1056
1057   createElement: function(){
1058     return this._super("div", {
1059       className: "vjs-time-divider",
1060       innerHTML: '<div><span>/</span></div>'
1061     });
1062   }
1063
1064 });
1065
1066 _V_.RemainingTimeDisplay = _V_.Component.extend({
1067
1068   init: function(player, options){
1069     this._super(player, options);
1070
1071     player.addEvent("timeupdate", _V_.proxy(this, this.updateContent));
1072   },
1073
1074   createElement: function(){
1075     var el = this._super("div", {
1076       className: "vjs-remaining-time vjs-time-controls vjs-control"
1077     });
1078
1079     this.content = _V_.createElement("div", {
1080       className: "vjs-remaining-time-display",
1081       innerHTML: '-0:00'
1082     });
1083
1084     el.appendChild(_V_.createElement("div").appendChild(this.content));
1085     return el;
1086   },
1087
1088   updateContent: function(){
1089     if (this.player.duration()) { this.content.innerHTML = "-"+_V_.formatTime(this.player.remainingTime()); }
1090
1091     // Allows for smooth scrubbing, when player can't keep up.
1092     // var time = (this.player.scrubbing) ? this.player.values.currentTime : this.player.currentTime();
1093     // this.content.innerHTML = _V_.formatTime(time, this.player.duration());
1094   }
1095
1096 });
1097
1098 /* Slider - Parent for seek bar and volume slider
1099 ================================================================================ */
1100 _V_.Slider = _V_.Component.extend({
1101
1102   init: function(player, options){
1103     this._super(player, options);
1104
1105     player.addEvent(this.playerEvent, _V_.proxy(this, this.update));
1106
1107     this.addEvent("mousedown", this.onMouseDown);
1108     this.addEvent("focus", this.onFocus);
1109     this.addEvent("blur", this.onBlur);
1110
1111     this.player.addEvent("controlsvisible", this.proxy(this.update));
1112
1113     // This is actually to fix the volume handle position. http://twitter.com/#!/gerritvanaaken/status/159046254519787520
1114     // this.player.one("timeupdate", this.proxy(this.update));
1115
1116     this.update();
1117   },
1118
1119   createElement: function(type, attrs) {
1120     attrs = _V_.merge({
1121       role: "slider",
1122       "aria-valuenow": 0,
1123       "aria-valuemin": 0,
1124       "aria-valuemax": 100,
1125       tabIndex: 0
1126     }, attrs);
1127
1128     return this._super(type, attrs);
1129   },
1130
1131   onMouseDown: function(event){
1132     event.preventDefault();
1133     _V_.blockTextSelection();
1134
1135     _V_.addEvent(document, "mousemove", _V_.proxy(this, this.onMouseMove));
1136     _V_.addEvent(document, "mouseup", _V_.proxy(this, this.onMouseUp));
1137
1138     this.onMouseMove(event);
1139   },
1140
1141   onMouseUp: function(event) {
1142     _V_.unblockTextSelection();
1143     _V_.removeEvent(document, "mousemove", this.onMouseMove, false);
1144     _V_.removeEvent(document, "mouseup", this.onMouseUp, false);
1145
1146     this.update();
1147   },
1148
1149   update: function(){
1150     // If scrubbing, we could use a cached value to make the handle keep up with the user's mouse.
1151     // On HTML5 browsers scrubbing is really smooth, but some flash players are slow, so we might want to utilize this later.
1152     // var progress =  (this.player.scrubbing) ? this.player.values.currentTime / this.player.duration() : this.player.currentTime() / this.player.duration();
1153
1154     var barProgress,
1155         progress = this.getPercent();
1156         handle = this.handle,
1157         bar = this.bar;
1158
1159     // Protect against no duration and other division issues
1160     if (isNaN(progress)) { progress = 0; }
1161
1162     barProgress = progress;
1163
1164     // If there is a handle, we need to account for the handle in our calculation for progress bar
1165     // so that it doesn't fall short of or extend past the handle.
1166     if (handle) {
1167
1168       var box = this.el,
1169           boxWidth = box.offsetWidth,
1170
1171           handleWidth = handle.el.offsetWidth,
1172
1173           // The width of the handle in percent of the containing box
1174           // In IE, widths may not be ready yet causing NaN
1175           handlePercent = (handleWidth) ? handleWidth / boxWidth : 0,
1176
1177           // Get the adjusted size of the box, considering that the handle's center never touches the left or right side.
1178           // There is a margin of half the handle's width on both sides.
1179           boxAdjustedPercent = 1 - handlePercent;
1180
1181           // Adjust the progress that we'll use to set widths to the new adjusted box width
1182           adjustedProgress = progress * boxAdjustedPercent,
1183
1184           // The bar does reach the left side, so we need to account for this in the bar's width
1185           barProgress = adjustedProgress + (handlePercent / 2);
1186
1187       // Move the handle from the left based on the adjected progress
1188       handle.el.style.left = _V_.round(adjustedProgress * 100, 2) + "%";
1189     }
1190
1191     // Set the new bar width
1192     bar.el.style.width = _V_.round(barProgress * 100, 2) + "%";
1193   },
1194
1195   calculateDistance: function(event){
1196     var box = this.el,
1197         boxX = _V_.findPosX(box),
1198         boxW = box.offsetWidth,
1199         handle = this.handle;
1200
1201     if (handle) {
1202       var handleW = handle.el.offsetWidth;
1203
1204       // Adjusted X and Width, so handle doesn't go outside the bar
1205       boxX = boxX + (handleW / 2);
1206       boxW = boxW - handleW;
1207     }
1208
1209     // Percent that the click is through the adjusted area
1210     return Math.max(0, Math.min(1, (event.pageX - boxX) / boxW));
1211   },
1212
1213   onFocus: function(event){
1214     _V_.addEvent(document, "keyup", _V_.proxy(this, this.onKeyPress));
1215   },
1216
1217   onKeyPress: function(event){
1218     if (event.which == 37) { // Left Arrow
1219       event.preventDefault();
1220       this.stepBack();
1221     } else if (event.which == 39) { // Right Arrow
1222       event.preventDefault();
1223       this.stepForward();
1224     }
1225   },
1226
1227   onBlur: function(event){
1228     _V_.removeEvent(document, "keyup", _V_.proxy(this, this.onKeyPress));
1229   }
1230 });
1231
1232
1233 /* Progress
1234 ================================================================================ */
1235
1236 // Progress Control: Seek, Load Progress, and Play Progress
1237 _V_.ProgressControl = _V_.Component.extend({
1238
1239   options: {
1240     components: {
1241       "seekBar": {}
1242     }
1243   },
1244
1245   createElement: function(){
1246     return this._super("div", {
1247       className: "vjs-progress-control vjs-control"
1248     });
1249   }
1250
1251 });
1252
1253 // Seek Bar and holder for the progress bars
1254 _V_.SeekBar = _V_.Slider.extend({
1255
1256   options: {
1257     components: {
1258       "loadProgressBar": {},
1259
1260       // Set property names to bar and handle to match with the parent Slider class is looking for
1261       "bar": { componentClass: "PlayProgressBar" },
1262       "handle": { componentClass: "SeekHandle" }
1263     }
1264   },
1265
1266   playerEvent: "timeupdate",
1267
1268   init: function(player, options){
1269     this._super(player, options);
1270   },
1271
1272   createElement: function(){
1273     return this._super("div", {
1274       className: "vjs-progress-holder"
1275     });
1276   },
1277
1278   getPercent: function(){
1279     return this.player.currentTime() / this.player.duration();
1280   },
1281
1282   onMouseDown: function(event){
1283     this._super(event);
1284
1285     this.player.scrubbing = true;
1286
1287     this.videoWasPlaying = !this.player.paused();
1288     this.player.pause();
1289   },
1290
1291   onMouseMove: function(event){
1292     var newTime = this.calculateDistance(event) * this.player.duration();
1293
1294     // Don't let video end while scrubbing.
1295     if (newTime == this.player.duration()) { newTime = newTime - 0.1; }
1296
1297     // Set new time (tell player to seek to new time)
1298     this.player.currentTime(newTime);
1299   },
1300
1301   onMouseUp: function(event){
1302     this._super(event);
1303
1304     this.player.scrubbing = false;
1305     if (this.videoWasPlaying) {
1306       this.player.play();
1307     }
1308   },
1309
1310   stepForward: function(){
1311     this.player.currentTime(this.player.currentTime() + 1);
1312   },
1313
1314   stepBack: function(){
1315     this.player.currentTime(this.player.currentTime() - 1);
1316   }
1317
1318 });
1319
1320 // Load Progress Bar
1321 _V_.LoadProgressBar = _V_.Component.extend({
1322
1323   init: function(player, options){
1324     this._super(player, options);
1325     player.addEvent("progress", _V_.proxy(this, this.update));
1326   },
1327
1328   createElement: function(){
1329     return this._super("div", {
1330       className: "vjs-load-progress",
1331       innerHTML: '<span class="vjs-control-text">Loaded: 0%</span>'
1332     });
1333   },
1334
1335   update: function(){
1336     if (this.el.style) { this.el.style.width = _V_.round(this.player.bufferedPercent() * 100, 2) + "%"; }
1337   }
1338
1339 });
1340
1341 // Play Progress Bar
1342 _V_.PlayProgressBar = _V_.Component.extend({
1343
1344   createElement: function(){
1345     return this._super("div", {
1346       className: "vjs-play-progress",
1347       innerHTML: '<span class="vjs-control-text">Progress: 0%</span>'
1348     });
1349   }
1350
1351 });
1352
1353 // Seek Handle
1354 // SeekBar Behavior includes play progress bar, and seek handle
1355 // Needed so it can determine seek position based on handle position/size
1356 _V_.SeekHandle = _V_.Component.extend({
1357
1358   createElement: function(){
1359     return this._super("div", {
1360       className: "vjs-seek-handle",
1361       innerHTML: '<span class="vjs-control-text">00:00</span>'
1362     });
1363   }
1364
1365 });
1366
1367 /* Volume Scrubber
1368 ================================================================================ */
1369 // Progress Control: Seek, Load Progress, and Play Progress
1370 _V_.VolumeControl = _V_.Component.extend({
1371
1372   options: {
1373     components: {
1374       "volumeBar": {}
1375     }
1376   },
1377
1378   createElement: function(){
1379     return this._super("div", {
1380       className: "vjs-volume-control vjs-control"
1381     });
1382   }
1383
1384 });
1385
1386 _V_.VolumeBar = _V_.Slider.extend({
1387
1388   options: {
1389     components: {
1390       "bar": { componentClass: "VolumeLevel" },
1391       "handle": { componentClass: "VolumeHandle" }
1392     }
1393   },
1394
1395   playerEvent: "volumechange",
1396
1397   createElement: function(){
1398     return this._super("div", {
1399       className: "vjs-volume-bar"
1400     });
1401   },
1402
1403   onMouseMove: function(event) {
1404     this.player.volume(this.calculateDistance(event));
1405   },
1406
1407   getPercent: function(){
1408    return this.player.volume();
1409   },
1410
1411   stepForward: function(){
1412     this.player.volume(this.player.volume() + 0.1);
1413   },
1414
1415   stepBack: function(){
1416     this.player.volume(this.player.volume() - 0.1);
1417   }
1418 });
1419
1420 _V_.VolumeLevel = _V_.Component.extend({
1421
1422   createElement: function(){
1423     return this._super("div", {
1424       className: "vjs-volume-level",
1425       innerHTML: '<span class="vjs-control-text"></span>'
1426     });
1427   }
1428
1429 });
1430
1431 _V_.VolumeHandle = _V_.Component.extend({
1432
1433   createElement: function(){
1434     return this._super("div", {
1435       className: "vjs-volume-handle",
1436       innerHTML: '<span class="vjs-control-text"></span>'
1437       // tabindex: 0,
1438       // role: "slider", "aria-valuenow": 0, "aria-valuemin": 0, "aria-valuemax": 100
1439     });
1440   }
1441
1442 });
1443
1444 _V_.MuteToggle = _V_.Button.extend({
1445
1446   init: function(player, options){
1447     this._super(player, options);
1448
1449     player.addEvent("volumechange", _V_.proxy(this, this.update));
1450   },
1451
1452   createElement: function(){
1453     return this._super("div", {
1454       className: "vjs-mute-control vjs-control",
1455       innerHTML: '<div><span class="vjs-control-text">Mute</span></div>'
1456     });
1457   },
1458
1459   onClick: function(event){
1460     this.player.muted( this.player.muted() ? false : true );
1461   },
1462
1463   update: function(event){
1464     var vol = this.player.volume(),
1465         level = 3;
1466
1467     if (vol == 0 || this.player.muted()) {
1468       level = 0;
1469     } else if (vol < 0.33) {
1470       level = 1;
1471     } else if (vol < 0.67) {
1472       level = 2;
1473     }
1474
1475     /* TODO improve muted icon classes */
1476     _V_.each.call(this, [0,1,2,3], function(i){
1477       _V_.removeClass(this.el, "vjs-vol-"+i);
1478     });
1479     _V_.addClass(this.el, "vjs-vol-"+level);
1480   }
1481
1482 });
1483
1484
1485 /* Poster Image
1486 ================================================================================ */
1487 _V_.PosterImage = _V_.Button.extend({
1488   init: function(player, options){
1489     this._super(player, options);
1490
1491     if (!this.player.options.poster) {
1492       this.hide();
1493     }
1494
1495     player.addEvent("play", _V_.proxy(this, this.hide));
1496   },
1497
1498   createElement: function(){
1499     return _V_.createElement("img", {
1500       className: "vjs-poster",
1501       src: this.player.options.poster,
1502
1503       // Don't want poster to be tabbable.
1504       tabIndex: -1
1505     });
1506   },
1507
1508   onClick: function(){
1509     this.player.play();
1510   }
1511 });
1512
1513 /* Menu
1514 ================================================================================ */
1515 // The base for text track and settings menu buttons.
1516 _V_.Menu = _V_.Component.extend({
1517
1518   init: function(player, options){
1519     this._super(player, options);
1520   },
1521
1522   addItem: function(component){
1523     this.addComponent(component);
1524     component.addEvent("click", this.proxy(function(){
1525       this.unlockShowing();
1526     }));
1527   },
1528
1529   createElement: function(){
1530     return this._super("ul", {
1531       className: "vjs-menu"
1532     });
1533   }
1534
1535 });
1536
1537 _V_.MenuItem = _V_.Button.extend({
1538
1539   init: function(player, options){
1540     this._super(player, options);
1541
1542     if (options.selected) {
1543       this.addClass("vjs-selected");
1544     }
1545   },
1546
1547   createElement: function(type, attrs){
1548     return this._super("li", _V_.merge({
1549       className: "vjs-menu-item",
1550       innerHTML: this.options.label
1551     }, attrs));
1552   },
1553
1554   onClick: function(){
1555     this.selected(true);
1556   },
1557
1558   selected: function(selected){
1559     if (selected) {
1560       this.addClass("vjs-selected");
1561     } else {
1562       this.removeClass("vjs-selected")
1563     }
1564   }
1565
1566 });// ECMA-262 is the standard for javascript.
1567 // The following methods are impelemented EXACTLY as described in the standard (according to Mozilla Docs), and do not override the default method if one exists.
1568 // This may conflict with other libraries that modify the array prototype, but those libs should update to use the standard.
1569
1570 // [].indexOf
1571 // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf
1572 if (!Array.prototype.indexOf) {
1573     Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) {
1574         "use strict";
1575         if (this === void 0 || this === null) {
1576             throw new TypeError();
1577         }
1578         var t = Object(this);
1579         var len = t.length >>> 0;
1580         if (len === 0) {
1581             return -1;
1582         }
1583         var n = 0;
1584         if (arguments.length > 0) {
1585             n = Number(arguments[1]);
1586             if (n !== n) { // shortcut for verifying if it's NaN
1587                 n = 0;
1588             } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) {
1589                 n = (n > 0 || -1) * Math.floor(Math.abs(n));
1590             }
1591         }
1592         if (n >= len) {
1593             return -1;
1594         }
1595         var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
1596         for (; k < len; k++) {
1597             if (k in t && t[k] === searchElement) {
1598                 return k;
1599             }
1600         }
1601         return -1;
1602     }
1603 }
1604
1605 // NOT NEEDED YET
1606 // [].lastIndexOf
1607 // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/lastIndexOf
1608 // if (!Array.prototype.lastIndexOf)
1609 // {
1610 //   Array.prototype.lastIndexOf = function(searchElement /*, fromIndex*/)
1611 //   {
1612 //     "use strict";
1613 // 
1614 //     if (this === void 0 || this === null)
1615 //       throw new TypeError();
1616 // 
1617 //     var t = Object(this);
1618 //     var len = t.length >>> 0;
1619 //     if (len === 0)
1620 //       return -1;
1621 // 
1622 //     var n = len;
1623 //     if (arguments.length > 1)
1624 //     {
1625 //       n = Number(arguments[1]);
1626 //       if (n !== n)
1627 //         n = 0;
1628 //       else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0))
1629 //         n = (n > 0 || -1) * Math.floor(Math.abs(n));
1630 //     }
1631 // 
1632 //     var k = n >= 0
1633 //           ? Math.min(n, len - 1)
1634 //           : len - Math.abs(n);
1635 // 
1636 //     for (; k >= 0; k--)
1637 //     {
1638 //       if (k in t && t[k] === searchElement)
1639 //         return k;
1640 //     }
1641 //     return -1;
1642 //   };
1643 // }
1644
1645
1646 // NOT NEEDED YET
1647 // Array forEach per ECMA standard https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/foreach
1648 // Production steps of ECMA-262, Edition 5, 15.4.4.18
1649 // Reference: http://es5.github.com/#x15.4.4.18
1650 // if ( !Array.prototype.forEach ) {
1651 // 
1652 //   Array.prototype.forEach = function( callback, thisArg ) {
1653 // 
1654 //     var T, k;
1655 // 
1656 //     if ( this == null ) {
1657 //       throw new TypeError( " this is null or not defined" );
1658 //     }
1659 // 
1660 //     // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
1661 //     var O = Object(this);
1662 // 
1663 //     // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
1664 //     // 3. Let len be ToUint32(lenValue).
1665 //     var len = O.length >>> 0;
1666 // 
1667 //     // 4. If IsCallable(callback) is false, throw a TypeError exception.
1668 //     // See: http://es5.github.com/#x9.11
1669 //     if ( {}.toString.call(callback) != "[object Function]" ) {
1670 //       throw new TypeError( callback + " is not a function" );
1671 //     }
1672 // 
1673 //     // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
1674 //     if ( thisArg ) {
1675 //       T = thisArg;
1676 //     }
1677 // 
1678 //     // 6. Let k be 0
1679 //     k = 0;
1680 // 
1681 //     // 7. Repeat, while k < len
1682 //     while( k < len ) {
1683 // 
1684 //       var kValue;
1685 // 
1686 //       // a. Let Pk be ToString(k).
1687 //       //   This is implicit for LHS operands of the in operator
1688 //       // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
1689 //       //   This step can be combined with c
1690 //       // c. If kPresent is true, then
1691 //       if ( k in O ) {
1692 // 
1693 //         // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
1694 //         kValue = O[ Pk ];
1695 // 
1696 //         // ii. Call the Call internal method of callback with T as the this value and
1697 //         // argument list containing kValue, k, and O.
1698 //         callback.call( T, kValue, k, O );
1699 //       }
1700 //       // d. Increase k by 1.
1701 //       k++;
1702 //     }
1703 //     // 8. return undefined
1704 //   };
1705 // }
1706
1707
1708 // NOT NEEDED YET
1709 // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/map
1710 // Production steps of ECMA-262, Edition 5, 15.4.4.19
1711 // Reference: http://es5.github.com/#x15.4.4.19
1712 // if (!Array.prototype.map) {
1713 //   Array.prototype.map = function(callback, thisArg) {
1714 // 
1715 //     var T, A, k;
1716 // 
1717 //     if (this == null) {
1718 //       throw new TypeError(" this is null or not defined");
1719 //     }
1720 // 
1721 //     // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
1722 //     var O = Object(this);
1723 // 
1724 //     // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
1725 //     // 3. Let len be ToUint32(lenValue).
1726 //     var len = O.length >>> 0;
1727 // 
1728 //     // 4. If IsCallable(callback) is false, throw a TypeError exception.
1729 //     // See: http://es5.github.com/#x9.11
1730 //     if ({}.toString.call(callback) != "[object Function]") {
1731 //       throw new TypeError(callback + " is not a function");
1732 //     }
1733 // 
1734 //     // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
1735 //     if (thisArg) {
1736 //       T = thisArg;
1737 //     }
1738 // 
1739 //     // 6. Let A be a new array created as if by the expression new Array(len) where Array is
1740 //     // the standard built-in constructor with that name and len is the value of len.
1741 //     A = new Array(len);
1742 // 
1743 //     // 7. Let k be 0
1744 //     k = 0;
1745 // 
1746 //     // 8. Repeat, while k < len
1747 //     while(k < len) {
1748 // 
1749 //       var kValue, mappedValue;
1750 // 
1751 //       // a. Let Pk be ToString(k).
1752 //       //   This is implicit for LHS operands of the in operator
1753 //       // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
1754 //       //   This step can be combined with c
1755 //       // c. If kPresent is true, then
1756 //       if (k in O) {
1757 // 
1758 //         // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
1759 //         kValue = O[ k ];
1760 // 
1761 //         // ii. Let mappedValue be the result of calling the Call internal method of callback
1762 //         // with T as the this value and argument list containing kValue, k, and O.
1763 //         mappedValue = callback.call(T, kValue, k, O);
1764 // 
1765 //         // iii. Call the DefineOwnProperty internal method of A with arguments
1766 //         // Pk, Property Descriptor {Value: mappedValue, Writable: true, Enumerable: true, Configurable: true},
1767 //         // and false.
1768 // 
1769 //         // In browsers that support Object.defineProperty, use the following:
1770 //         // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
1771 // 
1772 //         // For best browser support, use the following:
1773 //         A[ k ] = mappedValue;
1774 //       }
1775 //       // d. Increase k by 1.
1776 //       k++;
1777 //     }
1778 // 
1779 //     // 9. return A
1780 //     return A;
1781 //   };      
1782 // }
1783 // Event System (J.Resig - Secrets of a JS Ninja http://jsninja.com/ [Go read it, really])
1784 // (Book version isn't completely usable, so fixed some things and borrowed from jQuery where it's working)
1785 // 
1786 // This should work very similarly to jQuery's events, however it's based off the book version which isn't as
1787 // robust as jquery's, so there's probably some differences.
1788 // 
1789 // When you add an event listener using _V_.addEvent, 
1790 //   it stores the handler function in seperate cache object, 
1791 //   and adds a generic handler to the element's event,
1792 //   along with a unique id (guid) to the element.
1793
1794 _V_.extend({
1795
1796   // Add an event listener to element
1797   // It stores the handler function in a separate cache object
1798   // and adds a generic handler to the element's event,
1799   // along with a unique id (guid) to the element.
1800   addEvent: function(elem, type, fn){
1801     var data = _V_.getData(elem), handlers;
1802
1803     // We only need to generate one handler per element
1804     if (data && !data.handler) {
1805       // Our new meta-handler that fixes the event object and the context
1806       data.handler = function(event){
1807         event = _V_.fixEvent(event);
1808         var handlers = _V_.getData(elem).events[event.type];
1809         // Go through and call all the real bound handlers
1810         if (handlers) {
1811           
1812           // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
1813           var handlersCopy = [];
1814           _V_.each(handlers, function(handler, i){
1815             handlersCopy[i] = handler;
1816           })
1817           
1818           for (var i = 0, l = handlersCopy.length; i < l; i++) {
1819             handlersCopy[i].call(elem, event);
1820           }
1821         }
1822       };
1823     }
1824
1825     // We need a place to store all our event data
1826     if (!data.events) { data.events = {}; }
1827
1828     // And a place to store the handlers for this event type
1829     handlers = data.events[type];
1830
1831     if (!handlers) {
1832       handlers = data.events[ type ] = [];
1833
1834       // Attach our meta-handler to the element, since one doesn't exist
1835       if (document.addEventListener) {
1836         elem.addEventListener(type, data.handler, false);
1837       } else if (document.attachEvent) {
1838         elem.attachEvent("on" + type, data.handler);
1839       }
1840     }
1841
1842     if (!fn.guid) { fn.guid = _V_.guid++; }
1843
1844     handlers.push(fn);
1845   },
1846
1847   removeEvent: function(elem, type, fn) {
1848     var data = _V_.getData(elem), handlers;
1849     // If no events exist, nothing to unbind
1850     if (!data.events) { return; }
1851
1852     // Are we removing all bound events?
1853     if (!type) {
1854       for (type in data.events) {
1855         _V_.cleanUpEvents(elem, type);
1856       }
1857       return;
1858     }
1859
1860     // And a place to store the handlers for this event type
1861     handlers = data.events[type];
1862
1863     // If no handlers exist, nothing to unbind
1864     if (!handlers) { return; }
1865
1866     // See if we're only removing a single handler
1867     if (fn && fn.guid) {
1868       for (var i = 0; i < handlers.length; i++) {
1869         // We found a match (don't stop here, there could be a couple bound)
1870         if (handlers[i].guid === fn.guid) {
1871           // Remove the handler from the array of handlers
1872           handlers.splice(i--, 1);
1873         }
1874       }
1875     }
1876
1877     _V_.cleanUpEvents(elem, type);
1878   },
1879
1880   cleanUpEvents: function(elem, type) {
1881     var data = _V_.getData(elem);
1882     // Remove the events of a particular type if there are none left
1883
1884     if (data.events[type].length === 0) {
1885       delete data.events[type];
1886
1887       // Remove the meta-handler from the element
1888       if (document.removeEventListener) {
1889         elem.removeEventListener(type, data.handler, false);
1890       } else if (document.detachEvent) {
1891         elem.detachEvent("on" + type, data.handler);
1892       }
1893     }
1894
1895     // Remove the events object if there are no types left
1896     if (_V_.isEmpty(data.events)) {
1897       delete data.events;
1898       delete data.handler;
1899     }
1900
1901     // Finally remove the expando if there is no data left
1902     if (_V_.isEmpty(data)) {
1903       _V_.removeData(elem);
1904     }
1905   },
1906
1907   fixEvent: function(event) {
1908     if (event[_V_.expando]) { return event; }
1909     // store a copy of the original event object
1910     // and "clone" to set read-only properties
1911     var originalEvent = event;
1912     event = new _V_.Event(originalEvent);
1913
1914     for ( var i = _V_.Event.props.length, prop; i; ) {
1915       prop = _V_.Event.props[ --i ];
1916       event[prop] = originalEvent[prop];
1917     }
1918
1919     // Fix target property, if necessary
1920     if (!event.target) { event.target = event.srcElement || document; }
1921
1922     // check if target is a textnode (safari)
1923     if (event.target.nodeType === 3) { event.target = event.target.parentNode; }
1924
1925     // Add relatedTarget, if necessary
1926     if (!event.relatedTarget && event.fromElement) {
1927       event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
1928     }
1929
1930     // Calculate pageX/Y if missing and clientX/Y available
1931     if ( event.pageX == null && event.clientX != null ) {
1932       var eventDocument = event.target.ownerDocument || document,
1933         doc = eventDocument.documentElement,
1934         body = eventDocument.body;
1935
1936       event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
1937       event.pageY = event.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0);
1938     }
1939
1940     // Add which for key events
1941     if (event.which == null && (event.charCode != null || event.keyCode != null)) {
1942       event.which = event.charCode != null ? event.charCode : event.keyCode;
1943     }
1944
1945     // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
1946     if ( !event.metaKey && event.ctrlKey ) {
1947       event.metaKey = event.ctrlKey;
1948     }
1949
1950     // Add which for click: 1 === left; 2 === middle; 3 === right
1951     // Note: button is not normalized, so don't use it
1952     if ( !event.which && event.button !== undefined ) {
1953       event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
1954     }
1955
1956     return event;
1957   },
1958
1959   triggerEvent: function(elem, event) {
1960     var data = _V_.getData(elem),
1961         parent = elem.parentNode || elem.ownerDocument,
1962         type = event.type || event,
1963         handler;
1964
1965     if (data) { handler = data.handler }
1966
1967     // Added in attion to book. Book code was broke.
1968     event = typeof event === "object" ?
1969       event[_V_.expando] ? 
1970         event :
1971         new _V_.Event(type, event) :
1972       new _V_.Event(type);
1973
1974     event.type = type;
1975     if (handler) {
1976       handler.call(elem, event);
1977     }
1978
1979     // Clean up the event in case it is being reused
1980     event.result = undefined;
1981     event.target = elem;
1982
1983     // Bubble the event up the tree to the document,
1984     // Unless it's been explicitly stopped
1985     // if (parent && !event.isPropagationStopped()) {
1986     //   _V_.triggerEvent(parent, event);
1987     // 
1988     // // We're at the top document so trigger the default action
1989     // } else if (!parent && !event.isDefaultPrevented()) {
1990     //   // log(type);
1991     //   var targetData = _V_.getData(event.target);
1992     //   // log(targetData);
1993     //   var targetHandler = targetData.handler;
1994     //   // log("2");
1995     //   if (event.target[event.type]) {
1996     //     // Temporarily disable the bound handler,
1997     //     // don't want to execute it twice
1998     //     if (targetHandler) {
1999     //       targetData.handler = function(){};
2000     //     }
2001     // 
2002     //     // Trigger the native event (click, focus, blur)
2003     //     event.target[event.type]();
2004     // 
2005     //     // Restore the handler
2006     //     if (targetHandler) {
2007     //       targetData.handler = targetHandler;
2008     //     }
2009     //   }
2010     // }
2011   },
2012   
2013   one: function(elem, type, fn) {
2014     _V_.addEvent(elem, type, function(){
2015       _V_.removeEvent(elem, type, arguments.callee)
2016       fn.apply(this, arguments);
2017     });
2018   }
2019 });
2020
2021 // Custom Event object for standardizing event objects between browsers.
2022 _V_.Event = function(src, props){
2023   // Event object
2024   if (src && src.type) {
2025     this.originalEvent = src;
2026     this.type = src.type;
2027
2028     // Events bubbling up the document may have been marked as prevented
2029     // by a handler lower down the tree; reflect the correct value.
2030     this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false ||
2031       src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse;
2032
2033   // Event type
2034   } else {
2035     this.type = src;
2036   }
2037
2038   // Put explicitly provided properties onto the event object
2039   if (props) { _V_.merge(this, props); }
2040
2041   this.timeStamp = (new Date).getTime();
2042
2043   // Mark it as fixed
2044   this[_V_.expando] = true;
2045 };
2046
2047 _V_.Event.prototype = {
2048   preventDefault: function() {
2049     this.isDefaultPrevented = returnTrue;
2050
2051     var e = this.originalEvent;
2052     if (!e) { return; }
2053
2054     // if preventDefault exists run it on the original event
2055     if (e.preventDefault) { 
2056       e.preventDefault();
2057     // otherwise set the returnValue property of the original event to false (IE)
2058     } else {
2059       e.returnValue = false;
2060     }
2061   },
2062   stopPropagation: function() {
2063     this.isPropagationStopped = returnTrue;
2064
2065     var e = this.originalEvent;
2066     if (!e) { return; }
2067     // if stopPropagation exists run it on the original event
2068     if (e.stopPropagation) { e.stopPropagation(); }
2069     // otherwise set the cancelBubble property of the original event to true (IE)
2070     e.cancelBubble = true;
2071   },
2072   stopImmediatePropagation: function() {
2073     this.isImmediatePropagationStopped = returnTrue;
2074     this.stopPropagation();
2075   },
2076   isDefaultPrevented: returnFalse,
2077   isPropagationStopped: returnFalse,
2078   isImmediatePropagationStopped: returnFalse
2079 };
2080 _V_.Event.props = "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" ");
2081
2082 function returnTrue(){ return true; }
2083 function returnFalse(){ return false; }
2084
2085 // Javascript JSON implementation
2086 // (Parse Method Only)
2087 // https://github.com/douglascrockford/JSON-js/blob/master/json2.js
2088
2089 var JSON;
2090 if (!JSON) { JSON = {}; }
2091
2092 (function(){
2093   var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
2094
2095   if (typeof JSON.parse !== 'function') {
2096       JSON.parse = function (text, reviver) {
2097           var j;
2098
2099           function walk(holder, key) {
2100               var k, v, value = holder[key];
2101               if (value && typeof value === 'object') {
2102                   for (k in value) {
2103                       if (Object.prototype.hasOwnProperty.call(value, k)) {
2104                           v = walk(value, k);
2105                           if (v !== undefined) {
2106                               value[k] = v;
2107                           } else {
2108                               delete value[k];
2109                           }
2110                       }
2111                   }
2112               }
2113               return reviver.call(holder, key, value);
2114           }
2115           text = String(text);
2116           cx.lastIndex = 0;
2117           if (cx.test(text)) {
2118               text = text.replace(cx, function (a) {
2119                   return '\\u' +
2120                       ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
2121               });
2122           }
2123
2124           if (/^[\],:{}\s]*$/
2125                   .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
2126                       .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
2127                       .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
2128
2129               j = eval('(' + text + ')');
2130
2131               return typeof reviver === 'function' ?
2132                   walk({'': j}, '') : j;
2133           }
2134
2135           throw new SyntaxError('JSON.parse');
2136       };
2137   }
2138 }());
2139 /* UI Component- Base class for all UI objects
2140 ================================================================================ */
2141 _V_.Player = _V_.Component.extend({
2142
2143   init: function(tag, addOptions, ready){
2144
2145     this.tag = tag; // Store the original tag used to set options
2146
2147     var el = this.el = _V_.createElement("div"), // Div to contain video and controls
2148         options = this.options = {},
2149         width = options.width = tag.getAttribute('width'),
2150         height = options.height = tag.getAttribute('height'),
2151
2152         // Browsers default to 300x150 if there's no width/height or video size data.
2153         initWidth = width || 300,
2154         initHeight = height || 150;
2155
2156     // Make player findable on elements
2157     tag.player = el.player = this;
2158
2159     // Add callback to ready queue
2160     this.ready(ready);
2161
2162     // Wrap video tag in div (el/box) container
2163     tag.parentNode.insertBefore(el, tag);
2164     el.appendChild(tag); // Breaks iPhone, fixed in HTML5 setup.
2165
2166     // Give video tag properties to box
2167     el.id = this.id = tag.id; // ID will now reference box, not the video tag
2168     el.className = tag.className;
2169     // Update tag id/class for use as HTML5 playback tech
2170     tag.id += "_html5_api";
2171     tag.className = "vjs-tech";
2172
2173     // Make player easily findable by ID
2174     _V_.players[el.id] = this;
2175
2176     // Make box use width/height of tag, or default 300x150
2177     el.setAttribute("width", initWidth);
2178     el.setAttribute("height", initHeight);
2179     // Enforce with CSS since width/height attrs don't work on divs
2180     el.style.width = initWidth+"px";
2181     el.style.height = initHeight+"px";
2182     // Remove width/height attrs from tag so CSS can make it 100% width/height
2183     tag.removeAttribute("width");
2184     tag.removeAttribute("height");
2185
2186     // Set Options
2187     _V_.merge(options, _V_.options); // Copy Global Defaults
2188     _V_.merge(options, this.getVideoTagSettings()); // Override with Video Tag Options
2189     _V_.merge(options, addOptions); // Override/extend with options from setup call
2190
2191     // Store controls setting, and then remove immediately so native controls don't flash.
2192     tag.removeAttribute("controls");
2193
2194     // Poster will be handled by a manual <img>
2195     tag.removeAttribute("poster");
2196
2197     // Empty video tag sources and tracks so the built in player doesn't use them also.
2198     if (tag.hasChildNodes()) {
2199       for (var i=0,j=tag.childNodes;i<j.length;i++) {
2200         if (j[i].nodeName == "SOURCE" || j[i].nodeName == "TRACK") {
2201           tag.removeChild(j[i]);
2202         }
2203       }
2204     }
2205
2206     // Cache for video property values.
2207     this.values = {};
2208
2209     this.addClass("vjs-paused");
2210
2211     this.addEvent("ended", this.onEnded);
2212     this.addEvent("play", this.onPlay);
2213     this.addEvent("pause", this.onPause);
2214     this.addEvent("progress", this.onProgress);
2215     this.addEvent("error", this.onError);
2216
2217     // When the API is ready, loop through the components and add to the player.
2218     if (options.controls) {
2219       this.ready(function(){
2220         this.initComponents();
2221       });
2222     }
2223
2224     // Tracks defined in tracks.js
2225     this.textTracks = [];
2226     if (options.tracks && options.tracks.length > 0) {
2227       this.addTextTracks(options.tracks);
2228     }
2229
2230     // If there are no sources when the player is initialized,
2231     // load the first supported playback technology.
2232     if (!options.sources || options.sources.length == 0) {
2233       for (var i=0,j=options.techOrder; i<j.length; i++) {
2234         var techName = j[i],
2235             tech = _V_[techName];
2236
2237         // Check if the browser supports this technology
2238         if (tech.isSupported()) {
2239           this.loadTech(techName);
2240           break;
2241         }
2242       }
2243     } else {
2244       // Loop through playback technologies (HTML5, Flash) and check for support. Then load the best source.
2245       // A few assumptions here:
2246       //   All playback technologies respect preload false.
2247       this.src(options.sources);
2248     }
2249   },
2250
2251   // Cache for video property values.
2252   values: {},
2253
2254   destroy: function(){
2255     // Ensure that tracking progress and time progress will stop and plater deleted
2256     this.stopTrackingProgress();
2257     this.stopTrackingCurrentTime();
2258     _V_.players[this.id] = null;
2259     delete _V_.players[this.id];
2260     this.tech.destroy();
2261     this.el.parentNode.removeChild(this.el);
2262   },
2263
2264   createElement: function(type, options){},
2265
2266   getVideoTagSettings: function(){
2267     var options = {
2268       sources: [],
2269       tracks: []
2270     };
2271
2272     options.src = this.tag.getAttribute("src");
2273     options.controls = this.tag.getAttribute("controls") !== null;
2274     options.poster = this.tag.getAttribute("poster");
2275     options.preload = this.tag.getAttribute("preload");
2276     options.autoplay = this.tag.getAttribute("autoplay") !== null; // hasAttribute not IE <8 compatible
2277     options.loop = this.tag.getAttribute("loop") !== null;
2278     options.muted = this.tag.getAttribute("muted") !== null;
2279
2280     if (this.tag.hasChildNodes()) {
2281       for (var c,i=0,j=this.tag.childNodes;i<j.length;i++) {
2282         c = j[i];
2283         if (c.nodeName == "SOURCE") {
2284           options.sources.push({
2285             src: c.getAttribute('src'),
2286             type: c.getAttribute('type'),
2287             media: c.getAttribute('media'),
2288             title: c.getAttribute('title')
2289           });
2290         }
2291         if (c.nodeName == "TRACK") {
2292           options.tracks.push({
2293             src: c.getAttribute("src"),
2294             kind: c.getAttribute("kind"),
2295             srclang: c.getAttribute("srclang"),
2296             label: c.getAttribute("label"),
2297             'default': c.getAttribute("default") !== null,
2298             title: c.getAttribute("title")
2299           });
2300         }
2301       }
2302     }
2303     return options;
2304   },
2305
2306   /* PLayback Technology (tech)
2307   ================================================================================ */
2308   // Load/Create an instance of playback technlogy including element and API methods
2309   // And append playback element in player div.
2310   loadTech: function(techName, source){
2311
2312     // Pause and remove current playback technology
2313     if (this.tech) {
2314       this.unloadTech();
2315
2316     // If the first time loading, HTML5 tag will exist but won't be initialized
2317     // So we need to remove it if we're not loading HTML5
2318     } else if (techName != "html5" && this.tag) {
2319       this.el.removeChild(this.tag);
2320       this.tag = false;
2321     }
2322
2323     this.techName = techName;
2324
2325     // Turn off API access because we're loading a new tech that might load asynchronously
2326     this.isReady = false;
2327
2328     var techReady = function(){
2329       this.player.triggerReady();
2330
2331       // Manually track progress in cases where the browser/flash player doesn't report it.
2332       if (!this.support.progressEvent) {
2333         this.player.manualProgressOn();
2334       }
2335
2336       // Manually track timeudpates in cases where the browser/flash player doesn't report it.
2337       if (!this.support.timeupdateEvent) {
2338         this.player.manualTimeUpdatesOn();
2339       }
2340     }
2341
2342     // Grab tech-specific options from player options and add source and parent element to use.
2343     var techOptions = _V_.merge({ source: source, parentEl: this.el }, this.options[techName])
2344
2345     if (source) {
2346       if (source.src == this.values.src && this.values.currentTime > 0) {
2347         techOptions.startTime = this.values.currentTime;
2348       }
2349
2350       this.values.src = source.src;
2351     }
2352
2353     // Initialize tech instance
2354     this.tech = new _V_[techName](this, techOptions);
2355     this.tech.ready(techReady);
2356   },
2357
2358   unloadTech: function(){
2359     this.tech.destroy();
2360
2361     // Turn off any manual progress or timeupdate tracking
2362     if (this.manualProgress) { this.manualProgressOff(); }
2363
2364     if (this.manualTimeUpdates) { this.manualTimeUpdatesOff(); }
2365
2366     this.tech = false;
2367   },
2368
2369   // There's many issues around changing the size of a Flash (or other plugin) object.
2370   // First is a plugin reload issue in Firefox that has been around for 11 years: https://bugzilla.mozilla.org/show_bug.cgi?id=90268
2371   // Then with the new fullscreen API, Mozilla and webkit browsers will reload the flash object after going to fullscreen.
2372   // To get around this, we're unloading the tech, caching source and currentTime values, and reloading the tech once the plugin is resized.
2373   // reloadTech: function(betweenFn){
2374   //   _V_.log("unloadingTech")
2375   //   this.unloadTech();
2376   //   _V_.log("unloadedTech")
2377   //   if (betweenFn) { betweenFn.call(); }
2378   //   _V_.log("LoadingTech")
2379   //   this.loadTech(this.techName, { src: this.values.src })
2380   //   _V_.log("loadedTech")
2381   // },
2382
2383   /* Fallbacks for unsupported event types
2384   ================================================================================ */
2385   // Manually trigger progress events based on changes to the buffered amount
2386   // Many flash players and older HTML5 browsers don't send progress or progress-like events
2387   manualProgressOn: function(){
2388     this.manualProgress = true;
2389
2390     // Trigger progress watching when a source begins loading
2391     this.trackProgress();
2392
2393     // Watch for a native progress event call on the tech element
2394     // In HTML5, some older versions don't support the progress event
2395     // So we're assuming they don't, and turning off manual progress if they do.
2396     this.tech.addEvent("progress", function(){
2397
2398       // Remove this listener from the element
2399       this.removeEvent("progress", arguments.callee);
2400
2401       // Update known progress support for this playback technology
2402       this.support.progressEvent = true;
2403
2404       // Turn off manual progress tracking
2405       this.player.manualProgressOff();
2406     });
2407   },
2408
2409   manualProgressOff: function(){
2410     this.manualProgress = false;
2411     this.stopTrackingProgress();
2412   },
2413
2414   trackProgress: function(){
2415     this.progressInterval = setInterval(_V_.proxy(this, function(){
2416       // Don't trigger unless buffered amount is greater than last time
2417       // log(this.values.bufferEnd, this.buffered().end(0), this.duration())
2418       /* TODO: update for multiple buffered regions */
2419       if (this.values.bufferEnd < this.buffered().end(0)) {
2420         this.triggerEvent("progress");
2421       } else if (this.bufferedPercent() == 1) {
2422         this.stopTrackingProgress();
2423         this.triggerEvent("progress"); // Last update
2424       }
2425     }), 500);
2426   },
2427   stopTrackingProgress: function(){ clearInterval(this.progressInterval); },
2428
2429   /* Time Tracking -------------------------------------------------------------- */
2430   manualTimeUpdatesOn: function(){
2431     this.manualTimeUpdates = true;
2432
2433     this.addEvent("play", this.trackCurrentTime);
2434     this.addEvent("pause", this.stopTrackingCurrentTime);
2435     // timeupdate is also called by .currentTime whenever current time is set
2436
2437     // Watch for native timeupdate event
2438     this.tech.addEvent("timeupdate", function(){
2439
2440       // Remove this listener from the element
2441       this.removeEvent("timeupdate", arguments.callee);
2442
2443       // Update known progress support for this playback technology
2444       this.support.timeupdateEvent = true;
2445
2446       // Turn off manual progress tracking
2447       this.player.manualTimeUpdatesOff();
2448     });
2449   },
2450
2451   manualTimeUpdatesOff: function(){
2452     this.manualTimeUpdates = false;
2453     this.stopTrackingCurrentTime();
2454     this.removeEvent("play", this.trackCurrentTime);
2455     this.removeEvent("pause", this.stopTrackingCurrentTime);
2456   },
2457
2458   trackCurrentTime: function(){
2459     if (this.currentTimeInterval) { this.stopTrackingCurrentTime(); }
2460     this.currentTimeInterval = setInterval(_V_.proxy(this, function(){
2461       this.triggerEvent("timeupdate");
2462     }), 250); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
2463   },
2464
2465   // Turn off play progress tracking (when paused or dragging)
2466   stopTrackingCurrentTime: function(){ clearInterval(this.currentTimeInterval); },
2467
2468   /* Player event handlers (how the player reacts to certain events)
2469   ================================================================================ */
2470   onEnded: function(){
2471     if (this.options.loop) {
2472       this.currentTime(0);
2473       this.play();
2474     } else {
2475       this.pause();
2476       this.currentTime(0);
2477       this.pause();
2478     }
2479   },
2480
2481   onPlay: function(){
2482     _V_.removeClass(this.el, "vjs-paused");
2483     _V_.addClass(this.el, "vjs-playing");
2484   },
2485
2486   onPause: function(){
2487     _V_.removeClass(this.el, "vjs-playing");
2488     _V_.addClass(this.el, "vjs-paused");
2489   },
2490
2491   onProgress: function(){
2492     // Add custom event for when source is finished downloading.
2493     if (this.bufferedPercent() == 1) {
2494       this.triggerEvent("loadedalldata");
2495     }
2496   },
2497
2498   onError: function(e) {
2499     _V_.log("Video Error", e);
2500   },
2501
2502 /* Player API
2503 ================================================================================ */
2504
2505   // Pass values to the playback tech
2506   techCall: function(method, arg){
2507
2508     // If it's not ready yet, call method when it is
2509     if (!this.tech.isReady) {
2510       this.tech.ready(function(){
2511         this[method](arg);
2512       });
2513
2514     // Otherwise call method now
2515     } else {
2516       try {
2517         this.tech[method](arg);
2518       } catch(e) {
2519         _V_.log(e);
2520       }
2521     }
2522   },
2523
2524   // Get calls can't wait for the tech, and sometimes don't need to.
2525   techGet: function(method){
2526
2527     // Make sure tech is ready
2528     if (this.tech.isReady) {
2529
2530       // Flash likes to die and reload when you hide or reposition it.
2531       // In these cases the object methods go away and we get errors.
2532       // When that happens we'll catch the errors and inform tech that it's not ready any more.
2533       try {
2534         return this.tech[method]();
2535       } catch(e) {
2536
2537         // When building additional tech libs, an expected method may not be defined yet
2538         if (this.tech[method] === undefined) {
2539           _V_.log("Video.js: " + method + " method not defined for "+this.techName+" playback technology.", e);
2540
2541         } else {
2542
2543           // When a method isn't available on the object it throws a TypeError
2544           if (e.name == "TypeError") {
2545             _V_.log("Video.js: " + method + " unavailable on "+this.techName+" playback technology element.", e);
2546             this.tech.isReady = false;
2547
2548           } else {
2549             _V_.log(e);
2550           }
2551         }
2552       }
2553     }
2554
2555     return;
2556   },
2557
2558   // Method for calling methods on the current playback technology
2559   // techCall: function(method, arg){
2560   // 
2561   //   // if (this.isReady) {
2562   //   //   
2563   //   // } else {
2564   //   //   _V_.log("The playback technology API is not ready yet. Use player.ready(myFunction)."+" ["+method+"]", arguments.callee.caller.arguments.callee.caller.arguments.callee.caller)
2565   //   //   return false;
2566   //   //   // throw new Error("The playback technology API is not ready yet. Use player.ready(myFunction)."+" ["+method+"]");
2567   //   // }
2568   // },
2569
2570   // http://dev.w3.org/html5/spec/video.html#dom-media-play
2571   play: function(){
2572     this.techCall("play");
2573     return this;
2574   },
2575
2576   // http://dev.w3.org/html5/spec/video.html#dom-media-pause
2577   pause: function(){
2578     this.techCall("pause");
2579     return this;
2580   },
2581   
2582   // http://dev.w3.org/html5/spec/video.html#dom-media-paused
2583   // The initial state of paused should be true (in Safari it's actually false)
2584   paused: function(){
2585     return (this.techGet("paused") === false) ? false : true;
2586   },
2587
2588   // http://dev.w3.org/html5/spec/video.html#dom-media-currenttime
2589   currentTime: function(seconds){
2590     if (seconds !== undefined) {
2591
2592       // Cache the last set value for smoother scrubbing.
2593       this.values.lastSetCurrentTime = seconds;
2594
2595       this.techCall("setCurrentTime", seconds);
2596
2597       // Improve the accuracy of manual timeupdates
2598       if (this.manualTimeUpdates) { this.triggerEvent("timeupdate"); }
2599
2600       return this;
2601     }
2602
2603     // Cache last currentTime and return
2604     // Default to 0 seconds
2605     return this.values.currentTime = (this.techGet("currentTime") || 0);
2606   },
2607
2608   // http://dev.w3.org/html5/spec/video.html#dom-media-duration
2609   // Duration should return NaN if not available. ParseFloat will turn false-ish values to NaN.
2610   duration: function(){
2611     return parseFloat(this.techGet("duration"));
2612   },
2613
2614   // Calculates how much time is left. Not in spec, but useful.
2615   remainingTime: function(){
2616     return this.duration() - this.currentTime();
2617   },
2618
2619   // http://dev.w3.org/html5/spec/video.html#dom-media-buffered
2620   // Buffered returns a timerange object. Kind of like an array of portions of the video that have been downloaded.
2621   // So far no browsers return more than one range (portion)
2622   buffered: function(){
2623     var buffered = this.techGet("buffered"),
2624         start = 0,
2625         end = this.values.bufferEnd = this.values.bufferEnd || 0, // Default end to 0 and store in values
2626         timeRange;
2627
2628     if (buffered && buffered.length > 0 && buffered.end(0) !== end) {
2629       end = buffered.end(0);
2630       // Storing values allows them be overridden by setBufferedFromProgress
2631       this.values.bufferEnd = end;
2632     }
2633
2634     return _V_.createTimeRange(start, end);
2635   },
2636
2637   // Calculates amount of buffer is full. Not in spec but useful.
2638   bufferedPercent: function(){
2639     return (this.duration()) ? this.buffered().end(0) / this.duration() : 0;
2640   },
2641
2642   // http://dev.w3.org/html5/spec/video.html#dom-media-volume
2643   volume: function(percentAsDecimal){
2644     var vol;
2645
2646     if (percentAsDecimal !== undefined) {
2647       vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))); // Force value to between 0 and 1
2648       this.values.volume = vol;
2649       this.techCall("setVolume", vol);
2650       _V_.setLocalStorage("volume", vol);
2651       return this;
2652     }
2653
2654     // Default to 1 when returning current volume.
2655     vol = parseFloat(this.techGet("volume"));
2656     return (isNaN(vol)) ? 1 : vol;
2657   },
2658
2659   // http://dev.w3.org/html5/spec/video.html#attr-media-muted
2660   muted: function(muted){
2661     if (muted !== undefined) {
2662       this.techCall("setMuted", muted);
2663       return this;
2664     }
2665     return this.techGet("muted") || false; // Default to false
2666   },
2667
2668   // http://dev.w3.org/html5/spec/dimension-attributes.html#attr-dim-height
2669   // Video tag width/height only work in pixels. No percents.
2670   // We could potentially allow percents but won't for now until we can do testing around it.
2671   width: function(width, skipListeners){
2672     if (width !== undefined) {
2673       this.el.width = width;
2674       this.el.style.width = width+"px";
2675
2676       // skipListeners allows us to avoid triggering the resize event when setting both width and height
2677       if (!skipListeners) { this.triggerEvent("resize"); }
2678       return this;
2679     }
2680     return parseInt(this.el.getAttribute("width"));
2681   },
2682   height: function(height){
2683     if (height !== undefined) {
2684       this.el.height = height;
2685       this.el.style.height = height+"px";
2686       this.triggerEvent("resize");
2687       return this;
2688     }
2689     return parseInt(this.el.getAttribute("height"));
2690   },
2691   // Set both width and height at the same time.
2692   size: function(width, height){
2693     // Skip resize listeners on width for optimization
2694     return this.width(width, true).height(height);
2695   },
2696
2697   // Check if current tech can support native fullscreen (e.g. with built in controls lik iOS, so not our flash swf)
2698   supportsFullScreen: function(){ return this.techGet("supportsFullScreen") || false; },
2699
2700   // Turn on fullscreen (or window) mode
2701   requestFullScreen: function(){
2702     var requestFullScreen = _V_.support.requestFullScreen;
2703
2704     this.isFullScreen = true;
2705
2706     // Check for browser element fullscreen support
2707     if (requestFullScreen) {
2708
2709       // Trigger fullscreenchange event after change
2710       _V_.addEvent(document, requestFullScreen.eventName, this.proxy(function(){
2711         this.isFullScreen = document[requestFullScreen.isFullScreen];
2712
2713         // If cancelling fullscreen, remove event listener.
2714         if (this.isFullScreen == false) {
2715           _V_.removeEvent(document, requestFullScreen.eventName, arguments.callee);
2716         }
2717
2718         this.triggerEvent("fullscreenchange");
2719       }));
2720
2721       // Flash and other plugins get reloaded when you take their parent to fullscreen.
2722       // To fix that we'll remove the tech, and reload it after the resize has finished.
2723       if (this.tech.support.fullscreenResize === false && this.options.flash.iFrameMode != true) {
2724
2725         this.pause();
2726         this.unloadTech();
2727
2728         _V_.addEvent(document, requestFullScreen.eventName, this.proxy(function(){
2729           _V_.removeEvent(document, requestFullScreen.eventName, arguments.callee);
2730           this.loadTech(this.techName, { src: this.values.src });
2731         }));
2732
2733         this.el[requestFullScreen.requestFn]();
2734
2735       } else {
2736         this.el[requestFullScreen.requestFn]();
2737       }
2738
2739     } else if (this.tech.supportsFullScreen()) {
2740       this.triggerEvent("fullscreenchange");
2741       this.techCall("enterFullScreen");
2742
2743     } else {
2744       this.triggerEvent("fullscreenchange");
2745       this.enterFullWindow();
2746     }
2747
2748      return this;
2749    },
2750
2751    cancelFullScreen: function(){
2752     var requestFullScreen = _V_.support.requestFullScreen;
2753
2754     this.isFullScreen = false;
2755
2756     // Check for browser element fullscreen support
2757     if (requestFullScreen) {
2758
2759      // Flash and other plugins get reloaded when you take their parent to fullscreen.
2760      // To fix that we'll remove the tech, and reload it after the resize has finished.
2761      if (this.tech.support.fullscreenResize === false && this.options.flash.iFrameMode != true) {
2762
2763        this.pause();
2764        this.unloadTech();
2765
2766        _V_.addEvent(document, requestFullScreen.eventName, this.proxy(function(){
2767          _V_.removeEvent(document, requestFullScreen.eventName, arguments.callee);
2768          this.loadTech(this.techName, { src: this.values.src })
2769        }));
2770
2771        document[requestFullScreen.cancelFn]();
2772
2773      } else {
2774        document[requestFullScreen.cancelFn]();
2775      }
2776
2777     } else if (this.tech.supportsFullScreen()) {
2778      this.techCall("exitFullScreen");
2779      this.triggerEvent("fullscreenchange");
2780
2781     } else {
2782      this.exitFullWindow();
2783      this.triggerEvent("fullscreenchange");
2784     }
2785
2786     return this;
2787   },
2788
2789   // When fullscreen isn't supported we can stretch the video container to as wide as the browser will let us.
2790   enterFullWindow: function(){
2791     this.isFullWindow = true;
2792
2793     // Storing original doc overflow value to return to when fullscreen is off
2794     this.docOrigOverflow = document.documentElement.style.overflow;
2795
2796     // Add listener for esc key to exit fullscreen
2797     _V_.addEvent(document, "keydown", _V_.proxy(this, this.fullWindowOnEscKey));
2798
2799     // Hide any scroll bars
2800     document.documentElement.style.overflow = 'hidden';
2801
2802     // Apply fullscreen styles
2803     _V_.addClass(document.body, "vjs-full-window");
2804     _V_.addClass(this.el, "vjs-fullscreen");
2805
2806     this.triggerEvent("enterFullWindow");
2807   },
2808   fullWindowOnEscKey: function(event){
2809     if (event.keyCode == 27) {
2810       if (this.isFullScreen == true) {
2811         this.cancelFullScreen();
2812       } else {
2813         this.exitFullWindow();
2814       }
2815     }
2816   },
2817
2818   exitFullWindow: function(){
2819     this.isFullWindow = false;
2820     _V_.removeEvent(document, "keydown", this.fullWindowOnEscKey);
2821
2822     // Unhide scroll bars.
2823     document.documentElement.style.overflow = this.docOrigOverflow;
2824
2825     // Remove fullscreen styles
2826     _V_.removeClass(document.body, "vjs-full-window");
2827     _V_.removeClass(this.el, "vjs-fullscreen");
2828
2829     // Resize the box, controller, and poster to original sizes
2830     // this.positionAll();
2831     this.triggerEvent("exitFullWindow");
2832   },
2833
2834   selectSource: function(sources){
2835
2836     // Loop through each playback technology in the options order
2837     for (var i=0,j=this.options.techOrder;i<j.length;i++) {
2838       var techName = j[i],
2839           tech = _V_[techName];
2840           // tech = _V_.tech[techName];
2841
2842       // Check if the browser supports this technology
2843       if (tech.isSupported()) {
2844
2845         // Loop through each source object
2846         for (var a=0,b=sources;a<b.length;a++) {
2847           var source = b[a];
2848
2849           // Check if source can be played with this technology
2850           if (tech.canPlaySource.call(this, source)) {
2851
2852             return { source: source, tech: techName };
2853
2854           }
2855         }
2856       }
2857     }
2858
2859     return false;
2860   },
2861
2862   // src is a pretty powerful function
2863   // If you pass it an array of source objects, it will find the best source to play and use that object.src
2864   //   If the new source requires a new playback technology, it will switch to that.
2865   // If you pass it an object, it will set the source to object.src
2866   // If you pass it anything else (url string) it will set the video source to that
2867   src: function(source){
2868     // Case: Array of source objects to choose from and pick the best to play
2869     if (source instanceof Array) {
2870
2871       var sourceTech = this.selectSource(source),
2872           source,
2873           techName;
2874
2875       if (sourceTech) {
2876           source = sourceTech.source;
2877           techName = sourceTech.tech;
2878
2879         // If this technology is already loaded, set source
2880         if (techName == this.techName) {
2881           this.src(source); // Passing the source object
2882
2883         // Otherwise load this technology with chosen source
2884         } else {
2885           this.loadTech(techName, source);
2886         }
2887       } else {
2888         _V_.log("No compatible source and playback technology were found.")
2889       }
2890
2891     // Case: Source object { src: "", type: "" ... }
2892     } else if (source instanceof Object) {
2893
2894       if (_V_[this.techName].canPlaySource(source)) {
2895         this.src(source.src);
2896       } else {
2897         // Send through tech loop to check for a compatible technology.
2898         this.src([source]);
2899       }
2900
2901     // Case: URL String (http://myvideo...)
2902     } else {
2903       // Cache for getting last set source
2904       this.values.src = source;
2905
2906       if (!this.isReady) {
2907         this.ready(function(){
2908           this.src(source);
2909         });
2910       } else {
2911         this.techCall("src", source);
2912         if (this.options.preload == "auto") {
2913           this.load();
2914         }
2915         if (this.options.autoplay) {
2916           this.play();
2917         }
2918       }
2919     }
2920     return this;
2921   },
2922
2923   // Begin loading the src data
2924   // http://dev.w3.org/html5/spec/video.html#dom-media-load
2925   load: function(){
2926     this.techCall("load");
2927     return this;
2928   },
2929
2930   // http://dev.w3.org/html5/spec/video.html#dom-media-currentsrc
2931   currentSrc: function(){
2932     return this.techGet("currentSrc") || this.values.src || "";
2933   },
2934
2935   // Attributes/Options
2936   preload: function(value){
2937     if (value !== undefined) {
2938       this.techCall("setPreload", value);
2939       this.options.preload = value;
2940       return this;
2941     }
2942     return this.techGet("preload");
2943   },
2944   autoplay: function(value){
2945     if (value !== undefined) {
2946       this.techCall("setAutoplay", value);
2947       this.options.autoplay = value;
2948       return this;
2949     }
2950     return this.techGet("autoplay", value);
2951   },
2952   loop: function(value){
2953     if (value !== undefined) {
2954       this.techCall("setLoop", value);
2955       this.options.loop = value;
2956       return this;
2957     }
2958     return this.techGet("loop");
2959   },
2960
2961   controls: function(){ return this.options.controls; },
2962   poster: function(){ return this.techGet("poster"); },
2963   error: function(){ return this.techGet("error"); },
2964   ended: function(){ return this.techGet("ended"); }
2965
2966   // Methods to add support for
2967   // networkState: function(){ return this.techCall("networkState"); },
2968   // readyState: function(){ return this.techCall("readyState"); },
2969   // seeking: function(){ return this.techCall("seeking"); },
2970   // initialTime: function(){ return this.techCall("initialTime"); },
2971   // startOffsetTime: function(){ return this.techCall("startOffsetTime"); },
2972   // played: function(){ return this.techCall("played"); },
2973   // seekable: function(){ return this.techCall("seekable"); },
2974   // videoTracks: function(){ return this.techCall("videoTracks"); },
2975   // audioTracks: function(){ return this.techCall("audioTracks"); },
2976   // videoWidth: function(){ return this.techCall("videoWidth"); },
2977   // videoHeight: function(){ return this.techCall("videoHeight"); },
2978   // defaultPlaybackRate: function(){ return this.techCall("defaultPlaybackRate"); },
2979   // playbackRate: function(){ return this.techCall("playbackRate"); },
2980   // mediaGroup: function(){ return this.techCall("mediaGroup"); },
2981   // controller: function(){ return this.techCall("controller"); },
2982   // defaultMuted: function(){ return this.techCall("defaultMuted"); }
2983 });
2984
2985 // RequestFullscreen API
2986 (function(){
2987   var requestFn,
2988       cancelFn,
2989       eventName,
2990       isFullScreen,
2991       playerProto = _V_.Player.prototype;
2992
2993   // Current W3C Spec
2994   // http://dvcs.w3.org/hg/fullscreen/raw-file/tip/Overview.html#api
2995   // Mozilla Draft: https://wiki.mozilla.org/Gecko:FullScreenAPI#fullscreenchange_event
2996   if (document.cancelFullscreen !== undefined) {
2997     requestFn = "requestFullscreen";
2998     cancelFn = "exitFullscreen";
2999     eventName = "fullscreenchange";
3000     isFullScreen = "fullScreen";
3001
3002   // Webkit (Chrome/Safari) and Mozilla (Firefox) have working implementaitons
3003   // that use prefixes and vary slightly from the new W3C spec. Specifically, using 'exit' instead of 'cancel',
3004   // and lowercasing the 'S' in Fullscreen.
3005   // Other browsers don't have any hints of which version they might follow yet, so not going to try to predict by loopeing through all prefixes.
3006   } else {
3007
3008     _V_.each(["moz", "webkit"], function(prefix){
3009
3010       // https://github.com/zencoder/video-js/pull/128
3011       if ((prefix != "moz" || document.mozFullScreenEnabled) && document[prefix + "CancelFullScreen"] !== undefined) {
3012         requestFn = prefix + "RequestFullScreen";
3013         cancelFn = prefix + "CancelFullScreen";
3014         eventName = prefix + "fullscreenchange";
3015
3016         if (prefix == "webkit") {
3017           isFullScreen = prefix + "IsFullScreen";
3018         } else {
3019           isFullScreen = prefix + "FullScreen";
3020         }
3021       }
3022
3023     });
3024
3025   }
3026
3027   if (requestFn) {
3028     _V_.support.requestFullScreen = {
3029       requestFn: requestFn,
3030       cancelFn: cancelFn,
3031       eventName: eventName,
3032       isFullScreen: isFullScreen
3033     };
3034   }
3035
3036 })();/* Playback Technology - Base class for playback technologies
3037 ================================================================================ */
3038 _V_.PlaybackTech = _V_.Component.extend({
3039   init: function(player, options){
3040     // this._super(player, options);
3041
3042     // Make playback element clickable
3043     // _V_.addEvent(this.el, "click", _V_.proxy(this, _V_.PlayToggle.prototype.onClick));
3044
3045     // this.addEvent("click", this.proxy(this.onClick));
3046
3047     // player.triggerEvent("techready");
3048   },
3049   // destroy: function(){},
3050   // createElement: function(){},
3051   onClick: function(){
3052     if (this.player.options.controls) {
3053       _V_.PlayToggle.prototype.onClick.call(this);
3054     }
3055   }
3056 });
3057
3058 // Create placeholder methods for each that warn when a method isn't supported by the current playback technology
3059 _V_.apiMethods = "play,pause,paused,currentTime,setCurrentTime,duration,buffered,volume,setVolume,muted,setMuted,width,height,supportsFullScreen,enterFullScreen,src,load,currentSrc,preload,setPreload,autoplay,setAutoplay,loop,setLoop,error,networkState,readyState,seeking,initialTime,startOffsetTime,played,seekable,ended,videoTracks,audioTracks,videoWidth,videoHeight,textTracks,defaultPlaybackRate,playbackRate,mediaGroup,controller,controls,defaultMuted".split(",");
3060 _V_.each(_V_.apiMethods, function(methodName){
3061   _V_.PlaybackTech.prototype[methodName] = function(){
3062     throw new Error("The '"+methodName+"' method is not available on the playback technology's API");
3063   }
3064 });
3065
3066 /* HTML5 Playback Technology - Wrapper for HTML5 Media API
3067 ================================================================================ */
3068 _V_.html5 = _V_.PlaybackTech.extend({
3069
3070   init: function(player, options, ready){
3071     this.player = player;
3072     this.el = this.createElement();
3073     this.ready(ready);
3074
3075     this.addEvent("click", this.proxy(this.onClick));
3076
3077     var source = options.source;
3078
3079     // If the element source is already set, we may have missed the loadstart event, and want to trigger it.
3080     // We don't want to set the source again and interrupt playback.
3081     if (source && this.el.currentSrc == source.src) {
3082       player.triggerEvent("loadstart");
3083
3084     // Otherwise set the source if one was provided.
3085     } else if (source) {
3086       this.el.src = source.src;
3087     }
3088
3089     // Chrome and Safari both have issues with autoplay.
3090     // In Safari (5.1.1), when we move the video element into the container div, autoplay doesn't work.
3091     // In Chrome (15), if you have autoplay + a poster + no controls, the video gets hidden (but audio plays)
3092     // This fixes both issues. Need to wait for API, so it updates displays correctly
3093     player.ready(function(){
3094       if (this.options.autoplay && this.paused()) {
3095         this.tag.poster = null; // Chrome Fix. Fixed in Chrome v16.
3096         this.play();
3097       }
3098     });
3099
3100     this.setupTriggers();
3101
3102     this.triggerReady();
3103   },
3104
3105   destroy: function(){
3106     this.player.tag = false;
3107     this.removeTriggers();
3108     this.el.parentNode.removeChild(this.el);
3109   },
3110
3111   createElement: function(){
3112     var html5 = _V_.html5,
3113         player = this.player,
3114
3115         // If possible, reuse original tag for HTML5 playback technology element
3116         el = player.tag,
3117         newEl;
3118
3119     // Check if this browser supports moving the element into the box.
3120     // On the iPhone video will break if you move the element,
3121     // So we have to create a brand new element.
3122     if (!el || this.support.movingElementInDOM === false) {
3123
3124       // If the original tag is still there, remove it.
3125       if (el) {
3126         player.el.removeChild(el);
3127       }
3128
3129       newEl = _V_.createElement("video", {
3130         id: el.id || player.el.id + "_html5_api",
3131         className: el.className || "vjs-tech"
3132       });
3133
3134       el = newEl;
3135       _V_.insertFirst(el, player.el);
3136     }
3137
3138     // Update tag settings, in case they were overridden
3139     _V_.each(["autoplay","preload","loop","muted"], function(attr){ // ,"poster"
3140       if (player.options[attr] !== null) {
3141         el[attr] = player.options[attr];
3142       }
3143     }, this);
3144
3145     return el;
3146   },
3147
3148   // Make video events trigger player events
3149   // May seem verbose here, but makes other APIs possible.
3150   setupTriggers: function(){
3151     _V_.each.call(this, _V_.html5.events, function(type){
3152       _V_.addEvent(this.el, type, _V_.proxy(this.player, this.eventHandler));
3153     });
3154   },
3155   removeTriggers: function(){
3156     _V_.each.call(this, _V_.html5.events, function(type){
3157       _V_.removeEvent(this.el, type, _V_.proxy(this.player, this.eventHandler));
3158     });
3159   },
3160   eventHandler: function(e){
3161     e.stopPropagation();
3162     this.triggerEvent(e);
3163   },
3164
3165   play: function(){ this.el.play(); },
3166   pause: function(){ this.el.pause(); },
3167   paused: function(){ return this.el.paused; },
3168
3169   currentTime: function(){ return this.el.currentTime; },
3170   setCurrentTime: function(seconds){
3171     try {
3172       this.el.currentTime = seconds;
3173       } catch(e) {
3174         _V_.log(e, "Video isn't ready. (VideoJS)");
3175       // this.warning(VideoJS.warnings.videoNotReady);
3176     }
3177   },
3178
3179   duration: function(){ return this.el.duration || 0; },
3180   buffered: function(){ return this.el.buffered; },
3181
3182   volume: function(){ return this.el.volume; },
3183   setVolume: function(percentAsDecimal){ this.el.volume = percentAsDecimal; },
3184   muted: function(){ return this.el.muted; },
3185   setMuted: function(muted){ this.el.muted = muted },
3186
3187   width: function(){ return this.el.offsetWidth; },
3188   height: function(){ return this.el.offsetHeight; },
3189
3190   supportsFullScreen: function(){
3191     if (typeof this.el.webkitEnterFullScreen == 'function') {
3192
3193       // Seems to be broken in Chromium/Chrome && Safari in Leopard
3194       if (!navigator.userAgent.match("Chrome") && !navigator.userAgent.match("Mac OS X 10.5")) {
3195         return true;
3196       }
3197     }
3198     return false;
3199   },
3200
3201   enterFullScreen: function(){
3202       try {
3203         this.el.webkitEnterFullScreen();
3204       } catch (e) {
3205         if (e.code == 11) {
3206           // this.warning(VideoJS.warnings.videoNotReady);
3207           _V_.log("VideoJS: Video not ready.")
3208         }
3209       }
3210   },
3211   src: function(src){ this.el.src = src; },
3212   load: function(){ this.el.load(); },
3213   currentSrc: function(){ return this.el.currentSrc; },
3214
3215   preload: function(){ return this.el.preload; },
3216   setPreload: function(val){ this.el.preload = val; },
3217   autoplay: function(){ return this.el.autoplay; },
3218   setAutoplay: function(val){ this.el.autoplay = val; },
3219   loop: function(){ return this.el.loop; },
3220   setLoop: function(val){ this.el.loop = val; },
3221
3222   error: function(){ return this.el.error; },
3223   // networkState: function(){ return this.el.networkState; },
3224   // readyState: function(){ return this.el.readyState; },
3225   seeking: function(){ return this.el.seeking; },
3226   // initialTime: function(){ return this.el.initialTime; },
3227   // startOffsetTime: function(){ return this.el.startOffsetTime; },
3228   // played: function(){ return this.el.played; },
3229   // seekable: function(){ return this.el.seekable; },
3230   ended: function(){ return this.el.ended; },
3231   // videoTracks: function(){ return this.el.videoTracks; },
3232   // audioTracks: function(){ return this.el.audioTracks; },
3233   // videoWidth: function(){ return this.el.videoWidth; },
3234   // videoHeight: function(){ return this.el.videoHeight; },
3235   // textTracks: function(){ return this.el.textTracks; },
3236   // defaultPlaybackRate: function(){ return this.el.defaultPlaybackRate; },
3237   // playbackRate: function(){ return this.el.playbackRate; },
3238   // mediaGroup: function(){ return this.el.mediaGroup; },
3239   // controller: function(){ return this.el.controller; },
3240   controls: function(){ return this.player.options.controls; },
3241   defaultMuted: function(){ return this.el.defaultMuted; }
3242 });
3243
3244 /* HTML5 Support Testing -------------------------------------------------------- */
3245
3246 _V_.html5.isSupported = function(){
3247   return !!document.createElement("video").canPlayType;
3248 };
3249
3250 _V_.html5.canPlaySource = function(srcObj){
3251   return !!document.createElement("video").canPlayType(srcObj.type);
3252   // TODO: Check Type
3253   // If no Type, check ext
3254   // Check Media Type
3255 };
3256
3257 // List of all HTML5 events (various uses).
3258 _V_.html5.events = "loadstart,suspend,abort,error,emptied,stalled,loadedmetadata,loadeddata,canplay,canplaythrough,playing,waiting,seeking,seeked,ended,durationchange,timeupdate,progress,play,pause,ratechange,volumechange".split(",");
3259
3260 /* HTML5 Device Fixes ---------------------------------------------------------- */
3261
3262 _V_.html5.prototype.support = {
3263
3264   // Support for tech specific full screen. (webkitEnterFullScreen, not requestFullscreen)
3265   // http://developer.apple.com/library/safari/#documentation/AudioVideo/Reference/HTMLVideoElementClassReference/HTMLVideoElement/HTMLVideoElement.html
3266   // Seems to be broken in Chromium/Chrome && Safari in Leopard
3267   fullscreen: (typeof _V_.testVid.webkitEnterFullScreen !== undefined) ? (!_V_.ua.match("Chrome") && !_V_.ua.match("Mac OS X 10.5") ? true : false) : false,
3268
3269   // In iOS, if you move a video element in the DOM, it breaks video playback.
3270   movingElementInDOM: !_V_.isIOS()
3271
3272 };
3273
3274 // Android
3275 if (_V_.isAndroid()) {
3276
3277   // Override Android 2.2 and less canPlayType method which is broken
3278   if (_V_.androidVersion() < 3) {
3279     document.createElement("video").constructor.prototype.canPlayType = function(type){
3280       return (type && type.toLowerCase().indexOf("video/mp4") != -1) ? "maybe" : "";
3281     };
3282   }
3283 }
3284
3285
3286 /* VideoJS-SWF - Custom Flash Player with HTML5-ish API - https://github.com/zencoder/video-js-swf
3287 ================================================================================ */
3288 _V_.flash = _V_.PlaybackTech.extend({
3289
3290   init: function(player, options){
3291     this.player = player;
3292
3293     var source = options.source,
3294
3295         // Which element to embed in
3296         parentEl = options.parentEl,
3297
3298         // Create a temporary element to be replaced by swf object
3299         placeHolder = this.el = _V_.createElement("div", { id: parentEl.id + "_temp_flash" }),
3300
3301         // Generate ID for swf object
3302         objId = player.el.id+"_flash_api",
3303
3304         // Store player options in local var for optimization
3305         playerOptions = player.options,
3306
3307         // Merge default flashvars with ones passed in to init
3308         flashVars = _V_.merge({
3309
3310           // SWF Callback Functions
3311           readyFunction: "_V_.flash.onReady",
3312           eventProxyFunction: "_V_.flash.onEvent",
3313           errorEventProxyFunction: "_V_.flash.onError",
3314
3315           // Player Settings
3316           autoplay: playerOptions.autoplay,
3317           preload: playerOptions.preload,
3318           loop: playerOptions.loop,
3319           muted: playerOptions.muted
3320
3321         }, options.flashVars),
3322
3323         // Merge default parames with ones passed in
3324         params = _V_.merge({
3325           wmode: "opaque", // Opaque is needed to overlay controls, but can affect playback performance
3326           bgcolor: "#000000" // Using bgcolor prevents a white flash when the object is loading
3327         }, options.params),
3328
3329         // Merge default attributes with ones passed in
3330         attributes = _V_.merge({
3331           id: objId,
3332           name: objId, // Both ID and Name needed or swf to identifty itself
3333           'class': 'vjs-tech'
3334         }, options.attributes)
3335     ;
3336
3337     // If source was supplied pass as a flash var.
3338     if (source) {
3339       flashVars.src = encodeURIComponent(_V_.getAbsoluteURL(source.src));
3340     }
3341
3342     // Add placeholder to player div
3343     _V_.insertFirst(placeHolder, parentEl);
3344
3345     // Having issues with Flash reloading on certain page actions (hide/resize/fullscreen) in certain browsers
3346     // This allows resetting the playhead when we catch the reload
3347     if (options.startTime) {
3348       this.ready(function(){
3349         this.load();
3350         this.play();
3351         this.currentTime(options.startTime);
3352       });
3353     }
3354
3355     // Flash iFrame Mode
3356     // In web browsers there are multiple instances where changing the parent element or visibility of a plugin causes the plugin to reload.
3357     // - Firefox just about always. https://bugzilla.mozilla.org/show_bug.cgi?id=90268 (might be fixed by version 13)
3358     // - Webkit when hiding the plugin
3359     // - Webkit and Firefox when using requestFullScreen on a parent element
3360     // Loading the flash plugin into a dynamically generated iFrame gets around most of these issues.
3361     // Issues that remain include hiding the element and requestFullScreen in Firefox specifically
3362
3363     // There's on particularly annoying issue with this method which is that Firefox throws a security error on an offsite Flash object loaded into a dynamically created iFrame.
3364     // Even though the iframe was inserted into a page on the web, Firefox + Flash considers it a local app trying to access an internet file.
3365     // I tried mulitple ways of setting the iframe src attribute but couldn't find a src that worked well. Tried a real/fake source, in/out of domain.
3366     // Also tried a method from stackoverflow that caused a security error in all browsers. http://stackoverflow.com/questions/2486901/how-to-set-document-domain-for-a-dynamically-generated-iframe
3367     // In the end the solution I found to work was setting the iframe window.location.href right before doing a document.write of the Flash object.
3368     // The only downside of this it seems to trigger another http request to the original page (no matter what's put in the href). Not sure why that is.
3369
3370     // NOTE (2012-01-29): Cannot get Firefox to load the remote hosted SWF into a dynamically created iFrame
3371     // Firefox 9 throws a security error, unleess you call location.href right before doc.write.
3372     //    Not sure why that even works, but it causes the browser to look like it's continuously trying to load the page.
3373     // Firefox 3.6 keeps calling the iframe onload function anytime I write to it, causing an endless loop.
3374
3375     if (options.iFrameMode == true && !_V_.isFF) {
3376
3377       // Create iFrame with vjs-tech class so it's 100% width/height
3378       var iFrm = _V_.createElement("iframe", {
3379         id: objId + "_iframe",
3380         name: objId + "_iframe",
3381         className: "vjs-tech",
3382         scrolling: "no",
3383         marginWidth: 0,
3384         marginHeight: 0,
3385         frameBorder: 0
3386       });
3387
3388       // Update ready function names in flash vars for iframe window
3389       flashVars.readyFunction = "ready";
3390       flashVars.eventProxyFunction = "events";
3391       flashVars.errorEventProxyFunction = "errors";
3392
3393       // Tried multiple methods to get this to work in all browsers
3394
3395       // Tried embedding the flash object in the page first, and then adding a place holder to the iframe, then replacing the placeholder with the page object.
3396       // The goal here was to try to load the swf URL in the parent page first and hope that got around the firefox security error
3397       // var newObj = _V_.flash.embed(options.swf, placeHolder, flashVars, params, attributes);
3398       // (in onload)
3399       //  var temp = _V_.createElement("a", { id:"asdf", innerHTML: "asdf" } );
3400       //  iDoc.body.appendChild(temp);
3401
3402       // Tried embedding the flash object through javascript in the iframe source.
3403       // This works in webkit but still triggers the firefox security error
3404       // iFrm.src = "javascript: document.write('"+_V_.flash.getEmbedCode(options.swf, flashVars, params, attributes)+"');";
3405
3406       // Tried an actual local iframe just to make sure that works, but it kills the easiness of the CDN version if you require the user to host an iframe
3407       // We should add an option to host the iframe locally though, because it could help a lot of issues.
3408       // iFrm.src = "iframe.html";
3409
3410       // Wait until iFrame has loaded to write into it.
3411       _V_.addEvent(iFrm, "load", _V_.proxy(this, function(){
3412
3413         var iDoc, objTag, swfLoc,
3414             iWin = iFrm.contentWindow,
3415             varString = "";
3416
3417
3418         // The one working method I found was to use the iframe's document.write() to create the swf object
3419         // This got around the security issue in all browsers except firefox.
3420         // I did find a hack where if I call the iframe's window.location.href="", it would get around the security error
3421         // However, the main page would look like it was loading indefinitely (URL bar loading spinner would never stop)
3422         // Plus Firefox 3.6 didn't work no matter what I tried.
3423         // if (_V_.ua.match("Firefox")) {
3424         //   iWin.location.href = "";
3425         // }
3426
3427         // Get the iFrame's document depending on what the browser supports
3428         iDoc = iFrm.contentDocument ? iFrm.contentDocument : iFrm.contentWindow.document;
3429
3430         // Tried ensuring both document domains were the same, but they already were, so that wasn't the issue.
3431         // Even tried adding /. that was mentioned in a browser security writeup
3432         // document.domain = document.domain+"/.";
3433         // iDoc.domain = document.domain+"/.";
3434
3435         // Tried adding the object to the iframe doc's innerHTML. Security error in all browsers.
3436         // iDoc.body.innerHTML = swfObjectHTML;
3437
3438         // Tried appending the object to the iframe doc's body. Security error in all browsers.
3439         // iDoc.body.appendChild(swfObject);
3440
3441         // Using document.write actually got around the security error that browsers were throwing.
3442         // Again, it's a dynamically generated (same domain) iframe, loading an external Flash swf.
3443         // Not sure why that's a security issue, but apparently it is.
3444         iDoc.write(_V_.flash.getEmbedCode(options.swf, flashVars, params, attributes));
3445
3446         // Setting variables on the window needs to come after the doc write because otherwise they can get reset in some browsers
3447         // So far no issues with swf ready event being called before it's set on the window.
3448         iWin.player = this.player;
3449
3450         // Create swf ready function for iFrame window
3451         iWin.ready = _V_.proxy(this.player, function(currSwf){
3452           var el = iDoc.getElementById(currSwf),
3453               player = this,
3454               tech = player.tech;
3455
3456           // Update reference to playback technology element
3457           tech.el = el;
3458
3459           // Now that the element is ready, make a click on the swf play the video
3460           _V_.addEvent(el, "click", tech.proxy(tech.onClick));
3461
3462           // Make sure swf is actually ready. Sometimes the API isn't actually yet.
3463           _V_.flash.checkReady(tech);
3464         });
3465
3466         // Create event listener for all swf events
3467         iWin.events = _V_.proxy(this.player, function(swfID, eventName, other){
3468           var player = this;
3469           if (player && player.techName == "flash") {
3470             player.triggerEvent(eventName);
3471           }
3472         });
3473
3474         // Create error listener for all swf errors
3475         iWin.errors = _V_.proxy(this.player, function(swfID, eventName){
3476           _V_.log("Flash Error", eventName);
3477         });
3478
3479       }));
3480
3481       // Replace placeholder with iFrame (it will load now)
3482       placeHolder.parentNode.replaceChild(iFrm, placeHolder);
3483
3484     // If not using iFrame mode, embed as normal object
3485     } else {
3486       _V_.flash.embed(options.swf, placeHolder, flashVars, params, attributes);
3487     }
3488   },
3489
3490   destroy: function(){
3491     this.el.parentNode.removeChild(this.el);
3492   },
3493
3494   // setupTriggers: function(){}, // Using global onEvent func to distribute events
3495
3496   play: function(){ this.el.vjs_play(); },
3497   pause: function(){ this.el.vjs_pause(); },
3498   src: function(src){
3499     // Make sure source URL is abosolute.
3500     src = _V_.getAbsoluteURL(src);
3501
3502     this.el.vjs_src(src);
3503
3504     // Currently the SWF doesn't autoplay if you load a source later.
3505     // e.g. Load player w/ no source, wait 2s, set src.
3506     if (this.player.autoplay()) {
3507       var tech = this;
3508       setTimeout(function(){ tech.play(); }, 0);
3509     }
3510   },
3511   load: function(){ this.el.vjs_load(); },
3512   poster: function(){ this.el.vjs_getProperty("poster"); },
3513
3514   buffered: function(){
3515     return _V_.createTimeRange(0, this.el.vjs_getProperty("buffered"));
3516   },
3517
3518   supportsFullScreen: function(){
3519     return false; // Flash does not allow fullscreen through javascript
3520   },
3521   enterFullScreen: function(){
3522     return false;
3523   }
3524 });
3525
3526 // Create setters and getters for attributes
3527 (function(){
3528
3529   var api = _V_.flash.prototype,
3530       readWrite = "preload,currentTime,defaultPlaybackRate,playbackRate,autoplay,loop,mediaGroup,controller,controls,volume,muted,defaultMuted".split(","),
3531       readOnly = "error,currentSrc,networkState,readyState,seeking,initialTime,duration,startOffsetTime,paused,played,seekable,ended,videoTracks,audioTracks,videoWidth,videoHeight,textTracks".split(","),
3532       callOnly = "load,play,pause".split(",");
3533       // Overridden: buffered
3534
3535       createSetter = function(attr){
3536         var attrUpper = attr.charAt(0).toUpperCase() + attr.slice(1);
3537         api["set"+attrUpper] = function(val){ return this.el.vjs_setProperty(attr, val); };
3538       },
3539
3540       createGetter = function(attr){
3541         api[attr] = function(){ return this.el.vjs_getProperty(attr); };
3542       }
3543   ;
3544
3545   // Create getter and setters for all read/write attributes
3546   _V_.each(readWrite, function(attr){
3547     createGetter(attr);
3548     createSetter(attr);
3549   });
3550
3551   // Create getters for read-only attributes
3552   _V_.each(readOnly, function(attr){
3553     createGetter(attr);
3554   });
3555
3556 })();
3557
3558 /* Flash Support Testing -------------------------------------------------------- */
3559
3560 _V_.flash.isSupported = function(){
3561   return _V_.flash.version()[0] >= 10;
3562   // return swfobject.hasFlashPlayerVersion("10");
3563 };
3564
3565 _V_.flash.canPlaySource = function(srcObj){
3566   if (srcObj.type in _V_.flash.prototype.support.formats) { return "maybe"; }
3567 };
3568
3569 _V_.flash.prototype.support = {
3570   formats: {
3571     "video/flv": "FLV",
3572     "video/x-flv": "FLV",
3573     "video/mp4": "MP4",
3574     "video/m4v": "MP4"
3575   },
3576
3577   // Optional events that we can manually mimic with timers
3578   progressEvent: false,
3579   timeupdateEvent: false,
3580
3581   // Resizing plugins using request fullscreen reloads the plugin
3582   fullscreenResize: false,
3583
3584   // Resizing plugins in Firefox always reloads the plugin (e.g. full window mode)
3585   parentResize: !(_V_.ua.match("Firefox"))
3586 };
3587
3588 _V_.flash.onReady = function(currSwf){
3589
3590   var el = _V_.el(currSwf);
3591
3592   // Get player from box
3593   // On firefox reloads, el might already have a player
3594   var player = el.player || el.parentNode.player,
3595       tech = player.tech;
3596
3597   // Reference player on tech element
3598   el.player = player;
3599
3600   // Update reference to playback technology element
3601   tech.el = el;
3602
3603   // Now that the element is ready, make a click on the swf play the video
3604   tech.addEvent("click", tech.onClick);
3605
3606   _V_.flash.checkReady(tech);
3607 };
3608
3609 // The SWF isn't alwasy ready when it says it is. Sometimes the API functions still need to be added to the object.
3610 // If it's not ready, we set a timeout to check again shortly.
3611 _V_.flash.checkReady = function(tech){
3612
3613   // Check if API property exists
3614   if (tech.el.vjs_getProperty) {
3615
3616     // If so, tell tech it's ready
3617     tech.triggerReady();
3618
3619   // Otherwise wait longer.
3620   } else {
3621
3622     setTimeout(function(){
3623       _V_.flash.checkReady(tech);
3624     }, 50);
3625
3626   }
3627 };
3628
3629 // Trigger events from the swf on the player
3630 _V_.flash.onEvent = function(swfID, eventName){
3631   var player = _V_.el(swfID).player;
3632   player.triggerEvent(eventName);
3633 };
3634
3635 // Log errors from the swf
3636 _V_.flash.onError = function(swfID, err){
3637   var player = _V_.el(swfID).player;
3638   player.triggerEvent("error");
3639   _V_.log("Flash Error", err, swfID);
3640 };
3641
3642 // Flash Version Check
3643 _V_.flash.version = function(){
3644   var version = '0,0,0'
3645
3646   // IE
3647   try {
3648     version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version').replace(/\D+/g, ',').match(/^,?(.+),?$/)[1];
3649
3650   // other browsers
3651   } catch(e) {
3652     try {
3653       if (navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin){
3654         version = (navigator.plugins["Shockwave Flash 2.0"] || navigator.plugins["Shockwave Flash"]).description.replace(/\D+/g, ",").match(/^,?(.+),?$/)[1];
3655       }
3656     } catch(e) {}
3657   }
3658   return version.split(",");
3659 }
3660
3661 // Flash embedding method. Only used in non-iframe mode
3662 _V_.flash.embed = function(swf, placeHolder, flashVars, params, attributes){
3663   var code = _V_.flash.getEmbedCode(swf, flashVars, params, attributes),
3664
3665       // Get element by embedding code and retrieving created element
3666       obj = _V_.createElement("div", { innerHTML: code }).childNodes[0],
3667
3668       par = placeHolder.parentNode
3669   ;
3670
3671   placeHolder.parentNode.replaceChild(obj, placeHolder);
3672
3673   // IE6 seems to have an issue where it won't initialize the swf object after injecting it.
3674   // This is a dumb temporary fix
3675   if (_V_.isIE()) {
3676     var newObj = par.childNodes[0];
3677     setTimeout(function(){
3678       newObj.style.display = "block";
3679     }, 1000);
3680   }
3681
3682   return obj;
3683
3684 };
3685
3686 _V_.flash.getEmbedCode = function(swf, flashVars, params, attributes){
3687
3688   var objTag = '<object type="application/x-shockwave-flash"',
3689       flashVarsString = '',
3690       paramsString = ''
3691       attrsString = '';
3692
3693   // Convert flash vars to string
3694   if (flashVars) {
3695     _V_.eachProp(flashVars, function(key, val){
3696       flashVarsString += (key + "=" + val + "&amp;");
3697     });
3698   }
3699
3700   // Add swf, flashVars, and other default params
3701   params = _V_.merge({
3702     movie: swf,
3703     flashvars: flashVarsString,
3704     allowScriptAccess: "always", // Required to talk to swf
3705     allowNetworking: "all" // All should be default, but having security issues.
3706   }, params);
3707
3708   // Create param tags string
3709   _V_.eachProp(params, function(key, val){
3710     paramsString += '<param name="'+key+'" value="'+val+'" />';
3711   });
3712
3713   attributes = _V_.merge({
3714     // Add swf to attributes (need both for IE and Others to work)
3715     data: swf,
3716
3717     // Default to 100% width/height
3718     width: "100%",
3719     height: "100%"
3720
3721   }, attributes);
3722
3723   // Create Attributes string
3724   _V_.eachProp(attributes, function(key, val){
3725     attrsString += (key + '="' + val + '" ');
3726   });
3727
3728   return objTag + attrsString + '>' + paramsString + '</object>';
3729 }
3730 // TEXT TRACKS
3731 // Text tracks are tracks of timed text events.
3732 //    Captions - text displayed over the video for the hearing impared
3733 //    Subtitles - text displayed over the video for those who don't understand langauge in the video
3734 //    Chapters - text displayed in a menu allowing the user to jump to particular points (chapters) in the video
3735 //    Descriptions (not supported yet) - audio descriptions that are read back to the user by a screen reading device
3736
3737 // Player Track Functions - Functions add to the player object for easier access to tracks
3738 _V_.merge(_V_.Player.prototype, {
3739
3740   // Add an array of text tracks. captions, subtitles, chapters, descriptions
3741   // Track objects will be stored in the player.textTracks array
3742   addTextTracks: function(trackObjects){
3743     var tracks = this.textTracks = (this.textTracks) ? this.textTracks : [],
3744         i = 0, j = trackObjects.length, track, Kind;
3745
3746     for (;i<j;i++) {
3747       // HTML5 Spec says default to subtitles.
3748       // Uppercase (uc) first letter to match class names
3749       Kind = _V_.uc(trackObjects[i].kind || "subtitles");
3750
3751       // Create correct texttrack class. CaptionsTrack, etc.
3752       track = new _V_[Kind + "Track"](this, trackObjects[i]);
3753
3754       tracks.push(track);
3755
3756       // If track.default is set, start showing immediately
3757       // TODO: Add a process to deterime the best track to show for the specific kind
3758       // Incase there are mulitple defaulted tracks of the same kind
3759       // Or the user has a set preference of a specific language that should override the default
3760       if (track['default']) {
3761         this.ready(_V_.proxy(track, track.show));
3762       }
3763     }
3764
3765     // Return the track so it can be appended to the display component
3766     return this;
3767   },
3768
3769   // Show a text track
3770   // disableSameKind: disable all other tracks of the same kind. Value should be a track kind (captions, etc.)
3771   showTextTrack: function(id, disableSameKind){
3772     var tracks = this.textTracks,
3773         i = 0,
3774         j = tracks.length,
3775         track, showTrack, kind;
3776
3777     // Find Track with same ID
3778     for (;i<j;i++) {
3779       track = tracks[i];
3780       if (track.id === id) {
3781         track.show();
3782         showTrack = track;
3783
3784       // Disable tracks of the same kind
3785       } else if (disableSameKind && track.kind == disableSameKind && track.mode > 0) {
3786         track.disable();
3787       }
3788     }
3789
3790     // Get track kind from shown track or disableSameKind
3791     kind = (showTrack) ? showTrack.kind : ((disableSameKind) ? disableSameKind : false);
3792
3793     // Trigger trackchange event, captionstrackchange, subtitlestrackchange, etc.
3794     if (kind) {
3795       this.triggerEvent(kind+"trackchange");
3796     }
3797
3798     return this;
3799   }
3800
3801 });
3802
3803 // Track Class
3804 // Contains track methods for loading, showing, parsing cues of tracks
3805 _V_.Track = _V_.Component.extend({
3806
3807   init: function(player, options){
3808     this._super(player, options);
3809
3810     // Apply track info to track object
3811     // Options will often be a track element
3812     _V_.merge(this, {
3813       // Build ID if one doesn't exist
3814       id: options.id || ("vjs_" + options.kind + "_" + options.language + "_" + _V_.guid++),
3815
3816       src: options.src,
3817
3818       // If default is used, subtitles/captions to start showing
3819       "default": options["default"], // 'default' is reserved-ish
3820       title: options.title,
3821
3822       // Language - two letter string to represent track language, e.g. "en" for English
3823       // readonly attribute DOMString language;
3824       language: options.srclang,
3825
3826       // Track label e.g. "English"
3827       // readonly attribute DOMString label;
3828       label: options.label,
3829
3830       // All cues of the track. Cues have a startTime, endTime, text, and other properties.
3831       // readonly attribute TextTrackCueList cues;
3832       cues: [],
3833
3834       // ActiveCues is all cues that are currently showing
3835       // readonly attribute TextTrackCueList activeCues;
3836       activeCues: [],
3837
3838       // ReadyState describes if the text file has been loaded
3839       // const unsigned short NONE = 0;
3840       // const unsigned short LOADING = 1;
3841       // const unsigned short LOADED = 2;
3842       // const unsigned short ERROR = 3;
3843       // readonly attribute unsigned short readyState;
3844       readyState: 0,
3845
3846       // Mode describes if the track is showing, hidden, or disabled
3847       // const unsigned short OFF = 0;
3848       // const unsigned short HIDDEN = 1; (still triggering cuechange events, but not visible)
3849       // const unsigned short SHOWING = 2;
3850       // attribute unsigned short mode;
3851       mode: 0
3852     });
3853   },
3854
3855   // Create basic div to hold cue text
3856   createElement: function(){
3857     return this._super("div", {
3858       className: "vjs-" + this.kind + " vjs-text-track"
3859     });
3860   },
3861
3862   // Show: Mode Showing (2)
3863   // Indicates that the text track is active. If no attempt has yet been made to obtain the track's cues, the user agent will perform such an attempt momentarily.
3864   // The user agent is maintaining a list of which cues are active, and events are being fired accordingly.
3865   // In addition, for text tracks whose kind is subtitles or captions, the cues are being displayed over the video as appropriate;
3866   // for text tracks whose kind is descriptions, the user agent is making the cues available to the user in a non-visual fashion;
3867   // and for text tracks whose kind is chapters, the user agent is making available to the user a mechanism by which the user can navigate to any point in the media resource by selecting a cue.
3868   // The showing by default state is used in conjunction with the default attribute on track elements to indicate that the text track was enabled due to that attribute.
3869   // This allows the user agent to override the state if a later track is discovered that is more appropriate per the user's preferences.
3870   show: function(){
3871     this.activate();
3872
3873     this.mode = 2;
3874
3875     // Show element.
3876     this._super();
3877   },
3878
3879   // Hide: Mode Hidden (1)
3880   // Indicates that the text track is active, but that the user agent is not actively displaying the cues.
3881   // If no attempt has yet been made to obtain the track's cues, the user agent will perform such an attempt momentarily.
3882   // The user agent is maintaining a list of which cues are active, and events are being fired accordingly.
3883   hide: function(){
3884     // When hidden, cues are still triggered. Disable to stop triggering.
3885     this.activate();
3886
3887     this.mode = 1;
3888
3889     // Hide element.
3890     this._super();
3891   },
3892
3893   // Disable: Mode Off/Disable (0)
3894   // Indicates that the text track is not active. Other than for the purposes of exposing the track in the DOM, the user agent is ignoring the text track.
3895   // No cues are active, no events are fired, and the user agent will not attempt to obtain the track's cues.
3896   disable: function(){
3897     // If showing, hide.
3898     if (this.mode == 2) { this.hide(); }
3899
3900     // Stop triggering cues
3901     this.deactivate();
3902
3903     // Switch Mode to Off
3904     this.mode = 0;
3905   },
3906
3907   // Turn on cue tracking. Tracks that are showing OR hidden are active.
3908   activate: function(){
3909     // Load text file if it hasn't been yet.
3910     if (this.readyState == 0) { this.load(); }
3911
3912     // Only activate if not already active.
3913     if (this.mode == 0) {
3914       // Update current cue on timeupdate
3915       // Using unique ID for proxy function so other tracks don't remove listener
3916       this.player.addEvent("timeupdate", this.proxy(this.update, this.id));
3917
3918       // Reset cue time on media end
3919       this.player.addEvent("ended", this.proxy(this.reset, this.id));
3920
3921       // Add to display
3922       if (this.kind == "captions" || this.kind == "subtitles") {
3923         this.player.textTrackDisplay.addComponent(this);
3924       }
3925     }
3926   },
3927
3928   // Turn off cue tracking.
3929   deactivate: function(){
3930     // Using unique ID for proxy function so other tracks don't remove listener
3931     this.player.removeEvent("timeupdate", this.proxy(this.update, this.id));
3932     this.player.removeEvent("ended", this.proxy(this.reset, this.id));
3933     this.reset(); // Reset
3934
3935     // Remove from display
3936     this.player.textTrackDisplay.removeComponent(this);
3937   },
3938
3939   // A readiness state
3940   // One of the following:
3941   //
3942   // Not loaded
3943   // Indicates that the text track is known to exist (e.g. it has been declared with a track element), but its cues have not been obtained.
3944   //
3945   // Loading
3946   // Indicates that the text track is loading and there have been no fatal errors encountered so far. Further cues might still be added to the track.
3947   //
3948   // Loaded
3949   // Indicates that the text track has been loaded with no fatal errors. No new cues will be added to the track except if the text track corresponds to a MutableTextTrack object.
3950   //
3951   // Failed to load
3952   // Indicates that the text track was enabled, but when the user agent attempted to obtain it, this failed in some way (e.g. URL could not be resolved, network error, unknown text track format). Some or all of the cues are likely missing and will not be obtained.
3953   load: function(){
3954
3955     // Only load if not loaded yet.
3956     if (this.readyState == 0) {
3957       this.readyState = 1;
3958       _V_.get(this.src, this.proxy(this.parseCues), this.proxy(this.onError));
3959     }
3960
3961   },
3962
3963   onError: function(err){
3964     this.error = err;
3965     this.readyState = 3;
3966     this.triggerEvent("error");
3967   },
3968
3969   // Parse the WebVTT text format for cue times.
3970   // TODO: Separate parser into own class so alternative timed text formats can be used. (TTML, DFXP)
3971   parseCues: function(srcContent) {
3972     var cue, time, text,
3973         lines = srcContent.split("\n"),
3974         line = "", id;
3975
3976     for (var i=1, j=lines.length; i<j; i++) {
3977       // Line 0 should be 'WEBVTT', so skipping i=0
3978
3979       line = _V_.trim(lines[i]); // Trim whitespace and linebreaks
3980
3981       if (line) { // Loop until a line with content
3982
3983         // First line could be an optional cue ID
3984         // Check if line has the time separator
3985         if (line.indexOf("-->") == -1) {
3986           id = line;
3987           // Advance to next line for timing.
3988           line = _V_.trim(lines[++i]);
3989         } else {
3990           id = this.cues.length;
3991         }
3992
3993         // First line - Number
3994         cue = {
3995           id: id, // Cue Number
3996           index: this.cues.length // Position in Array
3997         };
3998
3999         // Timing line
4000         time = line.split(" --> ");
4001         cue.startTime = this.parseCueTime(time[0]);
4002         cue.endTime = this.parseCueTime(time[1]);
4003
4004         // Additional lines - Cue Text
4005         text = [];
4006
4007         // Loop until a blank line or end of lines
4008         // Assumeing trim("") returns false for blank lines
4009         while (lines[++i] && (line = _V_.trim(lines[i]))) {
4010           text.push(line);
4011         }
4012
4013         cue.text = text.join('<br/>');
4014
4015         // Add this cue
4016         this.cues.push(cue);
4017       }
4018     }
4019
4020     this.readyState = 2;
4021     this.triggerEvent("loaded");
4022   },
4023
4024   parseCueTime: function(timeText) {
4025     var parts = timeText.split(':'),
4026         time = 0,
4027         hours, minutes, other, seconds, ms, flags;
4028
4029     // Check if optional hours place is included
4030     // 00:00:00.000 vs. 00:00.000
4031     if (parts.length == 3) {
4032       hours = parts[0];
4033       minutes = parts[1];
4034       other = parts[2];
4035     } else {
4036       hours = 0;
4037       minutes = parts[0];
4038       other = parts[1];
4039     }
4040
4041     // Break other (seconds, milliseconds, and flags) by spaces
4042     // TODO: Make additional cue layout settings work with flags
4043     other = other.split(/\s+/)
4044     // Remove seconds. Seconds is the first part before any spaces.
4045     seconds = other.splice(0,1)[0];
4046     // Could use either . or , for decimal
4047     seconds = seconds.split(/\.|,/);
4048     // Get milliseconds
4049     ms = parseFloat(seconds[1]);
4050     seconds = seconds[0];
4051
4052     // hours => seconds
4053     time += parseFloat(hours) * 3600;
4054     // minutes => seconds
4055     time += parseFloat(minutes) * 60;
4056     // Add seconds
4057     time += parseFloat(seconds);
4058     // Add milliseconds
4059     if (ms) { time += ms/1000; }
4060
4061     return time;
4062   },
4063
4064   // Update active cues whenever timeupdate events are triggered on the player.
4065   update: function(){
4066     if (this.cues.length > 0) {
4067
4068       // Get curent player time
4069       var time = this.player.currentTime();
4070
4071       // Check if the new time is outside the time box created by the the last update.
4072       if (this.prevChange === undefined || time < this.prevChange || this.nextChange <= time) {
4073         var cues = this.cues,
4074
4075             // Create a new time box for this state.
4076             newNextChange = this.player.duration(), // Start at beginning of the timeline
4077             newPrevChange = 0, // Start at end
4078
4079             reverse = false, // Set the direction of the loop through the cues. Optimized the cue check.
4080             newCues = [], // Store new active cues.
4081
4082             // Store where in the loop the current active cues are, to provide a smart starting point for the next loop.
4083             firstActiveIndex, lastActiveIndex,
4084
4085             html = "", // Create cue text HTML to add to the display
4086             cue, i, j; // Loop vars
4087
4088         // Check if time is going forwards or backwards (scrubbing/rewinding)
4089         // If we know the direction we can optimize the starting position and direction of the loop through the cues array.
4090         if (time >= this.nextChange || this.nextChange === undefined) { // NextChange should happen
4091           // Forwards, so start at the index of the first active cue and loop forward
4092           i = (this.firstActiveIndex !== undefined) ? this.firstActiveIndex : 0;
4093         } else {
4094           // Backwards, so start at the index of the last active cue and loop backward
4095           reverse = true;
4096           i = (this.lastActiveIndex !== undefined) ? this.lastActiveIndex : cues.length - 1;
4097         }
4098
4099         while (true) { // Loop until broken
4100           cue = cues[i];
4101
4102           // Cue ended at this point
4103           if (cue.endTime <= time) {
4104             newPrevChange = Math.max(newPrevChange, cue.endTime);
4105
4106             if (cue.active) {
4107               cue.active = false;
4108             }
4109
4110             // No earlier cues should have an active start time.
4111             // Nevermind. Assume first cue could have a duration the same as the video.
4112             // In that case we need to loop all the way back to the beginning.
4113             // if (reverse && cue.startTime) { break; }
4114
4115           // Cue hasn't started
4116           } else if (time < cue.startTime) {
4117             newNextChange = Math.min(newNextChange, cue.startTime);
4118
4119             if (cue.active) {
4120               cue.active = false;
4121             }
4122
4123             // No later cues should have an active start time.
4124             if (!reverse) { break; }
4125
4126           // Cue is current
4127           } else {
4128
4129             if (reverse) {
4130               // Add cue to front of array to keep in time order
4131               newCues.splice(0,0,cue);
4132
4133               // If in reverse, the first current cue is our lastActiveCue
4134               if (lastActiveIndex === undefined) { lastActiveIndex = i; }
4135               firstActiveIndex = i;
4136             } else {
4137               // Add cue to end of array
4138               newCues.push(cue);
4139
4140               // If forward, the first current cue is our firstActiveIndex
4141               if (firstActiveIndex === undefined) { firstActiveIndex = i; }
4142               lastActiveIndex = i;
4143             }
4144
4145             newNextChange = Math.min(newNextChange, cue.endTime);
4146             newPrevChange = Math.max(newPrevChange, cue.startTime);
4147
4148             cue.active = true;
4149           }
4150
4151           if (reverse) {
4152             // Reverse down the array of cues, break if at first
4153             if (i === 0) { break; } else { i--; }
4154           } else {
4155             // Walk up the array fo cues, break if at last
4156             if (i === cues.length - 1) { break; } else { i++; }
4157           }
4158
4159         }
4160
4161         this.activeCues = newCues;
4162         this.nextChange = newNextChange;
4163         this.prevChange = newPrevChange;
4164         this.firstActiveIndex = firstActiveIndex;
4165         this.lastActiveIndex = lastActiveIndex;
4166
4167         this.updateDisplay();
4168
4169         this.triggerEvent("cuechange");
4170       }
4171     }
4172   },
4173
4174   // Add cue HTML to display
4175   updateDisplay: function(){
4176     var cues = this.activeCues,
4177         html = "",
4178         i=0,j=cues.length;
4179
4180     for (;i<j;i++) {
4181       html += "<span class='vjs-tt-cue'>"+cues[i].text+"</span>";
4182     }
4183
4184     this.el.innerHTML = html;
4185   },
4186
4187   // Set all loop helper values back
4188   reset: function(){
4189     this.nextChange = 0;
4190     this.prevChange = this.player.duration();
4191     this.firstActiveIndex = 0;
4192     this.lastActiveIndex = 0;
4193   }
4194
4195 });
4196
4197 // Create specific track types
4198 _V_.CaptionsTrack = _V_.Track.extend({
4199   kind: "captions"
4200 });
4201
4202 _V_.SubtitlesTrack = _V_.Track.extend({
4203   kind: "subtitles"
4204 });
4205
4206 _V_.ChaptersTrack = _V_.Track.extend({
4207   kind: "chapters"
4208 });
4209
4210
4211 /* Text Track Display
4212 ================================================================================ */
4213 // Global container for both subtitle and captions text. Simple div container.
4214 _V_.TextTrackDisplay = _V_.Component.extend({
4215
4216   createElement: function(){
4217     return this._super("div", {
4218       className: "vjs-text-track-display"
4219     });
4220   }
4221
4222 });
4223
4224 /* Text Track Menu Items
4225 ================================================================================ */
4226 _V_.TextTrackMenuItem = _V_.MenuItem.extend({
4227
4228   init: function(player, options){
4229     var track = this.track = options.track;
4230
4231     // Modify options for parent MenuItem class's init.
4232     options.label = track.label;
4233     options.selected = track["default"];
4234     this._super(player, options);
4235
4236     this.player.addEvent(track.kind + "trackchange", _V_.proxy(this, this.update));
4237   },
4238
4239   onClick: function(){
4240     this._super();
4241     this.player.showTextTrack(this.track.id, this.track.kind);
4242   },
4243
4244   update: function(){
4245     if (this.track.mode == 2) {
4246       this.selected(true);
4247     } else {
4248       this.selected(false);
4249     }
4250   }
4251
4252 });
4253
4254 _V_.OffTextTrackMenuItem = _V_.TextTrackMenuItem.extend({
4255
4256   init: function(player, options){
4257     // Create pseudo track info
4258     // Requires options.kind
4259     options.track = { kind: options.kind, player: player, label: "Off" }
4260     this._super(player, options);
4261   },
4262
4263   onClick: function(){
4264     this._super();
4265     this.player.showTextTrack(this.track.id, this.track.kind);
4266   },
4267
4268   update: function(){
4269     var tracks = this.player.textTracks,
4270         i=0, j=tracks.length, track,
4271         off = true;
4272
4273     for (;i<j;i++) {
4274       track = tracks[i];
4275       if (track.kind == this.track.kind && track.mode == 2) {
4276         off = false;
4277       }
4278     }
4279
4280     if (off) {
4281       this.selected(true);
4282     } else {
4283       this.selected(false);
4284     }
4285   }
4286
4287 });
4288
4289 /* Captions Button
4290 ================================================================================ */
4291 _V_.TextTrackButton = _V_.Button.extend({
4292
4293   init: function(player, options){
4294     this._super(player, options);
4295
4296     this.menu = this.createMenu();
4297
4298     if (this.items.length === 0) {
4299       this.hide();
4300     }
4301   },
4302
4303   createMenu: function(){
4304     var menu = new _V_.Menu(this.player);
4305
4306     // Add a title list item to the top
4307     menu.el.appendChild(_V_.createElement("li", {
4308       className: "vjs-menu-title",
4309       innerHTML: _V_.uc(this.kind)
4310     }));
4311
4312     // Add an OFF menu item to turn all tracks off
4313     menu.addItem(new _V_.OffTextTrackMenuItem(this.player, { kind: this.kind }))
4314
4315     this.items = this.createItems();
4316
4317     // Add menu items to the menu
4318     this.each(this.items, function(item){
4319       menu.addItem(item);
4320     });
4321
4322     // Add list to element
4323     this.addComponent(menu);
4324
4325     return menu;
4326   },
4327
4328   // Create a menu item for each text track
4329   createItems: function(){
4330     var items = [];
4331     this.each(this.player.textTracks, function(track){
4332       if (track.kind === this.kind) {
4333         items.push(new _V_.TextTrackMenuItem(this.player, {
4334           track: track
4335         }));
4336       }
4337     });
4338     return items;
4339   },
4340
4341   buildCSSClass: function(){
4342     return this.className + " vjs-menu-button " + this._super();
4343   },
4344
4345   // Focus - Add keyboard functionality to element
4346   onFocus: function(){
4347     // Show the menu, and keep showing when the menu items are in focus
4348     this.menu.lockShowing();
4349     // this.menu.el.style.display = "block";
4350
4351     // When tabbing through, the menu should hide when focus goes from the last menu item to the next tabbed element.
4352     _V_.one(this.menu.el.childNodes[this.menu.el.childNodes.length - 1], "blur", this.proxy(function(){
4353       this.menu.unlockShowing();
4354     }));
4355   },
4356   // Can't turn off list display that we turned on with focus, because list would go away.
4357   onBlur: function(){},
4358
4359   onClick: function(){
4360     // When you click the button it adds focus, which will show the menu indefinitely.
4361     // So we'll remove focus when the mouse leaves the button.
4362     // Focus is needed for tab navigation.
4363     this.one("mouseout", this.proxy(function(){
4364       this.menu.unlockShowing();
4365       this.el.blur();
4366     }));
4367   }
4368
4369 });
4370
4371 _V_.CaptionsButton = _V_.TextTrackButton.extend({
4372   kind: "captions",
4373   buttonText: "Captions",
4374   className: "vjs-captions-button"
4375 });
4376
4377 _V_.SubtitlesButton = _V_.TextTrackButton.extend({
4378   kind: "subtitles",
4379   buttonText: "Subtitles",
4380   className: "vjs-subtitles-button"
4381 });
4382
4383 // Chapters act much differently than other text tracks
4384 // Cues are navigation vs. other tracks of alternative languages
4385 _V_.ChaptersButton = _V_.TextTrackButton.extend({
4386   kind: "chapters",
4387   buttonText: "Chapters",
4388   className: "vjs-chapters-button",
4389
4390   // Create a menu item for each text track
4391   createItems: function(chaptersTrack){
4392     var items = [];
4393
4394     this.each(this.player.textTracks, function(track){
4395       if (track.kind === this.kind) {
4396         items.push(new _V_.TextTrackMenuItem(this.player, {
4397           track: track
4398         }));
4399       }
4400     });
4401
4402     return items;
4403   },
4404
4405   createMenu: function(){
4406     var tracks = this.player.textTracks,
4407         i = 0,
4408         j = tracks.length,
4409         track, chaptersTrack,
4410         items = this.items = [];
4411
4412     for (;i<j;i++) {
4413       track = tracks[i];
4414       if (track.kind == this.kind && track["default"]) {
4415         if (track.readyState < 2) {
4416           this.chaptersTrack = track;
4417           track.addEvent("loaded", this.proxy(this.createMenu));
4418           return;
4419         } else {
4420           chaptersTrack = track;
4421           break;
4422         }
4423       }
4424     }
4425
4426     var menu = this.menu = new _V_.Menu(this.player);
4427
4428     menu.el.appendChild(_V_.createElement("li", {
4429       className: "vjs-menu-title",
4430       innerHTML: _V_.uc(this.kind)
4431     }));
4432
4433     if (chaptersTrack) {
4434       var cues = chaptersTrack.cues,
4435           i = 0, j = cues.length, cue, mi;
4436
4437       for (;i<j;i++) {
4438         cue = cues[i];
4439
4440         mi = new _V_.ChaptersTrackMenuItem(this.player, {
4441           track: chaptersTrack,
4442           cue: cue
4443         });
4444
4445         items.push(mi);
4446
4447         menu.addComponent(mi);
4448       }
4449     }
4450
4451     // Add list to element
4452     this.addComponent(menu);
4453
4454     if (this.items.length > 0) {
4455       this.show();
4456     }
4457
4458     return menu;
4459   }
4460
4461 });
4462
4463 _V_.ChaptersTrackMenuItem = _V_.MenuItem.extend({
4464
4465   init: function(player, options){
4466     var track = this.track = options.track,
4467         cue = this.cue = options.cue,
4468         currentTime = player.currentTime();
4469
4470     // Modify options for parent MenuItem class's init.
4471     options.label = cue.text;
4472     options.selected = (cue.startTime <= currentTime && currentTime < cue.endTime);
4473     this._super(player, options);
4474
4475     track.addEvent("cuechange", _V_.proxy(this, this.update));
4476   },
4477
4478   onClick: function(){
4479     this._super();
4480     this.player.currentTime(this.cue.startTime);
4481     this.update(this.cue.startTime);
4482   },
4483
4484   update: function(time){
4485     var cue = this.cue,
4486         currentTime = this.player.currentTime();
4487
4488     // _V_.log(currentTime, cue.startTime);
4489     if (cue.startTime <= currentTime && currentTime < cue.endTime) {
4490       this.selected(true);
4491     } else {
4492       this.selected(false);
4493     }
4494   }
4495
4496 });
4497
4498 // Add Buttons to controlBar
4499 _V_.merge(_V_.ControlBar.prototype.options.components, {
4500   "subtitlesButton": {},
4501   "captionsButton": {},
4502   "chaptersButton": {}
4503 });
4504
4505 // _V_.Cue = _V_.Component.extend({
4506 //   init: function(player, options){
4507 //     this._super(player, options);
4508 //   }
4509 // });// Automatically set up any tags that have a data-setup attribute
4510 _V_.autoSetup = function(){
4511   var options, vid, player,
4512       vids = document.getElementsByTagName("video");
4513
4514   // Check if any media elements exist
4515   if (vids && vids.length > 0) {
4516
4517     for (var i=0,j=vids.length; i<j; i++) {
4518       vid = vids[i];
4519
4520       // Check if element exists, has getAttribute func.
4521       // IE seems to consider typeof el.getAttribute == "object" instead of "function" like expected, at least when loading the player immediately.
4522       if (vid && vid.getAttribute) {
4523
4524         // Make sure this player hasn't already been set up.
4525         if (vid.player === undefined) {
4526           options = vid.getAttribute("data-setup");
4527
4528           // Check if data-setup attr exists. 
4529           // We only auto-setup if they've added the data-setup attr.
4530           if (options !== null) {
4531
4532             // Parse options JSON
4533             // If empty string, make it a parsable json object.
4534             options = JSON.parse(options || "{}");
4535
4536             // Create new video.js instance.
4537             player = _V_(vid, options);
4538           }
4539         }
4540
4541       // If getAttribute isn't defined, we need to wait for the DOM.
4542       } else {
4543         _V_.autoSetupTimeout(1);
4544         break;
4545       }
4546     }
4547
4548   // No videos were found, so keep looping unless page is finisehd loading.
4549   } else if (!_V_.windowLoaded) {
4550     _V_.autoSetupTimeout(1);
4551   }
4552 };
4553
4554 // Pause to let the DOM keep processing
4555 _V_.autoSetupTimeout = function(wait){
4556   setTimeout(_V_.autoSetup, wait);
4557 };
4558
4559 _V_.addEvent(window, "load", function(){
4560   _V_.windowLoaded = true;
4561 });
4562
4563 // Run Auto-load players
4564 _V_.autoSetup();
4565 // Expose to global
4566 window.VideoJS = window._V_ = VideoJS;
4567
4568 // End self-executing function
4569 })(window);