1/* 2VideoJS - HTML5 Video Player 3v2.0.2 4 5This file is part of VideoJS. Copyright 2010 Zencoder, Inc. 6 7VideoJS is free software: you can redistribute it and/or modify 8it under the terms of the GNU Lesser General Public License as published by 9the Free Software Foundation, either version 3 of the License, or 10(at your option) any later version. 11 12VideoJS is distributed in the hope that it will be useful, 13but WITHOUT ANY WARRANTY; without even the implied warranty of 14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15GNU Lesser General Public License for more details. 16 17You should have received a copy of the GNU Lesser General Public License 18along 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 29var 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 access 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}); 164VideoJS.player = VideoJS.prototype; 165 166//////////////////////////////////////////////////////////////////////////////// 167// Player Types 168//////////////////////////////////////////////////////////////////////////////// 169 170/* Flash Object Fallback (Player Type) 171================================================================================ */ 172VideoJS.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}); 213VideoJS.flashPlayers = {}; 214VideoJS.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================================================================================ */ 242VideoJS.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 266VideoJS.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}; 272VideoJS.extend = function(obj){ this.merge(this, obj, true); }; 273 274VideoJS.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 387if(VideoJS.isIE()) { document.createElement("video"); } 388 389// Expose to global 390window.VideoJS = window._V_ = VideoJS; 391 392/* HTML5 Player Type 393================================================================================ */ 394VideoJS.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 extension 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 if (this.options.controlsBelow) { 712 this.box.style.height = ""; 713 this.element.style.height = (this.box.offsetHeight - this.controls.offsetHeight) + "px"; 714 } 715 } else { 716 this.box.style.width = this.width() + "px"; 717 this.element.style.height=this.height()+"px"; 718 if (this.options.controlsBelow) { 719 this.element.style.height = ""; 720 // this.box.style.height = this.video.offsetHeight + this.controls.offsetHeight + "px"; 721 } 722 } 723 }, 724 /* Subtitles 725 ================================================================================ */ 726 getSubtitles: function(){ 727 var tracks = this.video.getElementsByTagName("TRACK"); 728 for (var i=0,j=tracks.length; i<j; i++) { 729 if (tracks[i].getAttribute("kind") == "subtitles" && tracks[i].getAttribute("src")) { 730 this.subtitlesSource = tracks[i].getAttribute("src"); 731 this.loadSubtitles(); 732 this.buildSubtitles(); 733 } 734 } 735 }, 736 loadSubtitles: function() { _V_.get(this.subtitlesSource, this.parseSubtitles.context(this)); }, 737 parseSubtitles: function(subText) { 738 var lines = subText.split("\n"), 739 line = "", 740 subtitle, time, text; 741 this.subtitles = []; 742 this.currentSubtitle = false; 743 this.lastSubtitleIndex = 0; 744 745 for (var i=0; i<lines.length; i++) { 746 line = _V_.trim(lines[i]); // Trim whitespace and linebreaks 747 if (line) { // Loop until a line with content 748 749 // First line - Number 750 subtitle = { 751 id: line, // Subtitle Number 752 index: this.subtitles.length // Position in Array 753 }; 754 755 // Second line - Time 756 line = _V_.trim(lines[++i]); 757 time = line.split(" --> "); 758 subtitle.start = this.parseSubtitleTime(time[0]); 759 subtitle.end = this.parseSubtitleTime(time[1]); 760 761 // Additional lines - Subtitle Text 762 text = []; 763 for (var j=i; j<lines.length; j++) { // Loop until a blank line or end of lines 764 line = _V_.trim(lines[++i]); 765 if (!line) { break; } 766 text.push(line); 767 } 768 subtitle.text = text.join('<br/>'); 769 770 // Add this subtitle 771 this.subtitles.push(subtitle); 772 } 773 } 774 }, 775 776 parseSubtitleTime: function(timeText) { 777 var parts = timeText.split(':'), 778 time = 0; 779 // hours => seconds 780 time += parseFloat(parts[0])*60*60; 781 // minutes => seconds 782 time += parseFloat(parts[1])*60; 783 // get seconds 784 var seconds = parts[2].split(/\.|,/); // Either . or , 785 time += parseFloat(seconds[0]); 786 // add miliseconds 787 ms = parseFloat(seconds[1]); 788 if (ms) { time += ms/1000; } 789 return time; 790 }, 791 792 buildSubtitles: function(){ 793 /* Creating this HTML 794 <div class="vjs-subtitles"></div> 795 */ 796 this.subtitlesDisplay = _V_.createElement("div", { className: 'vjs-subtitles' }); 797 this.box.appendChild(this.subtitlesDisplay); 798 this.activateElement(this.subtitlesDisplay, "subtitlesDisplay"); 799 }, 800 801 /* Player API - Translate functionality from player to video 802 ================================================================================ */ 803 addVideoListener: function(type, fn){ _V_.addListener(this.video, type, fn.rEvtContext(this)); }, 804 805 play: function(){ 806 this.video.play(); 807 return this; 808 }, 809 onPlay: function(fn){ this.addVideoListener("play", fn); return this; }, 810 811 pause: function(){ 812 this.video.pause(); 813 return this; 814 }, 815 onPause: function(fn){ this.addVideoListener("pause", fn); return this; }, 816 paused: function() { return this.video.paused; }, 817 818 currentTime: function(seconds){ 819 if (seconds !== undefined) { 820 try { this.video.currentTime = seconds; } 821 catch(e) { this.warning(VideoJS.warnings.videoNotReady); } 822 this.values.currentTime = seconds; 823 return this; 824 } 825 return this.video.currentTime; 826 }, 827 onCurrentTimeUpdate: function(fn){ 828 this.currentTimeListeners.push(fn); 829 }, 830 831 duration: function(){ 832 return this.video.duration; 833 }, 834 835 buffered: function(){ 836 // Storing values allows them be overridden by setBufferedFromProgress 837 if (this.values.bufferStart === undefined) { 838 this.values.bufferStart = 0; 839 this.values.bufferEnd = 0; 840 } 841 if (this.video.buffered && this.video.buffered.length > 0) { 842 var newEnd = this.video.buffered.end(0); 843 if (newEnd > this.values.bufferEnd) { this.values.bufferEnd = newEnd; } 844 } 845 return [this.values.bufferStart, this.values.bufferEnd]; 846 }, 847 848 volume: function(percentAsDecimal){ 849 if (percentAsDecimal !== undefined) { 850 // Force value to between 0 and 1 851 this.values.volume = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))); 852 this.video.volume = this.values.volume; 853 this.setLocalStorage("volume", this.values.volume); 854 return this; 855 } 856 if (this.values.volume) { return this.values.volume; } 857 return this.video.volume; 858 }, 859 onVolumeChange: function(fn){ _V_.addListener(this.video, 'volumechange', fn.rEvtContext(this)); }, 860 861 width: function(width){ 862 if (width !== undefined) { 863 this.video.width = width; // Not using style so it can be overridden on fullscreen. 864 this.box.style.width = width+"px"; 865 this.triggerResizeListeners(); 866 return this; 867 } 868 return this.video.offsetWidth; 869 }, 870 height: function(height){ 871 if (height !== undefined) { 872 this.video.height = height; 873 this.box.style.height = height+"px"; 874 this.triggerResizeListeners(); 875 return this; 876 } 877 return this.video.offsetHeight; 878 }, 879 880 supportsFullScreen: function(){ 881 if(typeof this.video.webkitEnterFullScreen == 'function') { 882 // Seems to be broken in Chromium/Chrome 883 if (!navigator.userAgent.match("Chrome") && !navigator.userAgent.match("Mac OS X 10.5")) { 884 return true; 885 } 886 } 887 return false; 888 }, 889 890 html5EnterNativeFullScreen: function(){ 891 try { 892 this.video.webkitEnterFullScreen(); 893 } catch (e) { 894 if (e.code == 11) { this.warning(VideoJS.warnings.videoNotReady); } 895 } 896 return this; 897 }, 898 899 // Turn on fullscreen (window) mode 900 // Real fullscreen isn't available in browsers quite yet. 901 enterFullScreen: function(){ 902 if (this.supportsFullScreen()) { 903 this.html5EnterNativeFullScreen(); 904 } else { 905 this.enterFullWindow(); 906 } 907 }, 908 909 exitFullScreen: function(){ 910 if (this.supportsFullScreen()) { 911 // Shouldn't be called 912 } else { 913 this.exitFullWindow(); 914 } 915 }, 916 917 enterFullWindow: function(){ 918 this.videoIsFullScreen = true; 919 // Storing original doc overflow value to return to when fullscreen is off 920 this.docOrigOverflow = document.documentElement.style.overflow; 921 // Add listener for esc key to exit fullscreen 922 _V_.addListener(document, "keydown", this.fullscreenOnEscKey.rEvtContext(this)); 923 // Add listener for a window resize 924 _V_.addListener(window, "resize", this.fullscreenOnWindowResize.rEvtContext(this)); 925 // Hide any scroll bars 926 document.documentElement.style.overflow = 'hidden'; 927 // Apply fullscreen styles 928 _V_.addClass(this.box, "vjs-fullscreen"); 929 // Resize the box, controller, and poster 930 this.positionAll(); 931 }, 932 933 // Turn off fullscreen (window) mode 934 exitFullWindow: function(){ 935 this.videoIsFullScreen = false; 936 document.removeEventListener("keydown", this.fullscreenOnEscKey, false); 937 window.removeEventListener("resize", this.fullscreenOnWindowResize, false); 938 // Unhide scroll bars. 939 document.documentElement.style.overflow = this.docOrigOverflow; 940 // Remove fullscreen styles 941 _V_.removeClass(this.box, "vjs-fullscreen"); 942 // Resize the box, controller, and poster to original sizes 943 this.positionAll(); 944 }, 945 946 onError: function(fn){ this.addVideoListener("error", fn); return this; }, 947 onEnded: function(fn){ 948 this.addVideoListener("ended", fn); return this; 949 } 950}); 951 952//////////////////////////////////////////////////////////////////////////////// 953// Element Behaviors 954// Tell elements how to act or react 955//////////////////////////////////////////////////////////////////////////////// 956 957/* Player Behaviors - How VideoJS reacts to what the video is doing. 958================================================================================ */ 959VideoJS.player.newBehavior("player", function(player){ 960 this.onError(this.playerOnVideoError); 961 // Listen for when the video is played 962 this.onPlay(this.playerOnVideoPlay); 963 this.onPlay(this.trackCurrentTime); 964 // Listen for when the video is paused 965 this.onPause(this.playerOnVideoPause); 966 this.onPause(this.stopTrackingCurrentTime); 967 // Listen for when the video ends 968 this.onEnded(this.playerOnVideoEnded); 969 // Set interval for load progress using buffer watching method 970 // this.trackCurrentTime(); 971 this.trackBuffered(); 972 // Buffer Full 973 this.onBufferedUpdate(this.isBufferFull); 974 },{ 975 playerOnVideoError: function(event){ 976 this.log(event); 977 this.log(this.video.error); 978 }, 979 playerOnVideoPlay: function(event){ this.hasPlayed = true; }, 980 playerOnVideoPause: function(event){}, 981 playerOnVideoEnded: function(event){ 982 this.currentTime(0); 983 this.pause(); 984 }, 985 986 /* Load Tracking -------------------------------------------------------------- */ 987 // Buffer watching method for load progress. 988 // Used for browsers that don't support the progress event 989 trackBuffered: function(){ 990 this.bufferedInterval = setInterval(this.triggerBufferedListeners.context(this), 500); 991 }, 992 stopTrackingBuffered: function(){ clearInterval(this.bufferedInterval); }, 993 bufferedListeners: [], 994 onBufferedUpdate: function(fn){ 995 this.bufferedListeners.push(fn); 996 }, 997 triggerBufferedListeners: function(){ 998 this.isBufferFull(); 999 this.each(this.bufferedListeners, function(listener){ 1000 (listener.context(this))(); 1001 }); 1002 }, 1003 isBufferFull: function(){ 1004 if (this.bufferedPercent() == 1) { this.stopTrackingBuffered(); } 1005 }, 1006 1007 /* Time Tracking -------------------------------------------------------------- */ 1008 trackCurrentTime: function(){ 1009 if (this.currentTimeInterval) { clearInterval(this.currentTimeInterval); } 1010 this.currentTimeInterval = setInterval(this.triggerCurrentTimeListeners.context(this), 100); // 42 = 24 fps 1011 this.trackingCurrentTime = true; 1012 }, 1013 // Turn off play progress tracking (when paused or dragging) 1014 stopTrackingCurrentTime: function(){ 1015 clearInterval(this.currentTimeInterval); 1016 this.trackingCurrentTime = false; 1017 }, 1018 currentTimeListeners: [], 1019 // onCurrentTimeUpdate is in API section now 1020 triggerCurrentTimeListeners: function(late, newTime){ // FF passes milliseconds late as the first argument 1021 this.each(this.currentTimeListeners, function(listener){ 1022 (listener.context(this))(newTime || this.currentTime()); 1023 }); 1024 }, 1025 1026 /* Resize Tracking -------------------------------------------------------------- */ 1027 resizeListeners: [], 1028 onResize: function(fn){ 1029 this.resizeListeners.push(fn); 1030 }, 1031 // Trigger anywhere the video/box size is changed. 1032 triggerResizeListeners: function(){ 1033 this.each(this.resizeListeners, function(listener){ 1034 (listener.context(this))(); 1035 }); 1036 } 1037 } 1038); 1039/* Mouse Over Video Reporter Behaviors - i.e. Controls hiding based on mouse location 1040================================================================================ */ 1041VideoJS.player.newBehavior("mouseOverVideoReporter", function(element){ 1042 // Listen for the mouse move the video. Used to reveal the controller. 1043 _V_.addListener(element, "mousemove", this.mouseOverVideoReporterOnMouseMove.context(this)); 1044 // Listen for the mouse moving out of the video. Used to hide the controller. 1045 _V_.addListener(element, "mouseout", this.mouseOverVideoReporterOnMouseOut.context(this)); 1046 },{ 1047 mouseOverVideoReporterOnMouseMove: function(){ 1048 this.showControlBars(); 1049 clearInterval(this.mouseMoveTimeout); 1050 this.mouseMoveTimeout = setTimeout(this.hideControlBars.context(this), 4000); 1051 }, 1052 mouseOverVideoReporterOnMouseOut: function(event){ 1053 // Prevent flicker by making sure mouse hasn't left the video 1054 var parent = event.relatedTarget; 1055 while (parent && parent !== this.box) { 1056 parent = parent.parentNode; 1057 } 1058 if (parent !== this.box) { 1059 this.hideControlBars(); 1060 } 1061 } 1062 } 1063); 1064/* Mouse Over Video Reporter Behaviors - i.e. Controls hiding based on mouse location 1065================================================================================ */ 1066VideoJS.player.newBehavior("box", function(element){ 1067 this.positionBox(); 1068 _V_.addClass(element, "vjs-paused"); 1069 this.activateElement(element, "mouseOverVideoReporter"); 1070 this.onPlay(this.boxOnVideoPlay); 1071 this.onPause(this.boxOnVideoPause); 1072 },{ 1073 boxOnVideoPlay: function(){ 1074 _V_.removeClass(this.box, "vjs-paused"); 1075 _V_.addClass(this.box, "vjs-playing"); 1076 }, 1077 boxOnVideoPause: function(){ 1078 _V_.removeClass(this.box, "vjs-playing"); 1079 _V_.addClass(this.box, "vjs-paused"); 1080 } 1081 } 1082); 1083/* Poster Image Overlay 1084================================================================================ */ 1085VideoJS.player.newBehavior("poster", function(element){ 1086 this.activateElement(element, "mouseOverVideoReporter"); 1087 this.activateElement(element, "playButton"); 1088 this.onPlay(this.hidePoster); 1089 this.onEnded(this.showPoster); 1090 this.onResize(this.positionPoster); 1091 },{ 1092 showPoster: function(){ 1093 if (!this.poster) { return; } 1094 this.poster.style.display = "block"; 1095 this.positionPoster(); 1096 }, 1097 positionPoster: function(){ 1098 // Only if the poster is visible 1099 if (!this.poster || this.poster.style.display == 'none') { return; } 1100 this.poster.style.height = this.height() + "px"; // Need incase controlsBelow 1101 this.poster.style.width = this.width() + "px"; // Could probably do 100% of box 1102 }, 1103 hidePoster: function(){ 1104 if (!this.poster) { return; } 1105 this.poster.style.display = "none"; 1106 }, 1107 // Update poster source from attribute or fallback image 1108 // iPad breaks if you include a poster attribute, so this fixes that 1109 updatePosterSource: function(){ 1110 if (!this.video.poster) { 1111 var images = this.video.getElementsByTagName("img"); 1112 if (images.length > 0) { this.video.poster = images[0].src; } 1113 } 1114 } 1115 } 1116); 1117/* Control Bar Behaviors 1118================================================================================ */ 1119VideoJS.player.newBehavior("controlBar", function(element){ 1120 if (!this.controlBars) { 1121 this.controlBars = []; 1122 this.onResize(this.positionControlBars); 1123 } 1124 this.controlBars.push(element); 1125 _V_.addListener(element, "mousemove", this.onControlBarsMouseMove.context(this)); 1126 _V_.addListener(element, "mouseout", this.onControlBarsMouseOut.context(this)); 1127 },{ 1128 showControlBars: function(){ 1129 if (!this.options.controlsAtStart && !this.hasPlayed) { return; } 1130 this.each(this.controlBars, function(bar){ 1131 bar.style.display = "block"; 1132 }); 1133 }, 1134 // Place controller relative to the video's position (now just resizing bars) 1135 positionControlBars: function(){ 1136 this.updatePlayProgressBars(); 1137 this.updateLoadProgressBars(); 1138 }, 1139 hideControlBars: function(){ 1140 if (this.options.controlsHiding && !this.mouseIsOverControls) { 1141 this.each(this.controlBars, function(bar){ 1142 bar.style.display = "none"; 1143 }); 1144 } 1145 }, 1146 // Block controls from hiding when mouse is over them. 1147 onControlBarsMouseMove: function(){ this.mouseIsOverControls = true; }, 1148 onControlBarsMouseOut: function(event){ 1149 this.mouseIsOverControls = false; 1150 } 1151 } 1152); 1153/* PlayToggle, PlayButton, PauseButton Behaviors 1154================================================================================ */ 1155// Play Toggle 1156VideoJS.player.newBehavior("playToggle", function(element){ 1157 if (!this.elements.playToggles) { 1158 this.elements.playToggles = []; 1159 this.onPlay(this.playTogglesOnPlay); 1160 this.onPause(this.playTogglesOnPause); 1161 } 1162 this.elements.playToggles.push(element); 1163 _V_.addListener(element, "click", this.onPlayToggleClick.context(this)); 1164 },{ 1165 onPlayToggleClick: function(event){ 1166 if (this.paused()) { 1167 this.play(); 1168 } else { 1169 this.pause(); 1170 } 1171 }, 1172 playTogglesOnPlay: function(event){ 1173 this.each(this.elements.playToggles, function(toggle){ 1174 _V_.removeClass(toggle, "vjs-paused"); 1175 _V_.addClass(toggle, "vjs-playing"); 1176 }); 1177 }, 1178 playTogglesOnPause: function(event){ 1179 this.each(this.elements.playToggles, function(toggle){ 1180 _V_.removeClass(toggle, "vjs-playing"); 1181 _V_.addClass(toggle, "vjs-paused"); 1182 }); 1183 } 1184 } 1185); 1186// Play 1187VideoJS.player.newBehavior("playButton", function(element){ 1188 _V_.addListener(element, "click", this.onPlayButtonClick.context(this)); 1189 },{ 1190 onPlayButtonClick: function(event){ this.play(); } 1191 } 1192); 1193// Pause 1194VideoJS.player.newBehavior("pauseButton", function(element){ 1195 _V_.addListener(element, "click", this.onPauseButtonClick.context(this)); 1196 },{ 1197 onPauseButtonClick: function(event){ this.pause(); } 1198 } 1199); 1200/* Play Progress Bar Behaviors 1201================================================================================ */ 1202VideoJS.player.newBehavior("playProgressBar", function(element){ 1203 if (!this.playProgressBars) { 1204 this.playProgressBars = []; 1205 this.onCurrentTimeUpdate(this.updatePlayProgressBars); 1206 } 1207 this.playProgressBars.push(element); 1208 },{ 1209 // Ajust the play progress bar's width based on the current play time 1210 updatePlayProgressBars: function(newTime){ 1211 var progress = (newTime !== undefined) ? newTime / this.duration() : this.currentTime() / this.duration(); 1212 if (isNaN(progress)) { progress = 0; } 1213 this.each(this.playProgressBars, function(bar){ 1214 if (bar.style) { bar.style.width = _V_.round(progress * 100, 2) + "%"; } 1215 }); 1216 } 1217 } 1218); 1219/* Load Progress Bar Behaviors 1220================================================================================ */ 1221VideoJS.player.newBehavior("loadProgressBar", function(element){ 1222 if (!this.loadProgressBars) { this.loadProgressBars = []; } 1223 this.loadProgressBars.push(element); 1224 this.onBufferedUpdate(this.updateLoadProgressBars); 1225 },{ 1226 updateLoadProgressBars: function(){ 1227 this.each(this.loadProgressBars, function(bar){ 1228 if (bar.style) { bar.style.width = _V_.round(this.bufferedPercent() * 100, 2) + "%"; } 1229 }); 1230 } 1231 } 1232); 1233 1234/* Current Time Display Behaviors 1235================================================================================ */ 1236VideoJS.player.newBehavior("currentTimeDisplay", function(element){ 1237 if (!this.currentTimeDisplays) { 1238 this.currentTimeDisplays = []; 1239 this.onCurrentTimeUpdate(this.updateCurrentTimeDisplays); 1240 } 1241 this.currentTimeDisplays.push(element); 1242 },{ 1243 // Update the displayed time (00:00) 1244 updateCurrentTimeDisplays: function(newTime){ 1245 if (!this.currentTimeDisplays) { return; } 1246 // Allows for smooth scrubbing, when player can't keep up. 1247 var time = (newTime) ? newTime : this.currentTime(); 1248 this.each(this.currentTimeDisplays, function(dis){ 1249 dis.innerHTML = _V_.formatTime(time); 1250 }); 1251 } 1252 } 1253); 1254 1255/* Duration Display Behaviors 1256================================================================================ */ 1257VideoJS.player.newBehavior("durationDisplay", function(element){ 1258 if (!this.durationDisplays) { 1259 this.durationDisplays = []; 1260 this.onCurrentTimeUpdate(this.updateDurationDisplays); 1261 } 1262 this.durationDisplays.push(element); 1263 },{ 1264 updateDurationDisplays: function(){ 1265 if (!this.durationDisplays) { return; } 1266 this.each(this.durationDisplays, function(dis){ 1267 if (this.duration()) { dis.innerHTML = _V_.formatTime(this.duration()); } 1268 }); 1269 } 1270 } 1271); 1272 1273/* Current Time Scrubber Behaviors 1274================================================================================ */ 1275VideoJS.player.newBehavior("currentTimeScrubber", function(element){ 1276 _V_.addListener(element, "mousedown", this.onCurrentTimeScrubberMouseDown.rEvtContext(this)); 1277 },{ 1278 // Adjust the play position when the user drags on the progress bar 1279 onCurrentTimeScrubberMouseDown: function(event, scrubber){ 1280 event.preventDefault(); 1281 this.currentScrubber = scrubber; 1282 1283 this.stopTrackingCurrentTime(); // Allows for smooth scrubbing 1284 1285 this.videoWasPlaying = !this.paused(); 1286 this.pause(); 1287 1288 _V_.blockTextSelection(); 1289 this.setCurrentTimeWithScrubber(event); 1290 _V_.addListener(document, "mousemove", this.onCurrentTimeScrubberMouseMove.rEvtContext(this)); 1291 _V_.addListener(document, "mouseup", this.onCurrentTimeScrubberMouseUp.rEvtContext(this)); 1292 }, 1293 onCurrentTimeScrubberMouseMove: function(event){ // Removable 1294 this.setCurrentTimeWithScrubber(event); 1295 }, 1296 onCurrentTimeScrubberMouseUp: function(event){ // Removable 1297 _V_.unblockTextSelection(); 1298 document.removeEventListener("mousemove", this.onCurrentTimeScrubberMouseMove, false); 1299 document.removeEventListener("mouseup", this.onCurrentTimeScrubberMouseUp, false); 1300 if (this.videoWasPlaying) { 1301 this.play(); 1302 this.trackCurrentTime(); 1303 } 1304 }, 1305 setCurrentTimeWithScrubber: function(event){ 1306 var newProgress = _V_.getRelativePosition(event.pageX, this.currentScrubber); 1307 var newTime = newProgress * this.duration(); 1308 this.triggerCurrentTimeListeners(0, newTime); // Allows for smooth scrubbing 1309 // Don't let video end while scrubbing. 1310 if (newTime == this.duration()) { newTime = newTime - 0.1; } 1311 this.currentTime(newTime); 1312 } 1313 } 1314); 1315/* Volume Display Behaviors 1316================================================================================ */ 1317VideoJS.player.newBehavior("volumeDisplay", function(element){ 1318 if (!this.volumeDisplays) { 1319 this.volumeDisplays = []; 1320 this.onVolumeChange(this.updateVolumeDisplays); 1321 } 1322 this.volumeDisplays.push(element); 1323 this.updateVolumeDisplay(element); // Set the display to the initial volume 1324 },{ 1325 // Update the volume control display 1326 // Unique to these default controls. Uses borders to create the look of bars. 1327 updateVolumeDisplays: function(){ 1328 if (!this.volumeDisplays) { return; } 1329 this.each(this.volumeDisplays, function(dis){ 1330 this.updateVolumeDisplay(dis); 1331 }); 1332 }, 1333 updateVolumeDisplay: function(display){ 1334 var volNum = Math.ceil(this.volume() * 6); 1335 this.each(display.children, function(child, num){ 1336 if (num < volNum) { 1337 _V_.addClass(child, "vjs-volume-level-on"); 1338 } else { 1339 _V_.removeClass(child, "vjs-volume-level-on"); 1340 } 1341 }); 1342 } 1343 } 1344); 1345/* Volume Scrubber Behaviors 1346================================================================================ */ 1347VideoJS.player.newBehavior("volumeScrubber", function(element){ 1348 _V_.addListener(element, "mousedown", this.onVolumeScrubberMouseDown.rEvtContext(this)); 1349 },{ 1350 // Adjust the volume when the user drags on the volume control 1351 onVolumeScrubberMouseDown: function(event, scrubber){ 1352 // event.preventDefault(); 1353 _V_.blockTextSelection(); 1354 this.currentScrubber = scrubber; 1355 this.setVolumeWithScrubber(event); 1356 _V_.addListener(document, "mousemove", this.onVolumeScrubberMouseMove.rEvtContext(this)); 1357 _V_.addListener(document, "mouseup", this.onVolumeScrubberMouseUp.rEvtContext(this)); 1358 }, 1359 onVolumeScrubberMouseMove: function(event){ 1360 this.setVolumeWithScrubber(event); 1361 }, 1362 onVolumeScrubberMouseUp: function(event){ 1363 this.setVolumeWithScrubber(event); 1364 _V_.unblockTextSelection(); 1365 document.removeEventListener("mousemove", this.onVolumeScrubberMouseMove, false); 1366 document.removeEventListener("mouseup", this.onVolumeScrubberMouseUp, false); 1367 }, 1368 setVolumeWithScrubber: function(event){ 1369 var newVol = _V_.getRelativePosition(event.pageX, this.currentScrubber); 1370 this.volume(newVol); 1371 } 1372 } 1373); 1374/* Fullscreen Toggle Behaviors 1375================================================================================ */ 1376VideoJS.player.newBehavior("fullscreenToggle", function(element){ 1377 _V_.addListener(element, "click", this.onFullscreenToggleClick.context(this)); 1378 },{ 1379 // When the user clicks on the fullscreen button, update fullscreen setting 1380 onFullscreenToggleClick: function(event){ 1381 if (!this.videoIsFullScreen) { 1382 this.enterFullScreen(); 1383 } else { 1384 this.exitFullScreen(); 1385 } 1386 }, 1387 1388 fullscreenOnWindowResize: function(event){ // Removable 1389 this.positionControlBars(); 1390 }, 1391 // Create listener for esc key while in full screen mode 1392 fullscreenOnEscKey: function(event){ // Removable 1393 if (event.keyCode == 27) { 1394 this.exitFullScreen(); 1395 } 1396 } 1397 } 1398); 1399/* Big Play Button Behaviors 1400================================================================================ */ 1401VideoJS.player.newBehavior("bigPlayButton", function(element){ 1402 if (!this.elements.bigPlayButtons) { 1403 this.elements.bigPlayButtons = []; 1404 this.onPlay(this.bigPlayButtonsOnPlay); 1405 this.onEnded(this.bigPlayButtonsOnEnded); 1406 } 1407 this.elements.bigPlayButtons.push(element); 1408 this.activateElement(element, "playButton"); 1409 },{ 1410 bigPlayButtonsOnPlay: function(event){ this.hideBigPlayButtons(); }, 1411 bigPlayButtonsOnEnded: function(event){ this.showBigPlayButtons(); }, 1412 showBigPlayButtons: function(){ 1413 this.each(this.elements.bigPlayButtons, function(element){ 1414 element.style.display = "block"; 1415 }); 1416 }, 1417 hideBigPlayButtons: function(){ 1418 this.each(this.elements.bigPlayButtons, function(element){ 1419 element.style.display = "none"; 1420 }); 1421 } 1422 } 1423); 1424/* Spinner 1425================================================================================ */ 1426VideoJS.player.newBehavior("spinner", function(element){ 1427 if (!this.spinners) { 1428 this.spinners = []; 1429 _V_.addListener(this.video, "loadeddata", this.spinnersOnVideoLoadedData.context(this)); 1430 _V_.addListener(this.video, "loadstart", this.spinnersOnVideoLoadStart.context(this)); 1431 _V_.addListener(this.video, "seeking", this.spinnersOnVideoSeeking.context(this)); 1432 _V_.addListener(this.video, "seeked", this.spinnersOnVideoSeeked.context(this)); 1433 _V_.addListener(this.video, "canplay", this.spinnersOnVideoCanPlay.context(this)); 1434 _V_.addListener(this.video, "canplaythrough", this.spinnersOnVideoCanPlayThrough.context(this)); 1435 _V_.addListener(this.video, "waiting", this.spinnersOnVideoWaiting.context(this)); 1436 _V_.addListener(this.video, "stalled", this.spinnersOnVideoStalled.context(this)); 1437 _V_.addListener(this.video, "suspend", this.spinnersOnVideoSuspend.context(this)); 1438 _V_.addListener(this.video, "playing", this.spinnersOnVideoPlaying.context(this)); 1439 _V_.addListener(this.video, "timeupdate", this.spinnersOnVideoTimeUpdate.context(this)); 1440 } 1441 this.spinners.push(element); 1442 },{ 1443 showSpinners: function(){ 1444 this.each(this.spinners, function(spinner){ 1445 spinner.style.display = "block"; 1446 }); 1447 clearInterval(this.spinnerInterval); 1448 this.spinnerInterval = setInterval(this.rotateSpinners.context(this), 100); 1449 }, 1450 hideSpinners: function(){ 1451 this.each(this.spinners, function(spinner){ 1452 spinner.style.display = "none"; 1453 }); 1454 clearInterval(this.spinnerInterval); 1455 }, 1456 spinnersRotated: 0, 1457 rotateSpinners: function(){ 1458 this.each(this.spinners, function(spinner){ 1459 // spinner.style.transform = 'scale(0.5) rotate('+this.spinnersRotated+'deg)'; 1460 spinner.style.WebkitTransform = 'scale(0.5) rotate('+this.spinnersRotated+'deg)'; 1461 spinner.style.MozTransform = 'scale(0.5) rotate('+this.spinnersRotated+'deg)'; 1462 }); 1463 if (this.spinnersRotated == 360) { this.spinnersRotated = 0; } 1464 this.spinnersRotated += 45; 1465 }, 1466 spinnersOnVideoLoadedData: function(event){ this.hideSpinners(); }, 1467 spinnersOnVideoLoadStart: function(event){ this.showSpinners(); }, 1468 spinnersOnVideoSeeking: function(event){ /* this.showSpinners(); */ }, 1469 spinnersOnVideoSeeked: function(event){ /* this.hideSpinners(); */ }, 1470 spinnersOnVideoCanPlay: function(event){ /* this.hideSpinners(); */ }, 1471 spinnersOnVideoCanPlayThrough: function(event){ this.hideSpinners(); }, 1472 spinnersOnVideoWaiting: function(event){ 1473 // Safari sometimes triggers waiting inappropriately 1474 // Like after video has played, any you play again. 1475 this.showSpinners(); 1476 }, 1477 spinnersOnVideoStalled: function(event){}, 1478 spinnersOnVideoSuspend: function(event){}, 1479 spinnersOnVideoPlaying: function(event){ this.hideSpinners(); }, 1480 spinnersOnVideoTimeUpdate: function(event){ 1481 // Safari sometimes calls waiting and doesn't recover 1482 if(this.spinner.style.display == "block") { this.hideSpinners(); } 1483 } 1484 } 1485); 1486/* Subtitles 1487================================================================================ */ 1488VideoJS.player.newBehavior("subtitlesDisplay", function(element){ 1489 if (!this.subtitleDisplays) { 1490 this.subtitleDisplays = []; 1491 this.onCurrentTimeUpdate(this.subtitleDisplaysOnVideoTimeUpdate); 1492 this.onEnded(function() { this.lastSubtitleIndex = 0; }.context(this)); 1493 } 1494 this.subtitleDisplays.push(element); 1495 },{ 1496 subtitleDisplaysOnVideoTimeUpdate: function(time){ 1497 // Assuming all subtitles are in order by time, and do not overlap 1498 if (this.subtitles) { 1499 // If current subtitle should stay showing, don't do anything. Otherwise, find new subtitle. 1500 if (!this.currentSubtitle || this.currentSubtitle.start >= time || this.currentSubtitle.end < time) { 1501 var newSubIndex = false, 1502 // Loop in reverse if lastSubtitle is after current time (optimization) 1503 // Meaning the user is scrubbing in reverse or rewinding 1504 reverse = (this.subtitles[this.lastSubtitleIndex].start > time), 1505 // If reverse, step back 1 becase we know it's not the lastSubtitle 1506 i = this.lastSubtitleIndex - (reverse) ? 1 : 0; 1507 while (true) { // Loop until broken 1508 if (reverse) { // Looping in reverse 1509 // Stop if no more, or this subtitle ends before the current time (no earlier subtitles should apply) 1510 if (i < 0 || this.subtitles[i].end < time) { break; } 1511 // End is greater than time, so if start is less, show this subtitle 1512 if (this.subtitles[i].start < time) { 1513 newSubIndex = i; 1514 break; 1515 } 1516 i--; 1517 } else { // Looping forward 1518 // Stop if no more, or this subtitle starts after time (no later subtitles should apply) 1519 if (i >= this.subtitles.length || this.subtitles[i].start > time) { break; } 1520 // Start is less than time, so if end is later, show this subtitle 1521 if (this.subtitles[i].end > time) { 1522 newSubIndex = i; 1523 break; 1524 } 1525 i++; 1526 } 1527 } 1528 1529 // Set or clear current subtitle 1530 if (newSubIndex !== false) { 1531 this.currentSubtitle = this.subtitles[newSubIndex]; 1532 this.lastSubtitleIndex = newSubIndex; 1533 this.updateSubtitleDisplays(this.currentSubtitle.text); 1534 } else if (this.currentSubtitle) { 1535 this.currentSubtitle = false; 1536 this.updateSubtitleDisplays(""); 1537 } 1538 } 1539 } 1540 }, 1541 updateSubtitleDisplays: function(val){ 1542 this.each(this.subtitleDisplays, function(disp){ 1543 disp.innerHTML = val; 1544 }); 1545 } 1546 } 1547); 1548 1549//////////////////////////////////////////////////////////////////////////////// 1550// Convenience Functions (mini library) 1551// Functions not specific to video or VideoJS and could probably be replaced with a library like jQuery 1552//////////////////////////////////////////////////////////////////////////////// 1553 1554VideoJS.extend({ 1555 1556 addClass: function(element, classToAdd){ 1557 if ((" "+element.className+" ").indexOf(" "+classToAdd+" ") == -1) { 1558 element.className = element.className === "" ? classToAdd : element.className + " " + classToAdd; 1559 } 1560 }, 1561 removeClass: function(element, classToRemove){ 1562 if (element.className.indexOf(classToRemove) == -1) { return; } 1563 var classNames = element.className.split(/\s+/); 1564 classNames.splice(classNames.lastIndexOf(classToRemove),1); 1565 element.className = classNames.join(" "); 1566 }, 1567 createElement: function(tagName, attributes){ 1568 return this.merge(document.createElement(tagName), attributes); 1569 }, 1570 1571 // Attempt to block the ability to select text while dragging controls 1572 blockTextSelection: function(){ 1573 document.body.focus(); 1574 document.onselectstart = function () { return false; }; 1575 }, 1576 // Turn off text selection blocking 1577 unblockTextSelection: function(){ document.onselectstart = function () { return true; }; }, 1578 1579 // Return seconds as MM:SS 1580 formatTime: function(secs) { 1581 var seconds = Math.round(secs); 1582 var minutes = Math.floor(seconds / 60); 1583 minutes = (minutes >= 10) ? minutes : "0" + minutes; 1584 seconds = Math.floor(seconds % 60); 1585 seconds = (seconds >= 10) ? seconds : "0" + seconds; 1586 return minutes + ":" + seconds; 1587 }, 1588 1589 // Return the relative horizonal position of an event as a value from 0-1 1590 getRelativePosition: function(x, relativeElement){ 1591 return Math.max(0, Math.min(1, (x - this.findPosX(relativeElement)) / relativeElement.offsetWidth)); 1592 }, 1593 // Get an objects position on the page 1594 findPosX: function(obj) { 1595 var curleft = obj.offsetLeft; 1596 while(obj = obj.offsetParent) { 1597 curleft += obj.offsetLeft; 1598 } 1599 return curleft; 1600 }, 1601 getComputedStyleValue: function(element, style){ 1602 return window.getComputedStyle(element, null).getPropertyValue(style); 1603 }, 1604 1605 round: function(num, dec) { 1606 if (!dec) { dec = 0; } 1607 return Math.round(num*Math.pow(10,dec))/Math.pow(10,dec); 1608 }, 1609 1610 addListener: function(element, type, handler){ 1611 if (element.addEventListener) { 1612 element.addEventListener(type, handler, false); 1613 } else if (element.attachEvent) { 1614 element.attachEvent("on"+type, handler); 1615 } 1616 }, 1617 removeListener: function(element, type, handler){ 1618 if (element.removeEventListener) { 1619 element.removeEventListener(type, handler, false); 1620 } else if (element.attachEvent) { 1621 element.detachEvent("on"+type, handler); 1622 } 1623 }, 1624 1625 get: function(url, onSuccess){ 1626 if (typeof XMLHttpRequest == "undefined") { 1627 XMLHttpRequest = function () { 1628 try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e) {} 1629 try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (f) {} 1630 try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (g) {} 1631 //Microsoft.XMLHTTP points to Msxml2.XMLHTTP.3.0 and is redundant 1632 throw new Error("This browser does not support XMLHttpRequest."); 1633 }; 1634 } 1635 var request = new XMLHttpRequest(); 1636 request.open("GET",url); 1637 request.onreadystatechange = function() { 1638 if (request.readyState == 4 && request.status == 200) { 1639 onSuccess(request.responseText); 1640 } 1641 }.context(this); 1642 request.send(); 1643 }, 1644 1645 trim: function(string){ return string.toString().replace(/^\s+/, "").replace(/\s+$/, ""); }, 1646 1647 // DOM Ready functionality adapted from jQuery. http://jquery.com/ 1648 bindDOMReady: function(){ 1649 if (document.readyState === "complete") { 1650 return VideoJS.onDOMReady(); 1651 } 1652 if (document.addEventListener) { 1653 document.addEventListener("DOMContentLoaded", VideoJS.DOMContentLoaded, false); 1654 window.addEventListener("load", VideoJS.onDOMReady, false); 1655 } else if (document.attachEvent) { 1656 document.attachEvent("onreadystatechange", VideoJS.DOMContentLoaded); 1657 window.attachEvent("onload", VideoJS.onDOMReady); 1658 } 1659 }, 1660 1661 DOMContentLoaded: function(){ 1662 if (document.addEventListener) { 1663 document.removeEventListener( "DOMContentLoaded", VideoJS.DOMContentLoaded, false); 1664 VideoJS.onDOMReady(); 1665 } else if ( document.attachEvent ) { 1666 if ( document.readyState === "complete" ) { 1667 document.detachEvent("onreadystatechange", VideoJS.DOMContentLoaded); 1668 VideoJS.onDOMReady(); 1669 } 1670 } 1671 }, 1672 1673 // Functions to be run once the DOM is loaded 1674 DOMReadyList: [], 1675 addToDOMReady: function(fn){ 1676 if (VideoJS.DOMIsReady) { 1677 fn.call(document); 1678 } else { 1679 VideoJS.DOMReadyList.push(fn); 1680 } 1681 }, 1682 1683 DOMIsReady: false, 1684 onDOMReady: function(){ 1685 if (VideoJS.DOMIsReady) { return; } 1686 if (!document.body) { return setTimeout(VideoJS.onDOMReady, 13); } 1687 VideoJS.DOMIsReady = true; 1688 if (VideoJS.DOMReadyList) { 1689 for (var i=0; i<VideoJS.DOMReadyList.length; i++) { 1690 VideoJS.DOMReadyList[i].call(document); 1691 } 1692 VideoJS.DOMReadyList = null; 1693 } 1694 } 1695}); 1696VideoJS.bindDOMReady(); 1697 1698// Allows for binding context to functions 1699// when using in event listeners and timeouts 1700Function.prototype.context = function(obj){ 1701 var method = this, 1702 temp = function(){ 1703 return method.apply(obj, arguments); 1704 }; 1705 return temp; 1706}; 1707 1708// Like context, in that it creates a closure 1709// But insteaad keep "this" intact, and passes the var as the second argument of the function 1710// Need for event listeners where you need to know what called the event 1711// Only use with event callbacks 1712Function.prototype.evtContext = function(obj){ 1713 var method = this, 1714 temp = function(){ 1715 var origContext = this; 1716 return method.call(obj, arguments[0], origContext); 1717 }; 1718 return temp; 1719}; 1720 1721// Removable Event listener with Context 1722// Replaces the original function with a version that has context 1723// So it can be removed using the original function name. 1724// In order to work, a version of the function must already exist in the player/prototype 1725Function.prototype.rEvtContext = function(obj, funcParent){ 1726 if (this.hasContext === true) { return this; } 1727 if (!funcParent) { funcParent = obj; } 1728 for (var attrname in funcParent) { 1729 if (funcParent[attrname] == this) { 1730 funcParent[attrname] = this.evtContext(obj); 1731 funcParent[attrname].hasContext = true; 1732 return funcParent[attrname]; 1733 } 1734 } 1735 return this.evtContext(obj); 1736}; 1737 1738// jQuery Plugin 1739if (window.jQuery) { 1740 (function($) { 1741 $.fn.VideoJS = function(options) { 1742 this.each(function() { 1743 VideoJS.setup(this, options); 1744 }); 1745 return this; 1746 }; 1747 $.fn.player = function() { 1748 return this[0].player; 1749 }; 1750 })(jQuery); 1751} 1752 1753 1754// Expose to global 1755window.VideoJS = window._V_ = VideoJS; 1756 1757// End self-executing function 1758})(window);