Statistics
| Branch: | Tag: | Revision:

root / okeanos_site / static / okeanos_static / video / video.js @ 4beadc2f

History | View | Annotate | Download (66.2 kB)

1
/*
2
VideoJS - HTML5 Video Player
3
v2.0.2
4

5
This file is part of VideoJS. Copyright 2010 Zencoder, Inc.
6

7
VideoJS is free software: you can redistribute it and/or modify
8
it under the terms of the GNU Lesser General Public License as published by
9
the Free Software Foundation, either version 3 of the License, or
10
(at your option) any later version.
11

12
VideoJS is distributed in the hope that it will be useful,
13
but WITHOUT ANY WARRANTY; without even the implied warranty of
14
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
GNU Lesser General Public License for more details.
16

17
You should have received a copy of the GNU Lesser General Public License
18
along with VideoJS.  If not, see <http://www.gnu.org/licenses/>.
19
*/
20

    
21
// Self-executing function to prevent global vars and help with minification
22
(function(window, undefined){
23
  var document = window.document;
24

    
25
// Using jresig's Class implementation http://ejohn.org/blog/simple-javascript-inheritance/
26
(function(){var initializing=false, fnTest=/xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; this.JRClass = function(){}; JRClass.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 JRClass() { if ( !initializing && this.init ) this.init.apply(this, arguments); } JRClass.prototype = prototype; JRClass.constructor = JRClass; JRClass.extend = arguments.callee; return JRClass;};})();
27

    
28
// Video JS Player Class
29
var VideoJS = JRClass.extend({
30

    
31
  // Initialize the player for the supplied video tag element
32
  // element: video tag
33
  init: function(element, setOptions){
34

    
35
    // Allow an ID string or an element
36
    if (typeof element == 'string') {
37
      this.video = document.getElementById(element);
38
    } else {
39
      this.video = element;
40
    }
41
    // Store reference to player on the video element.
42
    // So you can acess the player later: document.getElementById("video_id").player.play();
43
    this.video.player = this;
44
    this.values = {}; // Cache video values.
45
    this.elements = {}; // Store refs to controls elements.
46

    
47
    // Default Options
48
    this.options = {
49
      autoplay: false,
50
      preload: true,
51
      useBuiltInControls: false, // Use the browser's controls (iPhone)
52
      controlsBelow: false, // Display control bar below video vs. in front of
53
      controlsAtStart: false, // Make controls visible when page loads
54
      controlsHiding: true, // Hide controls when not over the video
55
      defaultVolume: 0.85, // Will be overridden by localStorage volume if available
56
      playerFallbackOrder: ["html5", "flash", "links"], // Players and order to use them
57
      flashPlayer: "htmlObject",
58
      flashPlayerVersion: false // Required flash version for fallback
59
    };
60
    // Override default options with global options
61
    if (typeof VideoJS.options == "object") { _V_.merge(this.options, VideoJS.options); }
62
    // Override default & global options with options specific to this player
63
    if (typeof setOptions == "object") { _V_.merge(this.options, setOptions); }
64
    // Override preload & autoplay with video attributes
65
    if (this.getPreloadAttribute() !== undefined) { this.options.preload = this.getPreloadAttribute(); }
66
    if (this.getAutoplayAttribute() !== undefined) { this.options.autoplay = this.getAutoplayAttribute(); }
67

    
68
    // Store reference to embed code pieces
69
    this.box = this.video.parentNode;
70
    this.linksFallback = this.getLinksFallback();
71
    this.hideLinksFallback(); // Will be shown again if "links" player is used
72

    
73
    // Loop through the player names list in options, "html5" etc.
74
    // For each player name, initialize the player with that name under VideoJS.players
75
    // If the player successfully initializes, we're done
76
    // If not, try the next player in the list
77
    this.each(this.options.playerFallbackOrder, function(playerType){
78
      if (this[playerType+"Supported"]()) { // Check if player type is supported
79
        this[playerType+"Init"](); // Initialize player type
80
        return true; // Stop looping though players
81
      }
82
    });
83

    
84
    // Start Global Listeners - API doesn't exist before now
85
    this.activateElement(this, "player");
86
    this.activateElement(this.box, "box");
87
  },
88
  /* Behaviors
89
  ================================================================================ */
90
  behaviors: {},
91
  newBehavior: function(name, activate, functions){
92
    this.behaviors[name] = activate;
93
    this.extend(functions);
94
  },
95
  activateElement: function(element, behavior){
96
    // Allow passing and ID string
97
    if (typeof element == "string") { element = document.getElementById(element); }
98
    this.behaviors[behavior].call(this, element);
99
  },
100
  /* Errors/Warnings
101
  ================================================================================ */
102
  errors: [], // Array to track errors
103
  warnings: [],
104
  warning: function(warning){
105
    this.warnings.push(warning);
106
    this.log(warning);
107
  },
108
  /* History of errors/events (not quite there yet)
109
  ================================================================================ */
110
  history: [],
111
  log: function(event){
112
    if (!event) { return; }
113
    if (typeof event == "string") { event = { type: event }; }
114
    if (event.type) { this.history.push(event.type); }
115
    if (this.history.length >= 50) { this.history.shift(); }
116
    try { console.log(event.type); } catch(e) { try { opera.postError(event.type); } catch(e){} }
117
  },
118
  /* Local Storage
119
  ================================================================================ */
120
  setLocalStorage: function(key, value){
121
    if (!localStorage) { return; }
122
    try {
123
      localStorage[key] = value;
124
    } catch(e) {
125
      if (e.code == 22 || e.code == 1014) { // Webkit == 22 / Firefox == 1014
126
        this.warning(VideoJS.warnings.localStorageFull);
127
      }
128
    }
129
  },
130
  /* Helpers
131
  ================================================================================ */
132
  getPreloadAttribute: function(){
133
    if (typeof this.video.hasAttribute == "function" && this.video.hasAttribute("preload")) {
134
      var preload = this.video.getAttribute("preload");
135
      // Only included the attribute, thinking it was boolean
136
      if (preload === "" || preload === "true") { return "auto"; }
137
      if (preload === "false") { return "none"; }
138
      return preload;
139
    }
140
  },
141
  getAutoplayAttribute: function(){
142
    if (typeof this.video.hasAttribute == "function" && this.video.hasAttribute("autoplay")) {
143
      var autoplay = this.video.getAttribute("autoplay");
144
      if (autoplay === "false") { return false; }
145
      return true;
146
    }
147
  },
148
  // Calculates amoutn of buffer is full
149
  bufferedPercent: function(){ return (this.duration()) ? this.buffered()[1] / this.duration() : 0; },
150
  // Each that maintains player as context
151
  // Break if true is returned
152
  each: function(arr, fn){
153
    if (!arr || arr.length === 0) { return; }
154
    for (var i=0,j=arr.length; i<j; i++) {
155
      if (fn.call(this, arr[i], i)) { break; }
156
    }
157
  },
158
  extend: function(obj){
159
    for (var attrname in obj) {
160
      if (obj.hasOwnProperty(attrname)) { this[attrname]=obj[attrname]; }
161
    }
162
  }
163
});
164
VideoJS.player = VideoJS.prototype;
165

    
166
////////////////////////////////////////////////////////////////////////////////
167
// Player Types
168
////////////////////////////////////////////////////////////////////////////////
169

    
170
/* Flash Object Fallback (Player Type)
171
================================================================================ */
172
VideoJS.player.extend({
173
  flashSupported: function(){
174
    if (!this.flashElement) { this.flashElement = this.getFlashElement(); }
175
    // Check if object exists & Flash Player version is supported
176
    if (this.flashElement && this.flashPlayerVersionSupported()) {
177
      return true;
178
    } else {
179
      return false;
180
    }
181
  },
182
  flashInit: function(){
183
    this.replaceWithFlash();
184
    this.element = this.flashElement;
185
    this.video.src = ""; // Stop video from downloading if HTML5 is still supported
186
    var flashPlayerType = VideoJS.flashPlayers[this.options.flashPlayer];
187
    this.extend(VideoJS.flashPlayers[this.options.flashPlayer].api);
188
    (flashPlayerType.init.context(this))();
189
  },
190
  // Get Flash Fallback object element from Embed Code
191
  getFlashElement: function(){
192
    var children = this.video.children;
193
    for (var i=0,j=children.length; i<j; i++) {
194
      if (children[i].className == "vjs-flash-fallback") {
195
        return children[i];
196
      }
197
    }
198
  },
199
  // Used to force a browser to fall back when it's an HTML5 browser but there's no supported sources
200
  replaceWithFlash: function(){
201
    // this.flashElement = this.video.removeChild(this.flashElement);
202
    if (this.flashElement) {
203
      this.box.insertBefore(this.flashElement, this.video);
204
      this.video.style.display = "none"; // Removing it was breaking later players
205
    }
206
  },
207
  // Check if browser can use this flash player
208
  flashPlayerVersionSupported: function(){
209
    var playerVersion = (this.options.flashPlayerVersion) ? this.options.flashPlayerVersion : VideoJS.flashPlayers[this.options.flashPlayer].flashPlayerVersion;
210
    return VideoJS.getFlashVersion() >= playerVersion;
211
  }
212
});
213
VideoJS.flashPlayers = {};
214
VideoJS.flashPlayers.htmlObject = {
215
  flashPlayerVersion: 9,
216
  init: function() { return true; },
217
  api: { // No video API available with HTML Object embed method
218
    width: function(width){
219
      if (width !== undefined) {
220
        this.element.width = width;
221
        this.box.style.width = width+"px";
222
        this.triggerResizeListeners();
223
        return this;
224
      }
225
      return this.element.width;
226
    },
227
    height: function(height){
228
      if (height !== undefined) {
229
        this.element.height = height;
230
        this.box.style.height = height+"px";
231
        this.triggerResizeListeners();
232
        return this;
233
      }
234
      return this.element.height;
235
    }
236
  }
237
};
238

    
239

    
240
/* Download Links Fallback (Player Type)
241
================================================================================ */
242
VideoJS.player.extend({
243
  linksSupported: function(){ return true; },
244
  linksInit: function(){
245
    this.showLinksFallback();
246
    this.element = this.video;
247
  },
248
  // Get the download links block element
249
  getLinksFallback: function(){ return this.box.getElementsByTagName("P")[0]; },
250
  // Hide no-video download paragraph
251
  hideLinksFallback: function(){
252
    if (this.linksFallback) { this.linksFallback.style.display = "none"; }
253
  },
254
  // Hide no-video download paragraph
255
  showLinksFallback: function(){
256
    if (this.linksFallback) { this.linksFallback.style.display = "block"; }
257
  }
258
});
259

    
260
////////////////////////////////////////////////////////////////////////////////
261
// Class Methods
262
// Functions that don't apply to individual videos.
263
////////////////////////////////////////////////////////////////////////////////
264

    
265
// Combine Objects - Use "safe" to protect from overwriting existing items
266
VideoJS.merge = function(obj1, obj2, safe){
267
  for (var attrname in obj2){
268
    if (obj2.hasOwnProperty(attrname) && (!safe || !obj1.hasOwnProperty(attrname))) { obj1[attrname]=obj2[attrname]; }
269
  }
270
  return obj1;
271
};
272
VideoJS.extend = function(obj){ this.merge(this, obj, true); };
273

    
274
VideoJS.extend({
275
  // Add VideoJS to all video tags with the video-js class when the DOM is ready
276
  setupAllWhenReady: function(options){
277
    // Options is stored globally, and added ot any new player on init
278
    VideoJS.options = options;
279
    VideoJS.DOMReady(VideoJS.setup);
280
  },
281

    
282
  // Run the supplied function when the DOM is ready
283
  DOMReady: function(fn){
284
    VideoJS.addToDOMReady(fn);
285
  },
286

    
287
  // Set up a specific video or array of video elements
288
  // "video" can be:
289
  //    false, undefined, or "All": set up all videos with the video-js class
290
  //    A video tag ID or video tag element: set up one video and return one player
291
  //    An array of video tag elements/IDs: set up each and return an array of players
292
  setup: function(videos, options){
293
    var returnSingular = false,
294
    playerList = [],
295
    videoElement;
296

    
297
    // If videos is undefined or "All", set up all videos with the video-js class
298
    if (!videos || videos == "All") {
299
      videos = VideoJS.getVideoJSTags();
300
    // If videos is not an array, add to an array
301
    } else if (typeof videos != 'object' || videos.nodeType == 1) {
302
      videos = [videos];
303
      returnSingular = true;
304
    }
305

    
306
    // Loop through videos and create players for them
307
    for (var i=0; i<videos.length; i++) {
308
      if (typeof videos[i] == 'string') {
309
        videoElement = document.getElementById(videos[i]);
310
      } else { // assume DOM object
311
        videoElement = videos[i];
312
      }
313
      playerList.push(new VideoJS(videoElement, options));
314
    }
315

    
316
    // Return one or all depending on what was passed in
317
    return (returnSingular) ? playerList[0] : playerList;
318
  },
319

    
320
  // Find video tags with the video-js class
321
  getVideoJSTags: function() {
322
    var videoTags = document.getElementsByTagName("video"),
323
    videoJSTags = [], videoTag;
324

    
325
    for (var i=0,j=videoTags.length; i<j; i++) {
326
      videoTag = videoTags[i];
327
      if (videoTag.className.indexOf("video-js") != -1) {
328
        videoJSTags.push(videoTag);
329
      }
330
    }
331
    return videoJSTags;
332
  },
333

    
334
  // Check if the browser supports video.
335
  browserSupportsVideo: function() {
336
    if (typeof VideoJS.videoSupport != "undefined") { return VideoJS.videoSupport; }
337
    VideoJS.videoSupport = !!document.createElement('video').canPlayType;
338
    return VideoJS.videoSupport;
339
  },
340

    
341
  getFlashVersion: function(){
342
    // Cache Version
343
    if (typeof VideoJS.flashVersion != "undefined") { return VideoJS.flashVersion; }
344
    var version = 0, desc;
345
    if (typeof navigator.plugins != "undefined" && typeof navigator.plugins["Shockwave Flash"] == "object") {
346
      desc = navigator.plugins["Shockwave Flash"].description;
347
      if (desc && !(typeof navigator.mimeTypes != "undefined" && navigator.mimeTypes["application/x-shockwave-flash"] && !navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin)) {
348
        version = parseInt(desc.match(/^.*\s+([^\s]+)\.[^\s]+\s+[^\s]+$/)[1], 10);
349
      }
350
    } else if (typeof window.ActiveXObject != "undefined") {
351
      try {
352
        var testObject = new ActiveXObject("ShockwaveFlash.ShockwaveFlash");
353
        if (testObject) {
354
          version = parseInt(testObject.GetVariable("$version").match(/^[^\s]+\s(\d+)/)[1], 10);
355
        }
356
      }
357
      catch(e) {}
358
    }
359
    VideoJS.flashVersion = version;
360
    return VideoJS.flashVersion;
361
  },
362

    
363
  // Browser & Device Checks
364
  isIE: function(){ return !+"\v1"; },
365
  isIPad: function(){ return navigator.userAgent.match(/iPad/i) !== null; },
366
  isIPhone: function(){ return navigator.userAgent.match(/iPhone/i) !== null; },
367
  isIOS: function(){ return VideoJS.isIPhone() || VideoJS.isIPad(); },
368
  iOSVersion: function() {
369
    var match = navigator.userAgent.match(/OS (\d+)_/i);
370
    if (match && match[1]) { return match[1]; }
371
  },
372
  isAndroid: function(){ return navigator.userAgent.match(/Android/i) !== null; },
373
  androidVersion: function() {
374
    var match = navigator.userAgent.match(/Android (\d+)\./i);
375
    if (match && match[1]) { return match[1]; }
376
  },
377

    
378
  warnings: {
379
    // Safari errors if you call functions on a video that hasn't loaded yet
380
    videoNotReady: "Video is not ready yet (try playing the video first).",
381
    // Getting a QUOTA_EXCEEDED_ERR when setting local storage occasionally
382
    localStorageFull: "Local Storage is Full"
383
  }
384
});
385

    
386
// Shim to make Video tag valid in IE
387
if(VideoJS.isIE()) { document.createElement("video"); }
388

    
389
// Expose to global
390
window.VideoJS = window._V_ = VideoJS;
391

    
392
/* HTML5 Player Type
393
================================================================================ */
394
VideoJS.player.extend({
395
  html5Supported: function(){
396
    if (VideoJS.browserSupportsVideo() && this.canPlaySource()) {
397
      return true;
398
    } else {
399
      return false;
400
    }
401
  },
402
  html5Init: function(){
403
    this.element = this.video;
404

    
405
    this.fixPreloading(); // Support old browsers that used autobuffer
406
    this.supportProgressEvents(); // Support browsers that don't use 'buffered'
407

    
408
    // Set to stored volume OR 85%
409
    this.volume((localStorage && localStorage.volume) || this.options.defaultVolume);
410

    
411
    // Update interface for device needs
412
    if (VideoJS.isIOS()) {
413
      this.options.useBuiltInControls = true;
414
      this.iOSInterface();
415
    } else if (VideoJS.isAndroid()) {
416
      this.options.useBuiltInControls = true;
417
      this.androidInterface();
418
    }
419

    
420
    // Add VideoJS Controls
421
    if (!this.options.useBuiltInControls) {
422
      this.video.controls = false;
423

    
424
      if (this.options.controlsBelow) { _V_.addClass(this.box, "vjs-controls-below"); }
425

    
426
      // Make a click on th video act as a play button
427
      this.activateElement(this.video, "playToggle");
428

    
429
      // Build Interface
430
      this.buildStylesCheckDiv(); // Used to check if style are loaded
431
      this.buildAndActivatePoster();
432
      this.buildBigPlayButton();
433
      this.buildAndActivateSpinner();
434
      this.buildAndActivateControlBar();
435
      this.loadInterface(); // Show everything once styles are loaded
436
      this.getSubtitles();
437
    }
438
  },
439
  /* Source Managemet
440
  ================================================================================ */
441
  canPlaySource: function(){
442
    // Cache Result
443
    if (this.canPlaySourceResult) { return this.canPlaySourceResult; }
444
    // Loop through sources and check if any can play
445
    var children = this.video.children;
446
    for (var i=0,j=children.length; i<j; i++) {
447
      if (children[i].tagName.toUpperCase() == "SOURCE") {
448
        var canPlay = this.video.canPlayType(children[i].type) || this.canPlayExt(children[i].src);
449
        if (canPlay == "probably" || canPlay == "maybe") {
450
          this.firstPlayableSource = children[i];
451
          this.canPlaySourceResult = true;
452
          return true;
453
        }
454
      }
455
    }
456
    this.canPlaySourceResult = false;
457
    return false;
458
  },
459
  // Check if the extention is compatible, for when type won't work
460
  canPlayExt: function(src){
461
    if (!src) { return ""; }
462
    var match = src.match(/\.([^\.]+)$/);
463
    if (match && match[1]) {
464
      var ext = match[1].toLowerCase();
465
      // Android canPlayType doesn't work
466
      if (VideoJS.isAndroid()) {
467
        if (ext == "mp4" || ext == "m4v") { return "maybe"; }
468
      // Allow Apple HTTP Streaming for iOS
469
      } else if (VideoJS.isIOS()) {
470
        if (ext == "m3u8") { return "maybe"; }
471
      }
472
    }
473
    return "";
474
  },
475
  // Force the video source - Helps fix loading bugs in a handful of devices, like the iPad/iPhone poster bug
476
  // And iPad/iPhone javascript include location bug. And Android type attribute bug
477
  forceTheSource: function(){
478
    this.video.src = this.firstPlayableSource.src; // From canPlaySource()
479
    this.video.load();
480
  },
481
  /* Device Fixes
482
  ================================================================================ */
483
  // Support older browsers that used "autobuffer"
484
  fixPreloading: function(){
485
    if (typeof this.video.hasAttribute == "function" && this.video.hasAttribute("preload") && this.video.preload != "none") {
486
      this.video.autobuffer = true; // Was a boolean
487
    } else {
488
      this.video.autobuffer = false;
489
      this.video.preload = "none";
490
    }
491
  },
492

    
493
  // Listen for Video Load Progress (currently does not if html file is local)
494
  // Buffered does't work in all browsers, so watching progress as well
495
  supportProgressEvents: function(e){
496
    _V_.addListener(this.video, 'progress', this.playerOnVideoProgress.context(this));
497
  },
498
  playerOnVideoProgress: function(event){
499
    this.setBufferedFromProgress(event);
500
  },
501
  setBufferedFromProgress: function(event){ // HTML5 Only
502
    if(event.total > 0) {
503
      var newBufferEnd = (event.loaded / event.total) * this.duration();
504
      if (newBufferEnd > this.values.bufferEnd) { this.values.bufferEnd = newBufferEnd; }
505
    }
506
  },
507

    
508
  iOSInterface: function(){
509
    if(VideoJS.iOSVersion() < 4) { this.forceTheSource(); } // Fix loading issues
510
    if(VideoJS.isIPad()) { // iPad could work with controlsBelow
511
      this.buildAndActivateSpinner(); // Spinner still works well on iPad, since iPad doesn't have one
512
    }
513
  },
514

    
515
  // Fix android specific quirks
516
  // Use built-in controls, but add the big play button, since android doesn't have one.
517
  androidInterface: function(){
518
    this.forceTheSource(); // Fix loading issues
519
    _V_.addListener(this.video, "click", function(){ this.play(); }); // Required to play
520
    this.buildBigPlayButton(); // But don't activate the normal way. Pause doesn't work right on android.
521
    _V_.addListener(this.bigPlayButton, "click", function(){ this.play(); }.context(this));
522
    this.positionBox();
523
    this.showBigPlayButtons();
524
  },
525
  /* Wait for styles (TODO: move to _V_)
526
  ================================================================================ */
527
  loadInterface: function(){
528
    if(!this.stylesHaveLoaded()) {
529
      // Don't want to create an endless loop either.
530
      if (!this.positionRetries) { this.positionRetries = 1; }
531
      if (this.positionRetries++ < 100) {
532
        setTimeout(this.loadInterface.context(this),10);
533
        return;
534
      }
535
    }
536
    this.hideStylesCheckDiv();
537
    this.showPoster();
538
    if (this.video.paused !== false) { this.showBigPlayButtons(); }
539
    if (this.options.controlsAtStart) { this.showControlBars(); }
540
    this.positionAll();
541
  },
542
  /* Control Bar
543
  ================================================================================ */
544
  buildAndActivateControlBar: function(){
545
    /* Creating this HTML
546
      <div class="vjs-controls">
547
        <div class="vjs-play-control">
548
          <span></span>
549
        </div>
550
        <div class="vjs-progress-control">
551
          <div class="vjs-progress-holder">
552
            <div class="vjs-load-progress"></div>
553
            <div class="vjs-play-progress"></div>
554
          </div>
555
        </div>
556
        <div class="vjs-time-control">
557
          <span class="vjs-current-time-display">00:00</span><span> / </span><span class="vjs-duration-display">00:00</span>
558
        </div>
559
        <div class="vjs-volume-control">
560
          <div>
561
            <span></span><span></span><span></span><span></span><span></span><span></span>
562
          </div>
563
        </div>
564
        <div class="vjs-fullscreen-control">
565
          <div>
566
            <span></span><span></span><span></span><span></span>
567
          </div>
568
        </div>
569
      </div>
570
    */
571

    
572
    // Create a div to hold the different controls
573
    this.controls = _V_.createElement("div", { className: "vjs-controls" });
574
    // Add the controls to the video's container
575
    this.box.appendChild(this.controls);
576
    this.activateElement(this.controls, "controlBar");
577
    this.activateElement(this.controls, "mouseOverVideoReporter");
578

    
579
    // Build the play control
580
    this.playControl = _V_.createElement("div", { className: "vjs-play-control", innerHTML: "<span></span>" });
581
    this.controls.appendChild(this.playControl);
582
    this.activateElement(this.playControl, "playToggle");
583

    
584
    // Build the progress control
585
    this.progressControl = _V_.createElement("div", { className: "vjs-progress-control" });
586
    this.controls.appendChild(this.progressControl);
587

    
588
    // Create a holder for the progress bars
589
    this.progressHolder = _V_.createElement("div", { className: "vjs-progress-holder" });
590
    this.progressControl.appendChild(this.progressHolder);
591
    this.activateElement(this.progressHolder, "currentTimeScrubber");
592

    
593
    // Create the loading progress display
594
    this.loadProgressBar = _V_.createElement("div", { className: "vjs-load-progress" });
595
    this.progressHolder.appendChild(this.loadProgressBar);
596
    this.activateElement(this.loadProgressBar, "loadProgressBar");
597

    
598
    // Create the playing progress display
599
    this.playProgressBar = _V_.createElement("div", { className: "vjs-play-progress" });
600
    this.progressHolder.appendChild(this.playProgressBar);
601
    this.activateElement(this.playProgressBar, "playProgressBar");
602

    
603
    // Create the progress time display (00:00 / 00:00)
604
    this.timeControl = _V_.createElement("div", { className: "vjs-time-control" });
605
    this.controls.appendChild(this.timeControl);
606

    
607
    // Create the current play time display
608
    this.currentTimeDisplay = _V_.createElement("span", { className: "vjs-current-time-display", innerHTML: "00:00" });
609
    this.timeControl.appendChild(this.currentTimeDisplay);
610
    this.activateElement(this.currentTimeDisplay, "currentTimeDisplay");
611

    
612
    // Add time separator
613
    this.timeSeparator = _V_.createElement("span", { innerHTML: " / " });
614
    this.timeControl.appendChild(this.timeSeparator);
615

    
616
    // Create the total duration display
617
    this.durationDisplay = _V_.createElement("span", { className: "vjs-duration-display", innerHTML: "00:00" });
618
    this.timeControl.appendChild(this.durationDisplay);
619
    this.activateElement(this.durationDisplay, "durationDisplay");
620

    
621
    // Create the volumne control
622
    this.volumeControl = _V_.createElement("div", {
623
      className: "vjs-volume-control",
624
      innerHTML: "<div><span></span><span></span><span></span><span></span><span></span><span></span></div>"
625
    });
626
    this.controls.appendChild(this.volumeControl);
627
    this.activateElement(this.volumeControl, "volumeScrubber");
628

    
629
    this.volumeDisplay = this.volumeControl.children[0];
630
    this.activateElement(this.volumeDisplay, "volumeDisplay");
631

    
632
    // Crete the fullscreen control
633
    this.fullscreenControl = _V_.createElement("div", {
634
      className: "vjs-fullscreen-control",
635
      innerHTML: "<div><span></span><span></span><span></span><span></span></div>"
636
    });
637
    this.controls.appendChild(this.fullscreenControl);
638
    this.activateElement(this.fullscreenControl, "fullscreenToggle");
639
  },
640
  /* Poster Image
641
  ================================================================================ */
642
  buildAndActivatePoster: function(){
643
    this.updatePosterSource();
644
    if (this.video.poster) {
645
      this.poster = document.createElement("img");
646
      // Add poster to video box
647
      this.box.appendChild(this.poster);
648

    
649
      // Add poster image data
650
      this.poster.src = this.video.poster;
651
      // Add poster styles
652
      this.poster.className = "vjs-poster";
653
      this.activateElement(this.poster, "poster");
654
    } else {
655
      this.poster = false;
656
    }
657
  },
658
  /* Big Play Button
659
  ================================================================================ */
660
  buildBigPlayButton: function(){
661
    /* Creating this HTML
662
      <div class="vjs-big-play-button"><span></span></div>
663
    */
664
    this.bigPlayButton = _V_.createElement("div", {
665
      className: "vjs-big-play-button",
666
      innerHTML: "<span></span>"
667
    });
668
    this.box.appendChild(this.bigPlayButton);
669
    this.activateElement(this.bigPlayButton, "bigPlayButton");
670
  },
671
  /* Spinner (Loading)
672
  ================================================================================ */
673
  buildAndActivateSpinner: function(){
674
    this.spinner = _V_.createElement("div", {
675
      className: "vjs-spinner",
676
      innerHTML: "<div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div>"
677
    });
678
    this.box.appendChild(this.spinner);
679
    this.activateElement(this.spinner, "spinner");
680
  },
681
  /* Styles Check - Check if styles are loaded (move ot _V_)
682
  ================================================================================ */
683
  // Sometimes the CSS styles haven't been applied to the controls yet
684
  // when we're trying to calculate the height and position them correctly.
685
  // This causes a flicker where the controls are out of place.
686
  buildStylesCheckDiv: function(){
687
    this.stylesCheckDiv = _V_.createElement("div", { className: "vjs-styles-check" });
688
    this.stylesCheckDiv.style.position = "absolute";
689
    this.box.appendChild(this.stylesCheckDiv);
690
  },
691
  hideStylesCheckDiv: function(){ this.stylesCheckDiv.style.display = "none"; },
692
  stylesHaveLoaded: function(){
693
    if (this.stylesCheckDiv.offsetHeight != 5) {
694
       return false;
695
    } else {
696
      return true;
697
    }
698
  },
699
  /* VideoJS Box - Holds all elements
700
  ================================================================================ */
701
  positionAll: function(){
702
    this.positionBox();
703
    this.positionControlBars();
704
    this.positionPoster();
705
  },
706
  positionBox: function(){
707
    // Set width based on fullscreen or not.
708
    if (this.videoIsFullScreen) {
709
      this.box.style.width = "";
710
      this.element.style.height="";
711
      this.box.style.height="";
712
      if (this.options.controlsBelow) {
713
        this.box.style.height = "";
714
        this.element.style.height = (this.box.offsetHeight - this.controls.offsetHeight) + "px";
715
      }
716
    } else {
717
      this.box.style.width = this.width() + "px";
718
      this.element.style.height=this.height()+"px";
719
      if (this.options.controlsBelow) {
720
        this.element.style.height = "";
721
        // this.box.style.height = this.video.offsetHeight + this.controls.offsetHeight + "px";
722
      }
723
    }
724
  },
725
  /* Subtitles
726
  ================================================================================ */
727
  getSubtitles: function(){
728
    var tracks = this.video.getElementsByTagName("TRACK");
729
    for (var i=0,j=tracks.length; i<j; i++) {
730
      if (tracks[i].getAttribute("kind") == "subtitles" && tracks[i].getAttribute("src")) {
731
        this.subtitlesSource = tracks[i].getAttribute("src");
732
        this.loadSubtitles();
733
        this.buildSubtitles();
734
      }
735
    }
736
  },
737
  loadSubtitles: function() { _V_.get(this.subtitlesSource, this.parseSubtitles.context(this)); },
738
  parseSubtitles: function(subText) {
739
    var lines = subText.split("\n"),
740
        line = "",
741
        subtitle, time, text;
742
    this.subtitles = [];
743
    this.currentSubtitle = false;
744
    this.lastSubtitleIndex = 0;
745

    
746
    for (var i=0; i<lines.length; i++) {
747
      line = _V_.trim(lines[i]); // Trim whitespace and linebreaks
748
      if (line) { // Loop until a line with content
749

    
750
        // First line - Number
751
        subtitle = {
752
          id: line, // Subtitle Number
753
          index: this.subtitles.length // Position in Array
754
        };
755

    
756
        // Second line - Time
757
        line = _V_.trim(lines[++i]);
758
        time = line.split(" --> ");
759
        subtitle.start = this.parseSubtitleTime(time[0]);
760
        subtitle.end = this.parseSubtitleTime(time[1]);
761

    
762
        // Additional lines - Subtitle Text
763
        text = [];
764
        for (var j=i; j<lines.length; j++) { // Loop until a blank line or end of lines
765
          line = _V_.trim(lines[++i]);
766
          if (!line) { break; }
767
          text.push(line);
768
        }
769
        subtitle.text = text.join('<br/>');
770

    
771
        // Add this subtitle
772
        this.subtitles.push(subtitle);
773
      }
774
    }
775
  },
776

    
777
  parseSubtitleTime: function(timeText) {
778
    var parts = timeText.split(':'),
779
        time = 0;
780
    // hours => seconds
781
    time += parseFloat(parts[0])*60*60;
782
    // minutes => seconds
783
    time += parseFloat(parts[1])*60;
784
    // get seconds
785
    var seconds = parts[2].split(/\.|,/); // Either . or ,
786
    time += parseFloat(seconds[0]);
787
    // add miliseconds
788
    ms = parseFloat(seconds[1]);
789
    if (ms) { time += ms/1000; }
790
    return time;
791
  },
792

    
793
  buildSubtitles: function(){
794
    /* Creating this HTML
795
      <div class="vjs-subtitles"></div>
796
    */
797
    this.subtitlesDisplay = _V_.createElement("div", { className: 'vjs-subtitles' });
798
    this.box.appendChild(this.subtitlesDisplay);
799
    this.activateElement(this.subtitlesDisplay, "subtitlesDisplay");
800
  },
801

    
802
  /* Player API - Translate functionality from player to video
803
  ================================================================================ */
804
  addVideoListener: function(type, fn){ _V_.addListener(this.video, type, fn.rEvtContext(this)); },
805

    
806
  play: function(){
807
    this.video.play();
808
    return this;
809
  },
810
  onPlay: function(fn){ this.addVideoListener("play", fn); return this; },
811

    
812
  pause: function(){
813
    this.video.pause();
814
    return this;
815
  },
816
  onPause: function(fn){ this.addVideoListener("pause", fn); return this; },
817
  paused: function() { return this.video.paused; },
818

    
819
  currentTime: function(seconds){
820
    if (seconds !== undefined) {
821
      try { this.video.currentTime = seconds; }
822
      catch(e) { this.warning(VideoJS.warnings.videoNotReady); }
823
      this.values.currentTime = seconds;
824
      return this;
825
    }
826
    return this.video.currentTime;
827
  },
828
  onCurrentTimeUpdate: function(fn){
829
    this.currentTimeListeners.push(fn);
830
  },
831

    
832
  duration: function(){
833
    return this.video.duration;
834
  },
835

    
836
  buffered: function(){
837
    // Storing values allows them be overridden by setBufferedFromProgress
838
    if (this.values.bufferStart === undefined) {
839
      this.values.bufferStart = 0;
840
      this.values.bufferEnd = 0;
841
    }
842
    if (this.video.buffered && this.video.buffered.length > 0) {
843
      var newEnd = this.video.buffered.end(0);
844
      if (newEnd > this.values.bufferEnd) { this.values.bufferEnd = newEnd; }
845
    }
846
    return [this.values.bufferStart, this.values.bufferEnd];
847
  },
848

    
849
  volume: function(percentAsDecimal){
850
    if (percentAsDecimal !== undefined) {
851
      // Force value to between 0 and 1
852
      this.values.volume = Math.max(0, Math.min(1, parseFloat(percentAsDecimal)));
853
      this.video.volume = this.values.volume;
854
      this.setLocalStorage("volume", this.values.volume);
855
      return this;
856
    }
857
    if (this.values.volume) { return this.values.volume; }
858
    return this.video.volume;
859
  },
860
  onVolumeChange: function(fn){ _V_.addListener(this.video, 'volumechange', fn.rEvtContext(this)); },
861

    
862
  width: function(width){
863
    if (width !== undefined) {
864
      this.video.width = width; // Not using style so it can be overridden on fullscreen.
865
      this.box.style.width = width+"px";
866
      this.triggerResizeListeners();
867
      return this;
868
    }
869
    return this.video.offsetWidth;
870
  },
871
  height: function(height){
872
    if (height !== undefined) {
873
      this.video.height = height;
874
      this.box.style.height = height+"px";
875
      this.triggerResizeListeners();
876
      return this;
877
    }
878
    return this.video.offsetHeight;
879
  },
880

    
881
  supportsFullScreen: function(){
882
    if(typeof this.video.webkitEnterFullScreen == 'function') {
883
      // Seems to be broken in Chromium/Chrome
884
      if (!navigator.userAgent.match("Chrome") && !navigator.userAgent.match("Mac OS X 10.5")) {
885
        return true;
886
      }
887
    }
888
    return false;
889
  },
890

    
891
  html5EnterNativeFullScreen: function(){
892
    try {
893
      this.video.webkitEnterFullScreen();
894
    } catch (e) {
895
      if (e.code == 11) { this.warning(VideoJS.warnings.videoNotReady); }
896
    }
897
    return this;
898
  },
899

    
900
  // Turn on fullscreen (window) mode
901
  // Real fullscreen isn't available in browsers quite yet.
902
  enterFullScreen: function(){
903
    if (this.supportsFullScreen()) {
904
      this.html5EnterNativeFullScreen();
905
    } else {
906
      this.enterFullWindow();
907
    }
908
  },
909

    
910
  exitFullScreen: function(){
911
    if (this.supportsFullScreen()) {
912
      // Shouldn't be called
913
    } else {
914
      this.exitFullWindow();
915
    }
916
  },
917

    
918
  enterFullWindow: function(){
919
    this.videoIsFullScreen = true;
920
    // Storing original doc overflow value to return to when fullscreen is off
921
    this.docOrigOverflow = document.documentElement.style.overflow;
922
    // Add listener for esc key to exit fullscreen
923
    _V_.addListener(document, "keydown", this.fullscreenOnEscKey.rEvtContext(this));
924
    // Add listener for a window resize
925
    _V_.addListener(window, "resize", this.fullscreenOnWindowResize.rEvtContext(this));
926
    // Hide any scroll bars
927
    document.documentElement.style.overflow = 'hidden';
928
    // Apply fullscreen styles
929
    _V_.addClass(this.box, "vjs-fullscreen");
930
    // Resize the box, controller, and poster
931
    this.positionAll();
932
  },
933

    
934
  // Turn off fullscreen (window) mode
935
  exitFullWindow: function(){
936
    this.videoIsFullScreen = false;
937
    document.removeEventListener("keydown", this.fullscreenOnEscKey, false);
938
    window.removeEventListener("resize", this.fullscreenOnWindowResize, false);
939
    // Unhide scroll bars.
940
    document.documentElement.style.overflow = this.docOrigOverflow;
941
    // Remove fullscreen styles
942
    _V_.removeClass(this.box, "vjs-fullscreen");
943
    // Resize the box, controller, and poster to original sizes
944
    this.positionAll();
945
  },
946

    
947
  onError: function(fn){ this.addVideoListener("error", fn); return this; },
948
  onEnded: function(fn){
949
    this.addVideoListener("ended", fn); return this;
950
  }
951
});
952

    
953
////////////////////////////////////////////////////////////////////////////////
954
// Element Behaviors
955
// Tell elements how to act or react
956
////////////////////////////////////////////////////////////////////////////////
957

    
958
/* Player Behaviors - How VideoJS reacts to what the video is doing.
959
================================================================================ */
960
VideoJS.player.newBehavior("player", function(player){
961
    this.onError(this.playerOnVideoError);
962
    // Listen for when the video is played
963
    this.onPlay(this.playerOnVideoPlay);
964
    this.onPlay(this.trackCurrentTime);
965
    // Listen for when the video is paused
966
    this.onPause(this.playerOnVideoPause);
967
    this.onPause(this.stopTrackingCurrentTime);
968
    // Listen for when the video ends
969
    this.onEnded(this.playerOnVideoEnded);
970
    // Set interval for load progress using buffer watching method
971
    // this.trackCurrentTime();
972
    this.trackBuffered();
973
    // Buffer Full
974
    this.onBufferedUpdate(this.isBufferFull);
975
  },{
976
    playerOnVideoError: function(event){
977
      this.log(event);
978
      this.log(this.video.error);
979
    },
980
    playerOnVideoPlay: function(event){ this.hasPlayed = true; },
981
    playerOnVideoPause: function(event){},
982
    playerOnVideoEnded: function(event){
983
      this.currentTime(0);
984
      this.pause();
985
    },
986

    
987
    /* Load Tracking -------------------------------------------------------------- */
988
    // Buffer watching method for load progress.
989
    // Used for browsers that don't support the progress event
990
    trackBuffered: function(){
991
      this.bufferedInterval = setInterval(this.triggerBufferedListeners.context(this), 500);
992
    },
993
    stopTrackingBuffered: function(){ clearInterval(this.bufferedInterval); },
994
    bufferedListeners: [],
995
    onBufferedUpdate: function(fn){
996
      this.bufferedListeners.push(fn);
997
    },
998
    triggerBufferedListeners: function(){
999
      this.isBufferFull();
1000
      this.each(this.bufferedListeners, function(listener){
1001
        (listener.context(this))();
1002
      });
1003
    },
1004
    isBufferFull: function(){
1005
      if (this.bufferedPercent() == 1) { this.stopTrackingBuffered(); }
1006
    },
1007

    
1008
    /* Time Tracking -------------------------------------------------------------- */
1009
    trackCurrentTime: function(){
1010
      if (this.currentTimeInterval) { clearInterval(this.currentTimeInterval); }
1011
      this.currentTimeInterval = setInterval(this.triggerCurrentTimeListeners.context(this), 100); // 42 = 24 fps
1012
      this.trackingCurrentTime = true;
1013
    },
1014
    // Turn off play progress tracking (when paused or dragging)
1015
    stopTrackingCurrentTime: function(){
1016
      clearInterval(this.currentTimeInterval);
1017
      this.trackingCurrentTime = false;
1018
    },
1019
    currentTimeListeners: [],
1020
    // onCurrentTimeUpdate is in API section now
1021
    triggerCurrentTimeListeners: function(late, newTime){ // FF passes milliseconds late as the first argument
1022
      this.each(this.currentTimeListeners, function(listener){
1023
        (listener.context(this))(newTime || this.currentTime());
1024
      });
1025
    },
1026

    
1027
    /* Resize Tracking -------------------------------------------------------------- */
1028
    resizeListeners: [],
1029
    onResize: function(fn){
1030
      this.resizeListeners.push(fn);
1031
    },
1032
    // Trigger anywhere the video/box size is changed.
1033
    triggerResizeListeners: function(){
1034
      this.each(this.resizeListeners, function(listener){
1035
        (listener.context(this))();
1036
      });
1037
    }
1038
  }
1039
);
1040
/* Mouse Over Video Reporter Behaviors - i.e. Controls hiding based on mouse location
1041
================================================================================ */
1042
VideoJS.player.newBehavior("mouseOverVideoReporter", function(element){
1043
    // Listen for the mouse move the video. Used to reveal the controller.
1044
    _V_.addListener(element, "mousemove", this.mouseOverVideoReporterOnMouseMove.context(this));
1045
    // Listen for the mouse moving out of the video. Used to hide the controller.
1046
    _V_.addListener(element, "mouseout", this.mouseOverVideoReporterOnMouseOut.context(this));
1047
  },{
1048
    mouseOverVideoReporterOnMouseMove: function(){
1049
      this.showControlBars();
1050
      clearInterval(this.mouseMoveTimeout);
1051
      this.mouseMoveTimeout = setTimeout(this.hideControlBars.context(this), 4000);
1052
    },
1053
    mouseOverVideoReporterOnMouseOut: function(event){
1054
      // Prevent flicker by making sure mouse hasn't left the video
1055
      var parent = event.relatedTarget;
1056
      while (parent && parent !== this.box) {
1057
        parent = parent.parentNode;
1058
      }
1059
      if (parent !== this.box) {
1060
        this.hideControlBars();
1061
      }
1062
    }
1063
  }
1064
);
1065
/* Mouse Over Video Reporter Behaviors - i.e. Controls hiding based on mouse location
1066
================================================================================ */
1067
VideoJS.player.newBehavior("box", function(element){
1068
    this.positionBox();
1069
    _V_.addClass(element, "vjs-paused");
1070
    this.activateElement(element, "mouseOverVideoReporter");
1071
    this.onPlay(this.boxOnVideoPlay);
1072
    this.onPause(this.boxOnVideoPause);
1073
  },{
1074
    boxOnVideoPlay: function(){
1075
      _V_.removeClass(this.box, "vjs-paused");
1076
      _V_.addClass(this.box, "vjs-playing");
1077
    },
1078
    boxOnVideoPause: function(){
1079
      _V_.removeClass(this.box, "vjs-playing");
1080
      _V_.addClass(this.box, "vjs-paused");
1081
    }
1082
  }
1083
);
1084
/* Poster Image Overlay
1085
================================================================================ */
1086
VideoJS.player.newBehavior("poster", function(element){
1087
    this.activateElement(element, "mouseOverVideoReporter");
1088
    this.activateElement(element, "playButton");
1089
    this.onPlay(this.hidePoster);
1090
    this.onEnded(this.showPoster);
1091
    this.onResize(this.positionPoster);
1092
  },{
1093
    showPoster: function(){
1094
      if (!this.poster) { return; }
1095
      this.poster.style.display = "block";
1096
      this.positionPoster();
1097
    },
1098
    positionPoster: function(){
1099
      // Only if the poster is visible
1100
      if (!this.poster || this.poster.style.display == 'none') { return; }
1101
      this.poster.style.height = this.height() + "px"; // Need incase controlsBelow
1102
      this.poster.style.width = this.width() + "px"; // Could probably do 100% of box
1103
    },
1104
    hidePoster: function(){
1105
      if (!this.poster) { return; }
1106
      this.poster.style.display = "none";
1107
    },
1108
    // Update poster source from attribute or fallback image
1109
    // iPad breaks if you include a poster attribute, so this fixes that
1110
    updatePosterSource: function(){
1111
      if (!this.video.poster) {
1112
        var images = this.video.getElementsByTagName("img");
1113
        if (images.length > 0) { this.video.poster = images[0].src; }
1114
      }
1115
    }
1116
  }
1117
);
1118
/* Control Bar Behaviors
1119
================================================================================ */
1120
VideoJS.player.newBehavior("controlBar", function(element){
1121
    if (!this.controlBars) {
1122
      this.controlBars = [];
1123
      this.onResize(this.positionControlBars);
1124
    }
1125
    this.controlBars.push(element);
1126
    _V_.addListener(element, "mousemove", this.onControlBarsMouseMove.context(this));
1127
    _V_.addListener(element, "mouseout", this.onControlBarsMouseOut.context(this));
1128
  },{
1129
    showControlBars: function(){
1130
      if (!this.options.controlsAtStart && !this.hasPlayed) { return; }
1131
      this.each(this.controlBars, function(bar){
1132
        bar.style.display = "block";
1133
      });
1134
    },
1135
    // Place controller relative to the video's position (now just resizing bars)
1136
    positionControlBars: function(){
1137
      this.updatePlayProgressBars();
1138
      this.updateLoadProgressBars();
1139
    },
1140
    hideControlBars: function(){
1141
      if (this.options.controlsHiding && !this.mouseIsOverControls) {
1142
        this.each(this.controlBars, function(bar){
1143
          bar.style.display = "none";
1144
        });
1145
      }
1146
    },
1147
    // Block controls from hiding when mouse is over them.
1148
    onControlBarsMouseMove: function(){ this.mouseIsOverControls = true; },
1149
    onControlBarsMouseOut: function(event){
1150
      this.mouseIsOverControls = false;
1151
    }
1152
  }
1153
);
1154
/* PlayToggle, PlayButton, PauseButton Behaviors
1155
================================================================================ */
1156
// Play Toggle
1157
VideoJS.player.newBehavior("playToggle", function(element){
1158
    if (!this.elements.playToggles) {
1159
      this.elements.playToggles = [];
1160
      this.onPlay(this.playTogglesOnPlay);
1161
      this.onPause(this.playTogglesOnPause);
1162
    }
1163
    this.elements.playToggles.push(element);
1164
    _V_.addListener(element, "click", this.onPlayToggleClick.context(this));
1165
  },{
1166
    onPlayToggleClick: function(event){
1167
      if (this.paused()) {
1168
        this.play();
1169
      } else {
1170
        this.pause();
1171
      }
1172
    },
1173
    playTogglesOnPlay: function(event){
1174
      this.each(this.elements.playToggles, function(toggle){
1175
        _V_.removeClass(toggle, "vjs-paused");
1176
        _V_.addClass(toggle, "vjs-playing");
1177
      });
1178
    },
1179
    playTogglesOnPause: function(event){
1180
      this.each(this.elements.playToggles, function(toggle){
1181
        _V_.removeClass(toggle, "vjs-playing");
1182
        _V_.addClass(toggle, "vjs-paused");
1183
      });
1184
    }
1185
  }
1186
);
1187
// Play
1188
VideoJS.player.newBehavior("playButton", function(element){
1189
    _V_.addListener(element, "click", this.onPlayButtonClick.context(this));
1190
  },{
1191
    onPlayButtonClick: function(event){ this.play(); }
1192
  }
1193
);
1194
// Pause
1195
VideoJS.player.newBehavior("pauseButton", function(element){
1196
    _V_.addListener(element, "click", this.onPauseButtonClick.context(this));
1197
  },{
1198
    onPauseButtonClick: function(event){ this.pause(); }
1199
  }
1200
);
1201
/* Play Progress Bar Behaviors
1202
================================================================================ */
1203
VideoJS.player.newBehavior("playProgressBar", function(element){
1204
    if (!this.playProgressBars) {
1205
      this.playProgressBars = [];
1206
      this.onCurrentTimeUpdate(this.updatePlayProgressBars);
1207
    }
1208
    this.playProgressBars.push(element);
1209
  },{
1210
    // Ajust the play progress bar's width based on the current play time
1211
    updatePlayProgressBars: function(newTime){
1212
      var progress = (newTime !== undefined) ? newTime / this.duration() : this.currentTime() / this.duration();
1213
      if (isNaN(progress)) { progress = 0; }
1214
      this.each(this.playProgressBars, function(bar){
1215
        if (bar.style) { bar.style.width = _V_.round(progress * 100, 2) + "%"; }
1216
      });
1217
    }
1218
  }
1219
);
1220
/* Load Progress Bar Behaviors
1221
================================================================================ */
1222
VideoJS.player.newBehavior("loadProgressBar", function(element){
1223
    if (!this.loadProgressBars) { this.loadProgressBars = []; }
1224
    this.loadProgressBars.push(element);
1225
    this.onBufferedUpdate(this.updateLoadProgressBars);
1226
  },{
1227
    updateLoadProgressBars: function(){
1228
      this.each(this.loadProgressBars, function(bar){
1229
        if (bar.style) { bar.style.width = _V_.round(this.bufferedPercent() * 100, 2) + "%"; }
1230
      });
1231
    }
1232
  }
1233
);
1234

    
1235
/* Current Time Display Behaviors
1236
================================================================================ */
1237
VideoJS.player.newBehavior("currentTimeDisplay", function(element){
1238
    if (!this.currentTimeDisplays) {
1239
      this.currentTimeDisplays = [];
1240
      this.onCurrentTimeUpdate(this.updateCurrentTimeDisplays);
1241
    }
1242
    this.currentTimeDisplays.push(element);
1243
  },{
1244
    // Update the displayed time (00:00)
1245
    updateCurrentTimeDisplays: function(newTime){
1246
      if (!this.currentTimeDisplays) { return; }
1247
      // Allows for smooth scrubbing, when player can't keep up.
1248
      var time = (newTime) ? newTime : this.currentTime();
1249
      this.each(this.currentTimeDisplays, function(dis){
1250
        dis.innerHTML = _V_.formatTime(time);
1251
      });
1252
    }
1253
  }
1254
);
1255

    
1256
/* Duration Display Behaviors
1257
================================================================================ */
1258
VideoJS.player.newBehavior("durationDisplay", function(element){
1259
    if (!this.durationDisplays) {
1260
      this.durationDisplays = [];
1261
      this.onCurrentTimeUpdate(this.updateDurationDisplays);
1262
    }
1263
    this.durationDisplays.push(element);
1264
  },{
1265
    updateDurationDisplays: function(){
1266
      if (!this.durationDisplays) { return; }
1267
      this.each(this.durationDisplays, function(dis){
1268
        if (this.duration()) { dis.innerHTML = _V_.formatTime(this.duration()); }
1269
      });
1270
    }
1271
  }
1272
);
1273

    
1274
/* Current Time Scrubber Behaviors
1275
================================================================================ */
1276
VideoJS.player.newBehavior("currentTimeScrubber", function(element){
1277
    _V_.addListener(element, "mousedown", this.onCurrentTimeScrubberMouseDown.rEvtContext(this));
1278
  },{
1279
    // Adjust the play position when the user drags on the progress bar
1280
    onCurrentTimeScrubberMouseDown: function(event, scrubber){
1281
      event.preventDefault();
1282
      this.currentScrubber = scrubber;
1283

    
1284
      this.stopTrackingCurrentTime(); // Allows for smooth scrubbing
1285

    
1286
      this.videoWasPlaying = !this.paused();
1287
      this.pause();
1288

    
1289
      _V_.blockTextSelection();
1290
      this.setCurrentTimeWithScrubber(event);
1291
      _V_.addListener(document, "mousemove", this.onCurrentTimeScrubberMouseMove.rEvtContext(this));
1292
      _V_.addListener(document, "mouseup", this.onCurrentTimeScrubberMouseUp.rEvtContext(this));
1293
    },
1294
    onCurrentTimeScrubberMouseMove: function(event){ // Removeable
1295
      this.setCurrentTimeWithScrubber(event);
1296
    },
1297
    onCurrentTimeScrubberMouseUp: function(event){ // Removeable
1298
      _V_.unblockTextSelection();
1299
      document.removeEventListener("mousemove", this.onCurrentTimeScrubberMouseMove, false);
1300
      document.removeEventListener("mouseup", this.onCurrentTimeScrubberMouseUp, false);
1301
      if (this.videoWasPlaying) {
1302
        this.play();
1303
        this.trackCurrentTime();
1304
      }
1305
    },
1306
    setCurrentTimeWithScrubber: function(event){
1307
      var newProgress = _V_.getRelativePosition(event.pageX, this.currentScrubber);
1308
      var newTime = newProgress * this.duration();
1309
      this.triggerCurrentTimeListeners(0, newTime); // Allows for smooth scrubbing
1310
      // Don't let video end while scrubbing.
1311
      if (newTime == this.duration()) { newTime = newTime - 0.1; }
1312
      this.currentTime(newTime);
1313
    }
1314
  }
1315
);
1316
/* Volume Display Behaviors
1317
================================================================================ */
1318
VideoJS.player.newBehavior("volumeDisplay", function(element){
1319
    if (!this.volumeDisplays) {
1320
      this.volumeDisplays = [];
1321
      this.onVolumeChange(this.updateVolumeDisplays);
1322
    }
1323
    this.volumeDisplays.push(element);
1324
    this.updateVolumeDisplay(element); // Set the display to the initial volume
1325
  },{
1326
    // Update the volume control display
1327
    // Unique to these default controls. Uses borders to create the look of bars.
1328
    updateVolumeDisplays: function(){
1329
      if (!this.volumeDisplays) { return; }
1330
      this.each(this.volumeDisplays, function(dis){
1331
        this.updateVolumeDisplay(dis);
1332
      });
1333
    },
1334
    updateVolumeDisplay: function(display){
1335
      var volNum = Math.ceil(this.volume() * 6);
1336
      this.each(display.children, function(child, num){
1337
        if (num < volNum) {
1338
          _V_.addClass(child, "vjs-volume-level-on");
1339
        } else {
1340
          _V_.removeClass(child, "vjs-volume-level-on");
1341
        }
1342
      });
1343
    }
1344
  }
1345
);
1346
/* Volume Scrubber Behaviors
1347
================================================================================ */
1348
VideoJS.player.newBehavior("volumeScrubber", function(element){
1349
    _V_.addListener(element, "mousedown", this.onVolumeScrubberMouseDown.rEvtContext(this));
1350
  },{
1351
    // Adjust the volume when the user drags on the volume control
1352
    onVolumeScrubberMouseDown: function(event, scrubber){
1353
      // event.preventDefault();
1354
      _V_.blockTextSelection();
1355
      this.currentScrubber = scrubber;
1356
      this.setVolumeWithScrubber(event);
1357
      _V_.addListener(document, "mousemove", this.onVolumeScrubberMouseMove.rEvtContext(this));
1358
      _V_.addListener(document, "mouseup", this.onVolumeScrubberMouseUp.rEvtContext(this));
1359
    },
1360
    onVolumeScrubberMouseMove: function(event){
1361
      this.setVolumeWithScrubber(event);
1362
    },
1363
    onVolumeScrubberMouseUp: function(event){
1364
      this.setVolumeWithScrubber(event);
1365
      _V_.unblockTextSelection();
1366
      document.removeEventListener("mousemove", this.onVolumeScrubberMouseMove, false);
1367
      document.removeEventListener("mouseup", this.onVolumeScrubberMouseUp, false);
1368
    },
1369
    setVolumeWithScrubber: function(event){
1370
      var newVol = _V_.getRelativePosition(event.pageX, this.currentScrubber);
1371
      this.volume(newVol);
1372
    }
1373
  }
1374
);
1375
/* Fullscreen Toggle Behaviors
1376
================================================================================ */
1377
VideoJS.player.newBehavior("fullscreenToggle", function(element){
1378
    _V_.addListener(element, "click", this.onFullscreenToggleClick.context(this));
1379
  },{
1380
    // When the user clicks on the fullscreen button, update fullscreen setting
1381
    onFullscreenToggleClick: function(event){
1382
      if (!this.videoIsFullScreen) {
1383
        this.enterFullScreen();
1384
      } else {
1385
        this.exitFullScreen();
1386
      }
1387
    },
1388

    
1389
    fullscreenOnWindowResize: function(event){ // Removeable
1390
      this.positionControlBars();
1391
    },
1392
    // Create listener for esc key while in full screen mode
1393
    fullscreenOnEscKey: function(event){ // Removeable
1394
      if (event.keyCode == 27) {
1395
        this.exitFullScreen();
1396
      }
1397
    }
1398
  }
1399
);
1400
/* Big Play Button Behaviors
1401
================================================================================ */
1402
VideoJS.player.newBehavior("bigPlayButton", function(element){
1403
    if (!this.elements.bigPlayButtons) {
1404
      this.elements.bigPlayButtons = [];
1405
      this.onPlay(this.bigPlayButtonsOnPlay);
1406
      this.onEnded(this.bigPlayButtonsOnEnded);
1407
    }
1408
    this.elements.bigPlayButtons.push(element);
1409
    this.activateElement(element, "playButton");
1410
  },{
1411
    bigPlayButtonsOnPlay: function(event){ this.hideBigPlayButtons(); },
1412
    bigPlayButtonsOnEnded: function(event){ this.showBigPlayButtons(); },
1413
    showBigPlayButtons: function(){
1414
      this.each(this.elements.bigPlayButtons, function(element){
1415
        element.style.display = "block";
1416
      });
1417
    },
1418
    hideBigPlayButtons: function(){
1419
      this.each(this.elements.bigPlayButtons, function(element){
1420
        element.style.display = "none";
1421
      });
1422
    }
1423
  }
1424
);
1425
/* Spinner
1426
================================================================================ */
1427
VideoJS.player.newBehavior("spinner", function(element){
1428
    if (!this.spinners) {
1429
      this.spinners = [];
1430
      _V_.addListener(this.video, "loadeddata", this.spinnersOnVideoLoadedData.context(this));
1431
      _V_.addListener(this.video, "loadstart", this.spinnersOnVideoLoadStart.context(this));
1432
      _V_.addListener(this.video, "seeking", this.spinnersOnVideoSeeking.context(this));
1433
      _V_.addListener(this.video, "seeked", this.spinnersOnVideoSeeked.context(this));
1434
      _V_.addListener(this.video, "canplay", this.spinnersOnVideoCanPlay.context(this));
1435
      _V_.addListener(this.video, "canplaythrough", this.spinnersOnVideoCanPlayThrough.context(this));
1436
      _V_.addListener(this.video, "waiting", this.spinnersOnVideoWaiting.context(this));
1437
      _V_.addListener(this.video, "stalled", this.spinnersOnVideoStalled.context(this));
1438
      _V_.addListener(this.video, "suspend", this.spinnersOnVideoSuspend.context(this));
1439
      _V_.addListener(this.video, "playing", this.spinnersOnVideoPlaying.context(this));
1440
      _V_.addListener(this.video, "timeupdate", this.spinnersOnVideoTimeUpdate.context(this));
1441
    }
1442
    this.spinners.push(element);
1443
  },{
1444
    showSpinners: function(){
1445
      this.each(this.spinners, function(spinner){
1446
        spinner.style.display = "block";
1447
      });
1448
      clearInterval(this.spinnerInterval);
1449
      this.spinnerInterval = setInterval(this.rotateSpinners.context(this), 100);
1450
    },
1451
    hideSpinners: function(){
1452
      this.each(this.spinners, function(spinner){
1453
        spinner.style.display = "none";
1454
      });
1455
      clearInterval(this.spinnerInterval);
1456
    },
1457
    spinnersRotated: 0,
1458
    rotateSpinners: function(){
1459
      this.each(this.spinners, function(spinner){
1460
        // spinner.style.transform =       'scale(0.5) rotate('+this.spinnersRotated+'deg)';
1461
        spinner.style.WebkitTransform = 'scale(0.5) rotate('+this.spinnersRotated+'deg)';
1462
        spinner.style.MozTransform =    'scale(0.5) rotate('+this.spinnersRotated+'deg)';
1463
      });
1464
      if (this.spinnersRotated == 360) { this.spinnersRotated = 0; }
1465
      this.spinnersRotated += 45;
1466
    },
1467
    spinnersOnVideoLoadedData: function(event){ this.hideSpinners(); },
1468
    spinnersOnVideoLoadStart: function(event){ this.showSpinners(); },
1469
    spinnersOnVideoSeeking: function(event){ /* this.showSpinners(); */ },
1470
    spinnersOnVideoSeeked: function(event){ /* this.hideSpinners(); */ },
1471
    spinnersOnVideoCanPlay: function(event){ /* this.hideSpinners(); */ },
1472
    spinnersOnVideoCanPlayThrough: function(event){ this.hideSpinners(); },
1473
    spinnersOnVideoWaiting: function(event){
1474
      // Safari sometimes triggers waiting inappropriately
1475
      // Like after video has played, any you play again.
1476
      this.showSpinners();
1477
    },
1478
    spinnersOnVideoStalled: function(event){},
1479
    spinnersOnVideoSuspend: function(event){},
1480
    spinnersOnVideoPlaying: function(event){ this.hideSpinners(); },
1481
    spinnersOnVideoTimeUpdate: function(event){
1482
      // Safari sometimes calls waiting and doesn't recover
1483
      if(this.spinner.style.display == "block") { this.hideSpinners(); }
1484
    }
1485
  }
1486
);
1487
/* Subtitles
1488
================================================================================ */
1489
VideoJS.player.newBehavior("subtitlesDisplay", function(element){
1490
    if (!this.subtitleDisplays) {
1491
      this.subtitleDisplays = [];
1492
      this.onCurrentTimeUpdate(this.subtitleDisplaysOnVideoTimeUpdate);
1493
      this.onEnded(function() { this.lastSubtitleIndex = 0; }.context(this));
1494
    }
1495
    this.subtitleDisplays.push(element);
1496
  },{
1497
    subtitleDisplaysOnVideoTimeUpdate: function(time){
1498
      // Assuming all subtitles are in order by time, and do not overlap
1499
      if (this.subtitles) {
1500
        // If current subtitle should stay showing, don't do anything. Otherwise, find new subtitle.
1501
        if (!this.currentSubtitle || this.currentSubtitle.start >= time || this.currentSubtitle.end < time) {
1502
          var newSubIndex = false,
1503
              // Loop in reverse if lastSubtitle is after current time (optimization)
1504
              // Meaning the user is scrubbing in reverse or rewinding
1505
              reverse = (this.subtitles[this.lastSubtitleIndex].start > time),
1506
              // If reverse, step back 1 becase we know it's not the lastSubtitle
1507
              i = this.lastSubtitleIndex - (reverse) ? 1 : 0;
1508
          while (true) { // Loop until broken
1509
            if (reverse) { // Looping in reverse
1510
              // Stop if no more, or this subtitle ends before the current time (no earlier subtitles should apply)
1511
              if (i < 0 || this.subtitles[i].end < time) { break; }
1512
              // End is greater than time, so if start is less, show this subtitle
1513
              if (this.subtitles[i].start < time) {
1514
                newSubIndex = i;
1515
                break;
1516
              }
1517
              i--;
1518
            } else { // Looping forward
1519
              // Stop if no more, or this subtitle starts after time (no later subtitles should apply)
1520
              if (i >= this.subtitles.length || this.subtitles[i].start > time) { break; }
1521
              // Start is less than time, so if end is later, show this subtitle
1522
              if (this.subtitles[i].end > time) {
1523
                newSubIndex = i;
1524
                break;
1525
              }
1526
              i++;
1527
            }
1528
          }
1529

    
1530
          // Set or clear current subtitle
1531
          if (newSubIndex !== false) {
1532
            this.currentSubtitle = this.subtitles[newSubIndex];
1533
            this.lastSubtitleIndex = newSubIndex;
1534
            this.updateSubtitleDisplays(this.currentSubtitle.text);
1535
          } else if (this.currentSubtitle) {
1536
            this.currentSubtitle = false;
1537
            this.updateSubtitleDisplays("");
1538
          }
1539
        }
1540
      }
1541
    },
1542
    updateSubtitleDisplays: function(val){
1543
      this.each(this.subtitleDisplays, function(disp){
1544
        disp.innerHTML = val;
1545
      });
1546
    }
1547
  }
1548
);
1549

    
1550
////////////////////////////////////////////////////////////////////////////////
1551
// Convenience Functions (mini library)
1552
// Functions not specific to video or VideoJS and could probably be replaced with a library like jQuery
1553
////////////////////////////////////////////////////////////////////////////////
1554

    
1555
VideoJS.extend({
1556

    
1557
  addClass: function(element, classToAdd){
1558
    if ((" "+element.className+" ").indexOf(" "+classToAdd+" ") == -1) {
1559
      element.className = element.className === "" ? classToAdd : element.className + " " + classToAdd;
1560
    }
1561
  },
1562
  removeClass: function(element, classToRemove){
1563
    if (element.className.indexOf(classToRemove) == -1) { return; }
1564
    var classNames = element.className.split(/\s+/);
1565
    classNames.splice(classNames.lastIndexOf(classToRemove),1);
1566
    element.className = classNames.join(" ");
1567
  },
1568
  createElement: function(tagName, attributes){
1569
    return this.merge(document.createElement(tagName), attributes);
1570
  },
1571

    
1572
  // Attempt to block the ability to select text while dragging controls
1573
  blockTextSelection: function(){
1574
    document.body.focus();
1575
    document.onselectstart = function () { return false; };
1576
  },
1577
  // Turn off text selection blocking
1578
  unblockTextSelection: function(){ document.onselectstart = function () { return true; }; },
1579

    
1580
  // Return seconds as MM:SS
1581
  formatTime: function(secs) {
1582
    var seconds = Math.round(secs);
1583
    var minutes = Math.floor(seconds / 60);
1584
    minutes = (minutes >= 10) ? minutes : "0" + minutes;
1585
    seconds = Math.floor(seconds % 60);
1586
    seconds = (seconds >= 10) ? seconds : "0" + seconds;
1587
    return minutes + ":" + seconds;
1588
  },
1589

    
1590
  // Return the relative horizonal position of an event as a value from 0-1
1591
  getRelativePosition: function(x, relativeElement){
1592
    return Math.max(0, Math.min(1, (x - this.findPosX(relativeElement)) / relativeElement.offsetWidth));
1593
  },
1594
  // Get an objects position on the page
1595
  findPosX: function(obj) {
1596
    var curleft = obj.offsetLeft;
1597
    while(obj = obj.offsetParent) {
1598
      curleft += obj.offsetLeft;
1599
    }
1600
    return curleft;
1601
  },
1602
  getComputedStyleValue: function(element, style){
1603
    return window.getComputedStyle(element, null).getPropertyValue(style);
1604
  },
1605

    
1606
  round: function(num, dec) {
1607
    if (!dec) { dec = 0; }
1608
    return Math.round(num*Math.pow(10,dec))/Math.pow(10,dec);
1609
  },
1610

    
1611
  addListener: function(element, type, handler){
1612
    if (element.addEventListener) {
1613
      element.addEventListener(type, handler, false);
1614
    } else if (element.attachEvent) {
1615
      element.attachEvent("on"+type, handler);
1616
    }
1617
  },
1618
  removeListener: function(element, type, handler){
1619
    if (element.removeEventListener) {
1620
      element.removeEventListener(type, handler, false);
1621
    } else if (element.attachEvent) {
1622
      element.detachEvent("on"+type, handler);
1623
    }
1624
  },
1625

    
1626
  get: function(url, onSuccess){
1627
    if (typeof XMLHttpRequest == "undefined") {
1628
      XMLHttpRequest = function () {
1629
        try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e) {}
1630
        try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (f) {}
1631
        try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (g) {}
1632
        //Microsoft.XMLHTTP points to Msxml2.XMLHTTP.3.0 and is redundant
1633
        throw new Error("This browser does not support XMLHttpRequest.");
1634
      };
1635
    }
1636
    var request = new XMLHttpRequest();
1637
    request.open("GET",url);
1638
    request.onreadystatechange = function() {
1639
      if (request.readyState == 4 && request.status == 200) {
1640
        onSuccess(request.responseText);
1641
      }
1642
    }.context(this);
1643
    request.send();
1644
  },
1645

    
1646
  trim: function(string){ return string.toString().replace(/^\s+/, "").replace(/\s+$/, ""); },
1647

    
1648
  // DOM Ready functionality adapted from jQuery. http://jquery.com/
1649
  bindDOMReady: function(){
1650
    if (document.readyState === "complete") {
1651
      return VideoJS.onDOMReady();
1652
    }
1653
    if (document.addEventListener) {
1654
      document.addEventListener("DOMContentLoaded", VideoJS.DOMContentLoaded, false);
1655
      window.addEventListener("load", VideoJS.onDOMReady, false);
1656
    } else if (document.attachEvent) {
1657
      document.attachEvent("onreadystatechange", VideoJS.DOMContentLoaded);
1658
      window.attachEvent("onload", VideoJS.onDOMReady);
1659
    }
1660
  },
1661

    
1662
  DOMContentLoaded: function(){
1663
    if (document.addEventListener) {
1664
      document.removeEventListener( "DOMContentLoaded", VideoJS.DOMContentLoaded, false);
1665
      VideoJS.onDOMReady();
1666
    } else if ( document.attachEvent ) {
1667
      if ( document.readyState === "complete" ) {
1668
        document.detachEvent("onreadystatechange", VideoJS.DOMContentLoaded);
1669
        VideoJS.onDOMReady();
1670
      }
1671
    }
1672
  },
1673

    
1674
  // Functions to be run once the DOM is loaded
1675
  DOMReadyList: [],
1676
  addToDOMReady: function(fn){
1677
    if (VideoJS.DOMIsReady) {
1678
      fn.call(document);
1679
    } else {
1680
      VideoJS.DOMReadyList.push(fn);
1681
    }
1682
  },
1683

    
1684
  DOMIsReady: false,
1685
  onDOMReady: function(){
1686
    if (VideoJS.DOMIsReady) { return; }
1687
    if (!document.body) { return setTimeout(VideoJS.onDOMReady, 13); }
1688
    VideoJS.DOMIsReady = true;
1689
    if (VideoJS.DOMReadyList) {
1690
      for (var i=0; i<VideoJS.DOMReadyList.length; i++) {
1691
        VideoJS.DOMReadyList[i].call(document);
1692
      }
1693
      VideoJS.DOMReadyList = null;
1694
    }
1695
  }
1696
});
1697
VideoJS.bindDOMReady();
1698

    
1699
// Allows for binding context to functions
1700
// when using in event listeners and timeouts
1701
Function.prototype.context = function(obj){
1702
  var method = this,
1703
  temp = function(){
1704
    return method.apply(obj, arguments);
1705
  };
1706
  return temp;
1707
};
1708

    
1709
// Like context, in that it creates a closure
1710
// But insteaad keep "this" intact, and passes the var as the second argument of the function
1711
// Need for event listeners where you need to know what called the event
1712
// Only use with event callbacks
1713
Function.prototype.evtContext = function(obj){
1714
  var method = this,
1715
  temp = function(){
1716
    var origContext = this;
1717
    return method.call(obj, arguments[0], origContext);
1718
  };
1719
  return temp;
1720
};
1721

    
1722
// Removeable Event listener with Context
1723
// Replaces the original function with a version that has context
1724
// So it can be removed using the original function name.
1725
// In order to work, a version of the function must already exist in the player/prototype
1726
Function.prototype.rEvtContext = function(obj, funcParent){
1727
  if (this.hasContext === true) { return this; }
1728
  if (!funcParent) { funcParent = obj; }
1729
  for (var attrname in funcParent) {
1730
    if (funcParent[attrname] == this) {
1731
      funcParent[attrname] = this.evtContext(obj);
1732
      funcParent[attrname].hasContext = true;
1733
      return funcParent[attrname];
1734
    }
1735
  }
1736
  return this.evtContext(obj);
1737
};
1738

    
1739
// jQuery Plugin
1740
if (window.jQuery) {
1741
  (function($) {
1742
    $.fn.VideoJS = function(options) {
1743
      this.each(function() {
1744
        VideoJS.setup(this, options);
1745
      });
1746
      return this;
1747
    };
1748
    $.fn.player = function() {
1749
      return this[0].player;
1750
    };
1751
  })(jQuery);
1752
}
1753

    
1754

    
1755
// Expose to global
1756
window.VideoJS = window._V_ = VideoJS;
1757

    
1758
// End self-executing function
1759
})(window);