| /* |
| VideoJS - HTML5 Video Player |
| v2.0.2 |
| |
| This file is part of VideoJS. Copyright 2010 Zencoder, Inc. |
| |
| VideoJS is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Lesser General Public License as published by |
| the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| VideoJS is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Lesser General Public License for more details. |
| |
| You should have received a copy of the GNU Lesser General Public License |
| along with VideoJS. If not, see <http://www.gnu.org/licenses/>. |
| */ |
| |
| // Self-executing function to prevent global vars and help with minification |
| (function(window, undefined){ |
| var document = window.document; |
| |
| // Using jresig's Class implementation http://ejohn.org/blog/simple-javascript-inheritance/ |
| (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;};})(); |
| |
| // Video JS Player Class |
| var VideoJS = JRClass.extend({ |
| |
| // Initialize the player for the supplied video tag element |
| // element: video tag |
| init: function(element, setOptions){ |
| |
| // Allow an ID string or an element |
| if (typeof element == 'string') { |
| this.video = document.getElementById(element); |
| } else { |
| this.video = element; |
| } |
| // Store reference to player on the video element. |
| // So you can access the player later: document.getElementById("video_id").player.play(); |
| this.video.player = this; |
| this.values = {}; // Cache video values. |
| this.elements = {}; // Store refs to controls elements. |
| |
| // Default Options |
| this.options = { |
| autoplay: false, |
| preload: true, |
| useBuiltInControls: false, // Use the browser's controls (iPhone) |
| controlsBelow: false, // Display control bar below video vs. in front of |
| controlsAtStart: false, // Make controls visible when page loads |
| controlsHiding: true, // Hide controls when not over the video |
| defaultVolume: 0.85, // Will be overridden by localStorage volume if available |
| playerFallbackOrder: ["html5", "flash", "links"], // Players and order to use them |
| flashPlayer: "htmlObject", |
| flashPlayerVersion: false // Required flash version for fallback |
| }; |
| // Override default options with global options |
| if (typeof VideoJS.options == "object") { _V_.merge(this.options, VideoJS.options); } |
| // Override default & global options with options specific to this player |
| if (typeof setOptions == "object") { _V_.merge(this.options, setOptions); } |
| // Override preload & autoplay with video attributes |
| if (this.getPreloadAttribute() !== undefined) { this.options.preload = this.getPreloadAttribute(); } |
| if (this.getAutoplayAttribute() !== undefined) { this.options.autoplay = this.getAutoplayAttribute(); } |
| |
| // Store reference to embed code pieces |
| this.box = this.video.parentNode; |
| this.linksFallback = this.getLinksFallback(); |
| this.hideLinksFallback(); // Will be shown again if "links" player is used |
| |
| // Loop through the player names list in options, "html5" etc. |
| // For each player name, initialize the player with that name under VideoJS.players |
| // If the player successfully initializes, we're done |
| // If not, try the next player in the list |
| this.each(this.options.playerFallbackOrder, function(playerType){ |
| if (this[playerType+"Supported"]()) { // Check if player type is supported |
| this[playerType+"Init"](); // Initialize player type |
| return true; // Stop looping though players |
| } |
| }); |
| |
| // Start Global Listeners - API doesn't exist before now |
| this.activateElement(this, "player"); |
| this.activateElement(this.box, "box"); |
| }, |
| /* Behaviors |
| ================================================================================ */ |
| behaviors: {}, |
| newBehavior: function(name, activate, functions){ |
| this.behaviors[name] = activate; |
| this.extend(functions); |
| }, |
| activateElement: function(element, behavior){ |
| // Allow passing and ID string |
| if (typeof element == "string") { element = document.getElementById(element); } |
| this.behaviors[behavior].call(this, element); |
| }, |
| /* Errors/Warnings |
| ================================================================================ */ |
| errors: [], // Array to track errors |
| warnings: [], |
| warning: function(warning){ |
| this.warnings.push(warning); |
| this.log(warning); |
| }, |
| /* History of errors/events (not quite there yet) |
| ================================================================================ */ |
| history: [], |
| log: function(event){ |
| if (!event) { return; } |
| if (typeof event == "string") { event = { type: event }; } |
| if (event.type) { this.history.push(event.type); } |
| if (this.history.length >= 50) { this.history.shift(); } |
| try { console.log(event.type); } catch(e) { try { opera.postError(event.type); } catch(e){} } |
| }, |
| /* Local Storage |
| ================================================================================ */ |
| setLocalStorage: function(key, value){ |
| if (!localStorage) { return; } |
| try { |
| localStorage[key] = value; |
| } catch(e) { |
| if (e.code == 22 || e.code == 1014) { // Webkit == 22 / Firefox == 1014 |
| this.warning(VideoJS.warnings.localStorageFull); |
| } |
| } |
| }, |
| /* Helpers |
| ================================================================================ */ |
| getPreloadAttribute: function(){ |
| if (typeof this.video.hasAttribute == "function" && this.video.hasAttribute("preload")) { |
| var preload = this.video.getAttribute("preload"); |
| // Only included the attribute, thinking it was boolean |
| if (preload === "" || preload === "true") { return "auto"; } |
| if (preload === "false") { return "none"; } |
| return preload; |
| } |
| }, |
| getAutoplayAttribute: function(){ |
| if (typeof this.video.hasAttribute == "function" && this.video.hasAttribute("autoplay")) { |
| var autoplay = this.video.getAttribute("autoplay"); |
| if (autoplay === "false") { return false; } |
| return true; |
| } |
| }, |
| // Calculates amoutn of buffer is full |
| bufferedPercent: function(){ return (this.duration()) ? this.buffered()[1] / this.duration() : 0; }, |
| // Each that maintains player as context |
| // Break if true is returned |
| each: function(arr, fn){ |
| if (!arr || arr.length === 0) { return; } |
| for (var i=0,j=arr.length; i<j; i++) { |
| if (fn.call(this, arr[i], i)) { break; } |
| } |
| }, |
| extend: function(obj){ |
| for (var attrname in obj) { |
| if (obj.hasOwnProperty(attrname)) { this[attrname]=obj[attrname]; } |
| } |
| } |
| }); |
| VideoJS.player = VideoJS.prototype; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Player Types |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| /* Flash Object Fallback (Player Type) |
| ================================================================================ */ |
| VideoJS.player.extend({ |
| flashSupported: function(){ |
| if (!this.flashElement) { this.flashElement = this.getFlashElement(); } |
| // Check if object exists & Flash Player version is supported |
| if (this.flashElement && this.flashPlayerVersionSupported()) { |
| return true; |
| } else { |
| return false; |
| } |
| }, |
| flashInit: function(){ |
| this.replaceWithFlash(); |
| this.element = this.flashElement; |
| this.video.src = ""; // Stop video from downloading if HTML5 is still supported |
| var flashPlayerType = VideoJS.flashPlayers[this.options.flashPlayer]; |
| this.extend(VideoJS.flashPlayers[this.options.flashPlayer].api); |
| (flashPlayerType.init.context(this))(); |
| }, |
| // Get Flash Fallback object element from Embed Code |
| getFlashElement: function(){ |
| var children = this.video.children; |
| for (var i=0,j=children.length; i<j; i++) { |
| if (children[i].className == "vjs-flash-fallback") { |
| return children[i]; |
| } |
| } |
| }, |
| // Used to force a browser to fall back when it's an HTML5 browser but there's no supported sources |
| replaceWithFlash: function(){ |
| // this.flashElement = this.video.removeChild(this.flashElement); |
| if (this.flashElement) { |
| this.box.insertBefore(this.flashElement, this.video); |
| this.video.style.display = "none"; // Removing it was breaking later players |
| } |
| }, |
| // Check if browser can use this flash player |
| flashPlayerVersionSupported: function(){ |
| var playerVersion = (this.options.flashPlayerVersion) ? this.options.flashPlayerVersion : VideoJS.flashPlayers[this.options.flashPlayer].flashPlayerVersion; |
| return VideoJS.getFlashVersion() >= playerVersion; |
| } |
| }); |
| VideoJS.flashPlayers = {}; |
| VideoJS.flashPlayers.htmlObject = { |
| flashPlayerVersion: 9, |
| init: function() { return true; }, |
| api: { // No video API available with HTML Object embed method |
| width: function(width){ |
| if (width !== undefined) { |
| this.element.width = width; |
| this.box.style.width = width+"px"; |
| this.triggerResizeListeners(); |
| return this; |
| } |
| return this.element.width; |
| }, |
| height: function(height){ |
| if (height !== undefined) { |
| this.element.height = height; |
| this.box.style.height = height+"px"; |
| this.triggerResizeListeners(); |
| return this; |
| } |
| return this.element.height; |
| } |
| } |
| }; |
| |
| |
| /* Download Links Fallback (Player Type) |
| ================================================================================ */ |
| VideoJS.player.extend({ |
| linksSupported: function(){ return true; }, |
| linksInit: function(){ |
| this.showLinksFallback(); |
| this.element = this.video; |
| }, |
| // Get the download links block element |
| getLinksFallback: function(){ return this.box.getElementsByTagName("P")[0]; }, |
| // Hide no-video download paragraph |
| hideLinksFallback: function(){ |
| if (this.linksFallback) { this.linksFallback.style.display = "none"; } |
| }, |
| // Hide no-video download paragraph |
| showLinksFallback: function(){ |
| if (this.linksFallback) { this.linksFallback.style.display = "block"; } |
| } |
| }); |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Class Methods |
| // Functions that don't apply to individual videos. |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| // Combine Objects - Use "safe" to protect from overwriting existing items |
| VideoJS.merge = function(obj1, obj2, safe){ |
| for (var attrname in obj2){ |
| if (obj2.hasOwnProperty(attrname) && (!safe || !obj1.hasOwnProperty(attrname))) { obj1[attrname]=obj2[attrname]; } |
| } |
| return obj1; |
| }; |
| VideoJS.extend = function(obj){ this.merge(this, obj, true); }; |
| |
| VideoJS.extend({ |
| // Add VideoJS to all video tags with the video-js class when the DOM is ready |
| setupAllWhenReady: function(options){ |
| // Options is stored globally, and added ot any new player on init |
| VideoJS.options = options; |
| VideoJS.DOMReady(VideoJS.setup); |
| }, |
| |
| // Run the supplied function when the DOM is ready |
| DOMReady: function(fn){ |
| VideoJS.addToDOMReady(fn); |
| }, |
| |
| // Set up a specific video or array of video elements |
| // "video" can be: |
| // false, undefined, or "All": set up all videos with the video-js class |
| // A video tag ID or video tag element: set up one video and return one player |
| // An array of video tag elements/IDs: set up each and return an array of players |
| setup: function(videos, options){ |
| var returnSingular = false, |
| playerList = [], |
| videoElement; |
| |
| // If videos is undefined or "All", set up all videos with the video-js class |
| if (!videos || videos == "All") { |
| videos = VideoJS.getVideoJSTags(); |
| // If videos is not an array, add to an array |
| } else if (typeof videos != 'object' || videos.nodeType == 1) { |
| videos = [videos]; |
| returnSingular = true; |
| } |
| |
| // Loop through videos and create players for them |
| for (var i=0; i<videos.length; i++) { |
| if (typeof videos[i] == 'string') { |
| videoElement = document.getElementById(videos[i]); |
| } else { // assume DOM object |
| videoElement = videos[i]; |
| } |
| playerList.push(new VideoJS(videoElement, options)); |
| } |
| |
| // Return one or all depending on what was passed in |
| return (returnSingular) ? playerList[0] : playerList; |
| }, |
| |
| // Find video tags with the video-js class |
| getVideoJSTags: function() { |
| var videoTags = document.getElementsByTagName("video"), |
| videoJSTags = [], videoTag; |
| |
| for (var i=0,j=videoTags.length; i<j; i++) { |
| videoTag = videoTags[i]; |
| if (videoTag.className.indexOf("video-js") != -1) { |
| videoJSTags.push(videoTag); |
| } |
| } |
| return videoJSTags; |
| }, |
| |
| // Check if the browser supports video. |
| browserSupportsVideo: function() { |
| if (typeof VideoJS.videoSupport != "undefined") { return VideoJS.videoSupport; } |
| VideoJS.videoSupport = !!document.createElement('video').canPlayType; |
| return VideoJS.videoSupport; |
| }, |
| |
| getFlashVersion: function(){ |
| // Cache Version |
| if (typeof VideoJS.flashVersion != "undefined") { return VideoJS.flashVersion; } |
| var version = 0, desc; |
| if (typeof navigator.plugins != "undefined" && typeof navigator.plugins["Shockwave Flash"] == "object") { |
| desc = navigator.plugins["Shockwave Flash"].description; |
| if (desc && !(typeof navigator.mimeTypes != "undefined" && navigator.mimeTypes["application/x-shockwave-flash"] && !navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin)) { |
| version = parseInt(desc.match(/^.*\s+([^\s]+)\.[^\s]+\s+[^\s]+$/)[1], 10); |
| } |
| } else if (typeof window.ActiveXObject != "undefined") { |
| try { |
| var testObject = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"); |
| if (testObject) { |
| version = parseInt(testObject.GetVariable("$version").match(/^[^\s]+\s(\d+)/)[1], 10); |
| } |
| } |
| catch(e) {} |
| } |
| VideoJS.flashVersion = version; |
| return VideoJS.flashVersion; |
| }, |
| |
| // Browser & Device Checks |
| isIE: function(){ return !+"\v1"; }, |
| isIPad: function(){ return navigator.userAgent.match(/iPad/i) !== null; }, |
| isIPhone: function(){ return navigator.userAgent.match(/iPhone/i) !== null; }, |
| isIOS: function(){ return VideoJS.isIPhone() || VideoJS.isIPad(); }, |
| iOSVersion: function() { |
| var match = navigator.userAgent.match(/OS (\d+)_/i); |
| if (match && match[1]) { return match[1]; } |
| }, |
| isAndroid: function(){ return navigator.userAgent.match(/Android/i) !== null; }, |
| androidVersion: function() { |
| var match = navigator.userAgent.match(/Android (\d+)\./i); |
| if (match && match[1]) { return match[1]; } |
| }, |
| |
| warnings: { |
| // Safari errors if you call functions on a video that hasn't loaded yet |
| videoNotReady: "Video is not ready yet (try playing the video first).", |
| // Getting a QUOTA_EXCEEDED_ERR when setting local storage occasionally |
| localStorageFull: "Local Storage is Full" |
| } |
| }); |
| |
| // Shim to make Video tag valid in IE |
| if(VideoJS.isIE()) { document.createElement("video"); } |
| |
| // Expose to global |
| window.VideoJS = window._V_ = VideoJS; |
| |
| /* HTML5 Player Type |
| ================================================================================ */ |
| VideoJS.player.extend({ |
| html5Supported: function(){ |
| if (VideoJS.browserSupportsVideo() && this.canPlaySource()) { |
| return true; |
| } else { |
| return false; |
| } |
| }, |
| html5Init: function(){ |
| this.element = this.video; |
| |
| this.fixPreloading(); // Support old browsers that used autobuffer |
| this.supportProgressEvents(); // Support browsers that don't use 'buffered' |
| |
| // Set to stored volume OR 85% |
| this.volume((localStorage && localStorage.volume) || this.options.defaultVolume); |
| |
| // Update interface for device needs |
| if (VideoJS.isIOS()) { |
| this.options.useBuiltInControls = true; |
| this.iOSInterface(); |
| } else if (VideoJS.isAndroid()) { |
| this.options.useBuiltInControls = true; |
| this.androidInterface(); |
| } |
| |
| // Add VideoJS Controls |
| if (!this.options.useBuiltInControls) { |
| this.video.controls = false; |
| |
| if (this.options.controlsBelow) { _V_.addClass(this.box, "vjs-controls-below"); } |
| |
| // Make a click on th video act as a play button |
| this.activateElement(this.video, "playToggle"); |
| |
| // Build Interface |
| this.buildStylesCheckDiv(); // Used to check if style are loaded |
| this.buildAndActivatePoster(); |
| this.buildBigPlayButton(); |
| this.buildAndActivateSpinner(); |
| this.buildAndActivateControlBar(); |
| this.loadInterface(); // Show everything once styles are loaded |
| this.getSubtitles(); |
| } |
| }, |
| /* Source Managemet |
| ================================================================================ */ |
| canPlaySource: function(){ |
| // Cache Result |
| if (this.canPlaySourceResult) { return this.canPlaySourceResult; } |
| // Loop through sources and check if any can play |
| var children = this.video.children; |
| for (var i=0,j=children.length; i<j; i++) { |
| if (children[i].tagName.toUpperCase() == "SOURCE") { |
| var canPlay = this.video.canPlayType(children[i].type) || this.canPlayExt(children[i].src); |
| if (canPlay == "probably" || canPlay == "maybe") { |
| this.firstPlayableSource = children[i]; |
| this.canPlaySourceResult = true; |
| return true; |
| } |
| } |
| } |
| this.canPlaySourceResult = false; |
| return false; |
| }, |
| // Check if the extension is compatible, for when type won't work |
| canPlayExt: function(src){ |
| if (!src) { return ""; } |
| var match = src.match(/\.([^\.]+)$/); |
| if (match && match[1]) { |
| var ext = match[1].toLowerCase(); |
| // Android canPlayType doesn't work |
| if (VideoJS.isAndroid()) { |
| if (ext == "mp4" || ext == "m4v") { return "maybe"; } |
| // Allow Apple HTTP Streaming for iOS |
| } else if (VideoJS.isIOS()) { |
| if (ext == "m3u8") { return "maybe"; } |
| } |
| } |
| return ""; |
| }, |
| // Force the video source - Helps fix loading bugs in a handful of devices, like the iPad/iPhone poster bug |
| // And iPad/iPhone javascript include location bug. And Android type attribute bug |
| forceTheSource: function(){ |
| this.video.src = this.firstPlayableSource.src; // From canPlaySource() |
| this.video.load(); |
| }, |
| /* Device Fixes |
| ================================================================================ */ |
| // Support older browsers that used "autobuffer" |
| fixPreloading: function(){ |
| if (typeof this.video.hasAttribute == "function" && this.video.hasAttribute("preload") && this.video.preload != "none") { |
| this.video.autobuffer = true; // Was a boolean |
| } else { |
| this.video.autobuffer = false; |
| this.video.preload = "none"; |
| } |
| }, |
| |
| // Listen for Video Load Progress (currently does not if html file is local) |
| // Buffered does't work in all browsers, so watching progress as well |
| supportProgressEvents: function(e){ |
| _V_.addListener(this.video, 'progress', this.playerOnVideoProgress.context(this)); |
| }, |
| playerOnVideoProgress: function(event){ |
| this.setBufferedFromProgress(event); |
| }, |
| setBufferedFromProgress: function(event){ // HTML5 Only |
| if(event.total > 0) { |
| var newBufferEnd = (event.loaded / event.total) * this.duration(); |
| if (newBufferEnd > this.values.bufferEnd) { this.values.bufferEnd = newBufferEnd; } |
| } |
| }, |
| |
| iOSInterface: function(){ |
| if(VideoJS.iOSVersion() < 4) { this.forceTheSource(); } // Fix loading issues |
| if(VideoJS.isIPad()) { // iPad could work with controlsBelow |
| this.buildAndActivateSpinner(); // Spinner still works well on iPad, since iPad doesn't have one |
| } |
| }, |
| |
| // Fix android specific quirks |
| // Use built-in controls, but add the big play button, since android doesn't have one. |
| androidInterface: function(){ |
| this.forceTheSource(); // Fix loading issues |
| _V_.addListener(this.video, "click", function(){ this.play(); }); // Required to play |
| this.buildBigPlayButton(); // But don't activate the normal way. Pause doesn't work right on android. |
| _V_.addListener(this.bigPlayButton, "click", function(){ this.play(); }.context(this)); |
| this.positionBox(); |
| this.showBigPlayButtons(); |
| }, |
| /* Wait for styles (TODO: move to _V_) |
| ================================================================================ */ |
| loadInterface: function(){ |
| if(!this.stylesHaveLoaded()) { |
| // Don't want to create an endless loop either. |
| if (!this.positionRetries) { this.positionRetries = 1; } |
| if (this.positionRetries++ < 100) { |
| setTimeout(this.loadInterface.context(this),10); |
| return; |
| } |
| } |
| this.hideStylesCheckDiv(); |
| this.showPoster(); |
| if (this.video.paused !== false) { this.showBigPlayButtons(); } |
| if (this.options.controlsAtStart) { this.showControlBars(); } |
| this.positionAll(); |
| }, |
| /* Control Bar |
| ================================================================================ */ |
| buildAndActivateControlBar: function(){ |
| /* Creating this HTML |
| <div class="vjs-controls"> |
| <div class="vjs-play-control"> |
| <span></span> |
| </div> |
| <div class="vjs-progress-control"> |
| <div class="vjs-progress-holder"> |
| <div class="vjs-load-progress"></div> |
| <div class="vjs-play-progress"></div> |
| </div> |
| </div> |
| <div class="vjs-time-control"> |
| <span class="vjs-current-time-display">00:00</span><span> / </span><span class="vjs-duration-display">00:00</span> |
| </div> |
| <div class="vjs-volume-control"> |
| <div> |
| <span></span><span></span><span></span><span></span><span></span><span></span> |
| </div> |
| </div> |
| <div class="vjs-fullscreen-control"> |
| <div> |
| <span></span><span></span><span></span><span></span> |
| </div> |
| </div> |
| </div> |
| */ |
| |
| // Create a div to hold the different controls |
| this.controls = _V_.createElement("div", { className: "vjs-controls" }); |
| // Add the controls to the video's container |
| this.box.appendChild(this.controls); |
| this.activateElement(this.controls, "controlBar"); |
| this.activateElement(this.controls, "mouseOverVideoReporter"); |
| |
| // Build the play control |
| this.playControl = _V_.createElement("div", { className: "vjs-play-control", innerHTML: "<span></span>" }); |
| this.controls.appendChild(this.playControl); |
| this.activateElement(this.playControl, "playToggle"); |
| |
| // Build the progress control |
| this.progressControl = _V_.createElement("div", { className: "vjs-progress-control" }); |
| this.controls.appendChild(this.progressControl); |
| |
| // Create a holder for the progress bars |
| this.progressHolder = _V_.createElement("div", { className: "vjs-progress-holder" }); |
| this.progressControl.appendChild(this.progressHolder); |
| this.activateElement(this.progressHolder, "currentTimeScrubber"); |
| |
| // Create the loading progress display |
| this.loadProgressBar = _V_.createElement("div", { className: "vjs-load-progress" }); |
| this.progressHolder.appendChild(this.loadProgressBar); |
| this.activateElement(this.loadProgressBar, "loadProgressBar"); |
| |
| // Create the playing progress display |
| this.playProgressBar = _V_.createElement("div", { className: "vjs-play-progress" }); |
| this.progressHolder.appendChild(this.playProgressBar); |
| this.activateElement(this.playProgressBar, "playProgressBar"); |
| |
| // Create the progress time display (00:00 / 00:00) |
| this.timeControl = _V_.createElement("div", { className: "vjs-time-control" }); |
| this.controls.appendChild(this.timeControl); |
| |
| // Create the current play time display |
| this.currentTimeDisplay = _V_.createElement("span", { className: "vjs-current-time-display", innerHTML: "00:00" }); |
| this.timeControl.appendChild(this.currentTimeDisplay); |
| this.activateElement(this.currentTimeDisplay, "currentTimeDisplay"); |
| |
| // Add time separator |
| this.timeSeparator = _V_.createElement("span", { innerHTML: " / " }); |
| this.timeControl.appendChild(this.timeSeparator); |
| |
| // Create the total duration display |
| this.durationDisplay = _V_.createElement("span", { className: "vjs-duration-display", innerHTML: "00:00" }); |
| this.timeControl.appendChild(this.durationDisplay); |
| this.activateElement(this.durationDisplay, "durationDisplay"); |
| |
| // Create the volumne control |
| this.volumeControl = _V_.createElement("div", { |
| className: "vjs-volume-control", |
| innerHTML: "<div><span></span><span></span><span></span><span></span><span></span><span></span></div>" |
| }); |
| this.controls.appendChild(this.volumeControl); |
| this.activateElement(this.volumeControl, "volumeScrubber"); |
| |
| this.volumeDisplay = this.volumeControl.children[0]; |
| this.activateElement(this.volumeDisplay, "volumeDisplay"); |
| |
| // Crete the fullscreen control |
| this.fullscreenControl = _V_.createElement("div", { |
| className: "vjs-fullscreen-control", |
| innerHTML: "<div><span></span><span></span><span></span><span></span></div>" |
| }); |
| this.controls.appendChild(this.fullscreenControl); |
| this.activateElement(this.fullscreenControl, "fullscreenToggle"); |
| }, |
| /* Poster Image |
| ================================================================================ */ |
| buildAndActivatePoster: function(){ |
| this.updatePosterSource(); |
| if (this.video.poster) { |
| this.poster = document.createElement("img"); |
| // Add poster to video box |
| this.box.appendChild(this.poster); |
| |
| // Add poster image data |
| this.poster.src = this.video.poster; |
| // Add poster styles |
| this.poster.className = "vjs-poster"; |
| this.activateElement(this.poster, "poster"); |
| } else { |
| this.poster = false; |
| } |
| }, |
| /* Big Play Button |
| ================================================================================ */ |
| buildBigPlayButton: function(){ |
| /* Creating this HTML |
| <div class="vjs-big-play-button"><span></span></div> |
| */ |
| this.bigPlayButton = _V_.createElement("div", { |
| className: "vjs-big-play-button", |
| innerHTML: "<span></span>" |
| }); |
| this.box.appendChild(this.bigPlayButton); |
| this.activateElement(this.bigPlayButton, "bigPlayButton"); |
| }, |
| /* Spinner (Loading) |
| ================================================================================ */ |
| buildAndActivateSpinner: function(){ |
| this.spinner = _V_.createElement("div", { |
| className: "vjs-spinner", |
| innerHTML: "<div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div>" |
| }); |
| this.box.appendChild(this.spinner); |
| this.activateElement(this.spinner, "spinner"); |
| }, |
| /* Styles Check - Check if styles are loaded (move ot _V_) |
| ================================================================================ */ |
| // Sometimes the CSS styles haven't been applied to the controls yet |
| // when we're trying to calculate the height and position them correctly. |
| // This causes a flicker where the controls are out of place. |
| buildStylesCheckDiv: function(){ |
| this.stylesCheckDiv = _V_.createElement("div", { className: "vjs-styles-check" }); |
| this.stylesCheckDiv.style.position = "absolute"; |
| this.box.appendChild(this.stylesCheckDiv); |
| }, |
| hideStylesCheckDiv: function(){ this.stylesCheckDiv.style.display = "none"; }, |
| stylesHaveLoaded: function(){ |
| if (this.stylesCheckDiv.offsetHeight != 5) { |
| return false; |
| } else { |
| return true; |
| } |
| }, |
| /* VideoJS Box - Holds all elements |
| ================================================================================ */ |
| positionAll: function(){ |
| this.positionBox(); |
| this.positionControlBars(); |
| this.positionPoster(); |
| }, |
| positionBox: function(){ |
| // Set width based on fullscreen or not. |
| if (this.videoIsFullScreen) { |
| this.box.style.width = ""; |
| this.element.style.height=""; |
| if (this.options.controlsBelow) { |
| this.box.style.height = ""; |
| this.element.style.height = (this.box.offsetHeight - this.controls.offsetHeight) + "px"; |
| } |
| } else { |
| this.box.style.width = this.width() + "px"; |
| this.element.style.height=this.height()+"px"; |
| if (this.options.controlsBelow) { |
| this.element.style.height = ""; |
| // this.box.style.height = this.video.offsetHeight + this.controls.offsetHeight + "px"; |
| } |
| } |
| }, |
| /* Subtitles |
| ================================================================================ */ |
| getSubtitles: function(){ |
| var tracks = this.video.getElementsByTagName("TRACK"); |
| for (var i=0,j=tracks.length; i<j; i++) { |
| if (tracks[i].getAttribute("kind") == "subtitles" && tracks[i].getAttribute("src")) { |
| this.subtitlesSource = tracks[i].getAttribute("src"); |
| this.loadSubtitles(); |
| this.buildSubtitles(); |
| } |
| } |
| }, |
| loadSubtitles: function() { _V_.get(this.subtitlesSource, this.parseSubtitles.context(this)); }, |
| parseSubtitles: function(subText) { |
| var lines = subText.split("\n"), |
| line = "", |
| subtitle, time, text; |
| this.subtitles = []; |
| this.currentSubtitle = false; |
| this.lastSubtitleIndex = 0; |
| |
| for (var i=0; i<lines.length; i++) { |
| line = _V_.trim(lines[i]); // Trim whitespace and linebreaks |
| if (line) { // Loop until a line with content |
| |
| // First line - Number |
| subtitle = { |
| id: line, // Subtitle Number |
| index: this.subtitles.length // Position in Array |
| }; |
| |
| // Second line - Time |
| line = _V_.trim(lines[++i]); |
| time = line.split(" --> "); |
| subtitle.start = this.parseSubtitleTime(time[0]); |
| subtitle.end = this.parseSubtitleTime(time[1]); |
| |
| // Additional lines - Subtitle Text |
| text = []; |
| for (var j=i; j<lines.length; j++) { // Loop until a blank line or end of lines |
| line = _V_.trim(lines[++i]); |
| if (!line) { break; } |
| text.push(line); |
| } |
| subtitle.text = text.join('<br/>'); |
| |
| // Add this subtitle |
| this.subtitles.push(subtitle); |
| } |
| } |
| }, |
| |
| parseSubtitleTime: function(timeText) { |
| var parts = timeText.split(':'), |
| time = 0; |
| // hours => seconds |
| time += parseFloat(parts[0])*60*60; |
| // minutes => seconds |
| time += parseFloat(parts[1])*60; |
| // get seconds |
| var seconds = parts[2].split(/\.|,/); // Either . or , |
| time += parseFloat(seconds[0]); |
| // add miliseconds |
| ms = parseFloat(seconds[1]); |
| if (ms) { time += ms/1000; } |
| return time; |
| }, |
| |
| buildSubtitles: function(){ |
| /* Creating this HTML |
| <div class="vjs-subtitles"></div> |
| */ |
| this.subtitlesDisplay = _V_.createElement("div", { className: 'vjs-subtitles' }); |
| this.box.appendChild(this.subtitlesDisplay); |
| this.activateElement(this.subtitlesDisplay, "subtitlesDisplay"); |
| }, |
| |
| /* Player API - Translate functionality from player to video |
| ================================================================================ */ |
| addVideoListener: function(type, fn){ _V_.addListener(this.video, type, fn.rEvtContext(this)); }, |
| |
| play: function(){ |
| this.video.play(); |
| return this; |
| }, |
| onPlay: function(fn){ this.addVideoListener("play", fn); return this; }, |
| |
| pause: function(){ |
| this.video.pause(); |
| return this; |
| }, |
| onPause: function(fn){ this.addVideoListener("pause", fn); return this; }, |
| paused: function() { return this.video.paused; }, |
| |
| currentTime: function(seconds){ |
| if (seconds !== undefined) { |
| try { this.video.currentTime = seconds; } |
| catch(e) { this.warning(VideoJS.warnings.videoNotReady); } |
| this.values.currentTime = seconds; |
| return this; |
| } |
| return this.video.currentTime; |
| }, |
| onCurrentTimeUpdate: function(fn){ |
| this.currentTimeListeners.push(fn); |
| }, |
| |
| duration: function(){ |
| return this.video.duration; |
| }, |
| |
| buffered: function(){ |
| // Storing values allows them be overridden by setBufferedFromProgress |
| if (this.values.bufferStart === undefined) { |
| this.values.bufferStart = 0; |
| this.values.bufferEnd = 0; |
| } |
| if (this.video.buffered && this.video.buffered.length > 0) { |
| var newEnd = this.video.buffered.end(0); |
| if (newEnd > this.values.bufferEnd) { this.values.bufferEnd = newEnd; } |
| } |
| return [this.values.bufferStart, this.values.bufferEnd]; |
| }, |
| |
| volume: function(percentAsDecimal){ |
| if (percentAsDecimal !== undefined) { |
| // Force value to between 0 and 1 |
| this.values.volume = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))); |
| this.video.volume = this.values.volume; |
| this.setLocalStorage("volume", this.values.volume); |
| return this; |
| } |
| if (this.values.volume) { return this.values.volume; } |
| return this.video.volume; |
| }, |
| onVolumeChange: function(fn){ _V_.addListener(this.video, 'volumechange', fn.rEvtContext(this)); }, |
| |
| width: function(width){ |
| if (width !== undefined) { |
| this.video.width = width; // Not using style so it can be overridden on fullscreen. |
| this.box.style.width = width+"px"; |
| this.triggerResizeListeners(); |
| return this; |
| } |
| return this.video.offsetWidth; |
| }, |
| height: function(height){ |
| if (height !== undefined) { |
| this.video.height = height; |
| this.box.style.height = height+"px"; |
| this.triggerResizeListeners(); |
| return this; |
| } |
| return this.video.offsetHeight; |
| }, |
| |
| supportsFullScreen: function(){ |
| if(typeof this.video.webkitEnterFullScreen == 'function') { |
| // Seems to be broken in Chromium/Chrome |
| if (!navigator.userAgent.match("Chrome") && !navigator.userAgent.match("Mac OS X 10.5")) { |
| return true; |
| } |
| } |
| return false; |
| }, |
| |
| html5EnterNativeFullScreen: function(){ |
| try { |
| this.video.webkitEnterFullScreen(); |
| } catch (e) { |
| if (e.code == 11) { this.warning(VideoJS.warnings.videoNotReady); } |
| } |
| return this; |
| }, |
| |
| // Turn on fullscreen (window) mode |
| // Real fullscreen isn't available in browsers quite yet. |
| enterFullScreen: function(){ |
| if (this.supportsFullScreen()) { |
| this.html5EnterNativeFullScreen(); |
| } else { |
| this.enterFullWindow(); |
| } |
| }, |
| |
| exitFullScreen: function(){ |
| if (this.supportsFullScreen()) { |
| // Shouldn't be called |
| } else { |
| this.exitFullWindow(); |
| } |
| }, |
| |
| enterFullWindow: function(){ |
| this.videoIsFullScreen = true; |
| // Storing original doc overflow value to return to when fullscreen is off |
| this.docOrigOverflow = document.documentElement.style.overflow; |
| // Add listener for esc key to exit fullscreen |
| _V_.addListener(document, "keydown", this.fullscreenOnEscKey.rEvtContext(this)); |
| // Add listener for a window resize |
| _V_.addListener(window, "resize", this.fullscreenOnWindowResize.rEvtContext(this)); |
| // Hide any scroll bars |
| document.documentElement.style.overflow = 'hidden'; |
| // Apply fullscreen styles |
| _V_.addClass(this.box, "vjs-fullscreen"); |
| // Resize the box, controller, and poster |
| this.positionAll(); |
| }, |
| |
| // Turn off fullscreen (window) mode |
| exitFullWindow: function(){ |
| this.videoIsFullScreen = false; |
| document.removeEventListener("keydown", this.fullscreenOnEscKey, false); |
| window.removeEventListener("resize", this.fullscreenOnWindowResize, false); |
| // Unhide scroll bars. |
| document.documentElement.style.overflow = this.docOrigOverflow; |
| // Remove fullscreen styles |
| _V_.removeClass(this.box, "vjs-fullscreen"); |
| // Resize the box, controller, and poster to original sizes |
| this.positionAll(); |
| }, |
| |
| onError: function(fn){ this.addVideoListener("error", fn); return this; }, |
| onEnded: function(fn){ |
| this.addVideoListener("ended", fn); return this; |
| } |
| }); |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Element Behaviors |
| // Tell elements how to act or react |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| /* Player Behaviors - How VideoJS reacts to what the video is doing. |
| ================================================================================ */ |
| VideoJS.player.newBehavior("player", function(player){ |
| this.onError(this.playerOnVideoError); |
| // Listen for when the video is played |
| this.onPlay(this.playerOnVideoPlay); |
| this.onPlay(this.trackCurrentTime); |
| // Listen for when the video is paused |
| this.onPause(this.playerOnVideoPause); |
| this.onPause(this.stopTrackingCurrentTime); |
| // Listen for when the video ends |
| this.onEnded(this.playerOnVideoEnded); |
| // Set interval for load progress using buffer watching method |
| // this.trackCurrentTime(); |
| this.trackBuffered(); |
| // Buffer Full |
| this.onBufferedUpdate(this.isBufferFull); |
| },{ |
| playerOnVideoError: function(event){ |
| this.log(event); |
| this.log(this.video.error); |
| }, |
| playerOnVideoPlay: function(event){ this.hasPlayed = true; }, |
| playerOnVideoPause: function(event){}, |
| playerOnVideoEnded: function(event){ |
| this.currentTime(0); |
| this.pause(); |
| }, |
| |
| /* Load Tracking -------------------------------------------------------------- */ |
| // Buffer watching method for load progress. |
| // Used for browsers that don't support the progress event |
| trackBuffered: function(){ |
| this.bufferedInterval = setInterval(this.triggerBufferedListeners.context(this), 500); |
| }, |
| stopTrackingBuffered: function(){ clearInterval(this.bufferedInterval); }, |
| bufferedListeners: [], |
| onBufferedUpdate: function(fn){ |
| this.bufferedListeners.push(fn); |
| }, |
| triggerBufferedListeners: function(){ |
| this.isBufferFull(); |
| this.each(this.bufferedListeners, function(listener){ |
| (listener.context(this))(); |
| }); |
| }, |
| isBufferFull: function(){ |
| if (this.bufferedPercent() == 1) { this.stopTrackingBuffered(); } |
| }, |
| |
| /* Time Tracking -------------------------------------------------------------- */ |
| trackCurrentTime: function(){ |
| if (this.currentTimeInterval) { clearInterval(this.currentTimeInterval); } |
| this.currentTimeInterval = setInterval(this.triggerCurrentTimeListeners.context(this), 100); // 42 = 24 fps |
| this.trackingCurrentTime = true; |
| }, |
| // Turn off play progress tracking (when paused or dragging) |
| stopTrackingCurrentTime: function(){ |
| clearInterval(this.currentTimeInterval); |
| this.trackingCurrentTime = false; |
| }, |
| currentTimeListeners: [], |
| // onCurrentTimeUpdate is in API section now |
| triggerCurrentTimeListeners: function(late, newTime){ // FF passes milliseconds late as the first argument |
| this.each(this.currentTimeListeners, function(listener){ |
| (listener.context(this))(newTime || this.currentTime()); |
| }); |
| }, |
| |
| /* Resize Tracking -------------------------------------------------------------- */ |
| resizeListeners: [], |
| onResize: function(fn){ |
| this.resizeListeners.push(fn); |
| }, |
| // Trigger anywhere the video/box size is changed. |
| triggerResizeListeners: function(){ |
| this.each(this.resizeListeners, function(listener){ |
| (listener.context(this))(); |
| }); |
| } |
| } |
| ); |
| /* Mouse Over Video Reporter Behaviors - i.e. Controls hiding based on mouse location |
| ================================================================================ */ |
| VideoJS.player.newBehavior("mouseOverVideoReporter", function(element){ |
| // Listen for the mouse move the video. Used to reveal the controller. |
| _V_.addListener(element, "mousemove", this.mouseOverVideoReporterOnMouseMove.context(this)); |
| // Listen for the mouse moving out of the video. Used to hide the controller. |
| _V_.addListener(element, "mouseout", this.mouseOverVideoReporterOnMouseOut.context(this)); |
| },{ |
| mouseOverVideoReporterOnMouseMove: function(){ |
| this.showControlBars(); |
| clearInterval(this.mouseMoveTimeout); |
| this.mouseMoveTimeout = setTimeout(this.hideControlBars.context(this), 4000); |
| }, |
| mouseOverVideoReporterOnMouseOut: function(event){ |
| // Prevent flicker by making sure mouse hasn't left the video |
| var parent = event.relatedTarget; |
| while (parent && parent !== this.box) { |
| parent = parent.parentNode; |
| } |
| if (parent !== this.box) { |
| this.hideControlBars(); |
| } |
| } |
| } |
| ); |
| /* Mouse Over Video Reporter Behaviors - i.e. Controls hiding based on mouse location |
| ================================================================================ */ |
| VideoJS.player.newBehavior("box", function(element){ |
| this.positionBox(); |
| _V_.addClass(element, "vjs-paused"); |
| this.activateElement(element, "mouseOverVideoReporter"); |
| this.onPlay(this.boxOnVideoPlay); |
| this.onPause(this.boxOnVideoPause); |
| },{ |
| boxOnVideoPlay: function(){ |
| _V_.removeClass(this.box, "vjs-paused"); |
| _V_.addClass(this.box, "vjs-playing"); |
| }, |
| boxOnVideoPause: function(){ |
| _V_.removeClass(this.box, "vjs-playing"); |
| _V_.addClass(this.box, "vjs-paused"); |
| } |
| } |
| ); |
| /* Poster Image Overlay |
| ================================================================================ */ |
| VideoJS.player.newBehavior("poster", function(element){ |
| this.activateElement(element, "mouseOverVideoReporter"); |
| this.activateElement(element, "playButton"); |
| this.onPlay(this.hidePoster); |
| this.onEnded(this.showPoster); |
| this.onResize(this.positionPoster); |
| },{ |
| showPoster: function(){ |
| if (!this.poster) { return; } |
| this.poster.style.display = "block"; |
| this.positionPoster(); |
| }, |
| positionPoster: function(){ |
| // Only if the poster is visible |
| if (!this.poster || this.poster.style.display == 'none') { return; } |
| this.poster.style.height = this.height() + "px"; // Need incase controlsBelow |
| this.poster.style.width = this.width() + "px"; // Could probably do 100% of box |
| }, |
| hidePoster: function(){ |
| if (!this.poster) { return; } |
| this.poster.style.display = "none"; |
| }, |
| // Update poster source from attribute or fallback image |
| // iPad breaks if you include a poster attribute, so this fixes that |
| updatePosterSource: function(){ |
| if (!this.video.poster) { |
| var images = this.video.getElementsByTagName("img"); |
| if (images.length > 0) { this.video.poster = images[0].src; } |
| } |
| } |
| } |
| ); |
| /* Control Bar Behaviors |
| ================================================================================ */ |
| VideoJS.player.newBehavior("controlBar", function(element){ |
| if (!this.controlBars) { |
| this.controlBars = []; |
| this.onResize(this.positionControlBars); |
| } |
| this.controlBars.push(element); |
| _V_.addListener(element, "mousemove", this.onControlBarsMouseMove.context(this)); |
| _V_.addListener(element, "mouseout", this.onControlBarsMouseOut.context(this)); |
| },{ |
| showControlBars: function(){ |
| if (!this.options.controlsAtStart && !this.hasPlayed) { return; } |
| this.each(this.controlBars, function(bar){ |
| bar.style.display = "block"; |
| }); |
| }, |
| // Place controller relative to the video's position (now just resizing bars) |
| positionControlBars: function(){ |
| this.updatePlayProgressBars(); |
| this.updateLoadProgressBars(); |
| }, |
| hideControlBars: function(){ |
| if (this.options.controlsHiding && !this.mouseIsOverControls) { |
| this.each(this.controlBars, function(bar){ |
| bar.style.display = "none"; |
| }); |
| } |
| }, |
| // Block controls from hiding when mouse is over them. |
| onControlBarsMouseMove: function(){ this.mouseIsOverControls = true; }, |
| onControlBarsMouseOut: function(event){ |
| this.mouseIsOverControls = false; |
| } |
| } |
| ); |
| /* PlayToggle, PlayButton, PauseButton Behaviors |
| ================================================================================ */ |
| // Play Toggle |
| VideoJS.player.newBehavior("playToggle", function(element){ |
| if (!this.elements.playToggles) { |
| this.elements.playToggles = []; |
| this.onPlay(this.playTogglesOnPlay); |
| this.onPause(this.playTogglesOnPause); |
| } |
| this.elements.playToggles.push(element); |
| _V_.addListener(element, "click", this.onPlayToggleClick.context(this)); |
| },{ |
| onPlayToggleClick: function(event){ |
| if (this.paused()) { |
| this.play(); |
| } else { |
| this.pause(); |
| } |
| }, |
| playTogglesOnPlay: function(event){ |
| this.each(this.elements.playToggles, function(toggle){ |
| _V_.removeClass(toggle, "vjs-paused"); |
| _V_.addClass(toggle, "vjs-playing"); |
| }); |
| }, |
| playTogglesOnPause: function(event){ |
| this.each(this.elements.playToggles, function(toggle){ |
| _V_.removeClass(toggle, "vjs-playing"); |
| _V_.addClass(toggle, "vjs-paused"); |
| }); |
| } |
| } |
| ); |
| // Play |
| VideoJS.player.newBehavior("playButton", function(element){ |
| _V_.addListener(element, "click", this.onPlayButtonClick.context(this)); |
| },{ |
| onPlayButtonClick: function(event){ this.play(); } |
| } |
| ); |
| // Pause |
| VideoJS.player.newBehavior("pauseButton", function(element){ |
| _V_.addListener(element, "click", this.onPauseButtonClick.context(this)); |
| },{ |
| onPauseButtonClick: function(event){ this.pause(); } |
| } |
| ); |
| /* Play Progress Bar Behaviors |
| ================================================================================ */ |
| VideoJS.player.newBehavior("playProgressBar", function(element){ |
| if (!this.playProgressBars) { |
| this.playProgressBars = []; |
| this.onCurrentTimeUpdate(this.updatePlayProgressBars); |
| } |
| this.playProgressBars.push(element); |
| },{ |
| // Ajust the play progress bar's width based on the current play time |
| updatePlayProgressBars: function(newTime){ |
| var progress = (newTime !== undefined) ? newTime / this.duration() : this.currentTime() / this.duration(); |
| if (isNaN(progress)) { progress = 0; } |
| this.each(this.playProgressBars, function(bar){ |
| if (bar.style) { bar.style.width = _V_.round(progress * 100, 2) + "%"; } |
| }); |
| } |
| } |
| ); |
| /* Load Progress Bar Behaviors |
| ================================================================================ */ |
| VideoJS.player.newBehavior("loadProgressBar", function(element){ |
| if (!this.loadProgressBars) { this.loadProgressBars = []; } |
| this.loadProgressBars.push(element); |
| this.onBufferedUpdate(this.updateLoadProgressBars); |
| },{ |
| updateLoadProgressBars: function(){ |
| this.each(this.loadProgressBars, function(bar){ |
| if (bar.style) { bar.style.width = _V_.round(this.bufferedPercent() * 100, 2) + "%"; } |
| }); |
| } |
| } |
| ); |
| |
| /* Current Time Display Behaviors |
| ================================================================================ */ |
| VideoJS.player.newBehavior("currentTimeDisplay", function(element){ |
| if (!this.currentTimeDisplays) { |
| this.currentTimeDisplays = []; |
| this.onCurrentTimeUpdate(this.updateCurrentTimeDisplays); |
| } |
| this.currentTimeDisplays.push(element); |
| },{ |
| // Update the displayed time (00:00) |
| updateCurrentTimeDisplays: function(newTime){ |
| if (!this.currentTimeDisplays) { return; } |
| // Allows for smooth scrubbing, when player can't keep up. |
| var time = (newTime) ? newTime : this.currentTime(); |
| this.each(this.currentTimeDisplays, function(dis){ |
| dis.innerHTML = _V_.formatTime(time); |
| }); |
| } |
| } |
| ); |
| |
| /* Duration Display Behaviors |
| ================================================================================ */ |
| VideoJS.player.newBehavior("durationDisplay", function(element){ |
| if (!this.durationDisplays) { |
| this.durationDisplays = []; |
| this.onCurrentTimeUpdate(this.updateDurationDisplays); |
| } |
| this.durationDisplays.push(element); |
| },{ |
| updateDurationDisplays: function(){ |
| if (!this.durationDisplays) { return; } |
| this.each(this.durationDisplays, function(dis){ |
| if (this.duration()) { dis.innerHTML = _V_.formatTime(this.duration()); } |
| }); |
| } |
| } |
| ); |
| |
| /* Current Time Scrubber Behaviors |
| ================================================================================ */ |
| VideoJS.player.newBehavior("currentTimeScrubber", function(element){ |
| _V_.addListener(element, "mousedown", this.onCurrentTimeScrubberMouseDown.rEvtContext(this)); |
| },{ |
| // Adjust the play position when the user drags on the progress bar |
| onCurrentTimeScrubberMouseDown: function(event, scrubber){ |
| event.preventDefault(); |
| this.currentScrubber = scrubber; |
| |
| this.stopTrackingCurrentTime(); // Allows for smooth scrubbing |
| |
| this.videoWasPlaying = !this.paused(); |
| this.pause(); |
| |
| _V_.blockTextSelection(); |
| this.setCurrentTimeWithScrubber(event); |
| _V_.addListener(document, "mousemove", this.onCurrentTimeScrubberMouseMove.rEvtContext(this)); |
| _V_.addListener(document, "mouseup", this.onCurrentTimeScrubberMouseUp.rEvtContext(this)); |
| }, |
| onCurrentTimeScrubberMouseMove: function(event){ // Removable |
| this.setCurrentTimeWithScrubber(event); |
| }, |
| onCurrentTimeScrubberMouseUp: function(event){ // Removable |
| _V_.unblockTextSelection(); |
| document.removeEventListener("mousemove", this.onCurrentTimeScrubberMouseMove, false); |
| document.removeEventListener("mouseup", this.onCurrentTimeScrubberMouseUp, false); |
| if (this.videoWasPlaying) { |
| this.play(); |
| this.trackCurrentTime(); |
| } |
| }, |
| setCurrentTimeWithScrubber: function(event){ |
| var newProgress = _V_.getRelativePosition(event.pageX, this.currentScrubber); |
| var newTime = newProgress * this.duration(); |
| this.triggerCurrentTimeListeners(0, newTime); // Allows for smooth scrubbing |
| // Don't let video end while scrubbing. |
| if (newTime == this.duration()) { newTime = newTime - 0.1; } |
| this.currentTime(newTime); |
| } |
| } |
| ); |
| /* Volume Display Behaviors |
| ================================================================================ */ |
| VideoJS.player.newBehavior("volumeDisplay", function(element){ |
| if (!this.volumeDisplays) { |
| this.volumeDisplays = []; |
| this.onVolumeChange(this.updateVolumeDisplays); |
| } |
| this.volumeDisplays.push(element); |
| this.updateVolumeDisplay(element); // Set the display to the initial volume |
| },{ |
| // Update the volume control display |
| // Unique to these default controls. Uses borders to create the look of bars. |
| updateVolumeDisplays: function(){ |
| if (!this.volumeDisplays) { return; } |
| this.each(this.volumeDisplays, function(dis){ |
| this.updateVolumeDisplay(dis); |
| }); |
| }, |
| updateVolumeDisplay: function(display){ |
| var volNum = Math.ceil(this.volume() * 6); |
| this.each(display.children, function(child, num){ |
| if (num < volNum) { |
| _V_.addClass(child, "vjs-volume-level-on"); |
| } else { |
| _V_.removeClass(child, "vjs-volume-level-on"); |
| } |
| }); |
| } |
| } |
| ); |
| /* Volume Scrubber Behaviors |
| ================================================================================ */ |
| VideoJS.player.newBehavior("volumeScrubber", function(element){ |
| _V_.addListener(element, "mousedown", this.onVolumeScrubberMouseDown.rEvtContext(this)); |
| },{ |
| // Adjust the volume when the user drags on the volume control |
| onVolumeScrubberMouseDown: function(event, scrubber){ |
| // event.preventDefault(); |
| _V_.blockTextSelection(); |
| this.currentScrubber = scrubber; |
| this.setVolumeWithScrubber(event); |
| _V_.addListener(document, "mousemove", this.onVolumeScrubberMouseMove.rEvtContext(this)); |
| _V_.addListener(document, "mouseup", this.onVolumeScrubberMouseUp.rEvtContext(this)); |
| }, |
| onVolumeScrubberMouseMove: function(event){ |
| this.setVolumeWithScrubber(event); |
| }, |
| onVolumeScrubberMouseUp: function(event){ |
| this.setVolumeWithScrubber(event); |
| _V_.unblockTextSelection(); |
| document.removeEventListener("mousemove", this.onVolumeScrubberMouseMove, false); |
| document.removeEventListener("mouseup", this.onVolumeScrubberMouseUp, false); |
| }, |
| setVolumeWithScrubber: function(event){ |
| var newVol = _V_.getRelativePosition(event.pageX, this.currentScrubber); |
| this.volume(newVol); |
| } |
| } |
| ); |
| /* Fullscreen Toggle Behaviors |
| ================================================================================ */ |
| VideoJS.player.newBehavior("fullscreenToggle", function(element){ |
| _V_.addListener(element, "click", this.onFullscreenToggleClick.context(this)); |
| },{ |
| // When the user clicks on the fullscreen button, update fullscreen setting |
| onFullscreenToggleClick: function(event){ |
| if (!this.videoIsFullScreen) { |
| this.enterFullScreen(); |
| } else { |
| this.exitFullScreen(); |
| } |
| }, |
| |
| fullscreenOnWindowResize: function(event){ // Removable |
| this.positionControlBars(); |
| }, |
| // Create listener for esc key while in full screen mode |
| fullscreenOnEscKey: function(event){ // Removable |
| if (event.keyCode == 27) { |
| this.exitFullScreen(); |
| } |
| } |
| } |
| ); |
| /* Big Play Button Behaviors |
| ================================================================================ */ |
| VideoJS.player.newBehavior("bigPlayButton", function(element){ |
| if (!this.elements.bigPlayButtons) { |
| this.elements.bigPlayButtons = []; |
| this.onPlay(this.bigPlayButtonsOnPlay); |
| this.onEnded(this.bigPlayButtonsOnEnded); |
| } |
| this.elements.bigPlayButtons.push(element); |
| this.activateElement(element, "playButton"); |
| },{ |
| bigPlayButtonsOnPlay: function(event){ this.hideBigPlayButtons(); }, |
| bigPlayButtonsOnEnded: function(event){ this.showBigPlayButtons(); }, |
| showBigPlayButtons: function(){ |
| this.each(this.elements.bigPlayButtons, function(element){ |
| element.style.display = "block"; |
| }); |
| }, |
| hideBigPlayButtons: function(){ |
| this.each(this.elements.bigPlayButtons, function(element){ |
| element.style.display = "none"; |
| }); |
| } |
| } |
| ); |
| /* Spinner |
| ================================================================================ */ |
| VideoJS.player.newBehavior("spinner", function(element){ |
| if (!this.spinners) { |
| this.spinners = []; |
| _V_.addListener(this.video, "loadeddata", this.spinnersOnVideoLoadedData.context(this)); |
| _V_.addListener(this.video, "loadstart", this.spinnersOnVideoLoadStart.context(this)); |
| _V_.addListener(this.video, "seeking", this.spinnersOnVideoSeeking.context(this)); |
| _V_.addListener(this.video, "seeked", this.spinnersOnVideoSeeked.context(this)); |
| _V_.addListener(this.video, "canplay", this.spinnersOnVideoCanPlay.context(this)); |
| _V_.addListener(this.video, "canplaythrough", this.spinnersOnVideoCanPlayThrough.context(this)); |
| _V_.addListener(this.video, "waiting", this.spinnersOnVideoWaiting.context(this)); |
| _V_.addListener(this.video, "stalled", this.spinnersOnVideoStalled.context(this)); |
| _V_.addListener(this.video, "suspend", this.spinnersOnVideoSuspend.context(this)); |
| _V_.addListener(this.video, "playing", this.spinnersOnVideoPlaying.context(this)); |
| _V_.addListener(this.video, "timeupdate", this.spinnersOnVideoTimeUpdate.context(this)); |
| } |
| this.spinners.push(element); |
| },{ |
| showSpinners: function(){ |
| this.each(this.spinners, function(spinner){ |
| spinner.style.display = "block"; |
| }); |
| clearInterval(this.spinnerInterval); |
| this.spinnerInterval = setInterval(this.rotateSpinners.context(this), 100); |
| }, |
| hideSpinners: function(){ |
| this.each(this.spinners, function(spinner){ |
| spinner.style.display = "none"; |
| }); |
| clearInterval(this.spinnerInterval); |
| }, |
| spinnersRotated: 0, |
| rotateSpinners: function(){ |
| this.each(this.spinners, function(spinner){ |
| // spinner.style.transform = 'scale(0.5) rotate('+this.spinnersRotated+'deg)'; |
| spinner.style.WebkitTransform = 'scale(0.5) rotate('+this.spinnersRotated+'deg)'; |
| spinner.style.MozTransform = 'scale(0.5) rotate('+this.spinnersRotated+'deg)'; |
| }); |
| if (this.spinnersRotated == 360) { this.spinnersRotated = 0; } |
| this.spinnersRotated += 45; |
| }, |
| spinnersOnVideoLoadedData: function(event){ this.hideSpinners(); }, |
| spinnersOnVideoLoadStart: function(event){ this.showSpinners(); }, |
| spinnersOnVideoSeeking: function(event){ /* this.showSpinners(); */ }, |
| spinnersOnVideoSeeked: function(event){ /* this.hideSpinners(); */ }, |
| spinnersOnVideoCanPlay: function(event){ /* this.hideSpinners(); */ }, |
| spinnersOnVideoCanPlayThrough: function(event){ this.hideSpinners(); }, |
| spinnersOnVideoWaiting: function(event){ |
| // Safari sometimes triggers waiting inappropriately |
| // Like after video has played, any you play again. |
| this.showSpinners(); |
| }, |
| spinnersOnVideoStalled: function(event){}, |
| spinnersOnVideoSuspend: function(event){}, |
| spinnersOnVideoPlaying: function(event){ this.hideSpinners(); }, |
| spinnersOnVideoTimeUpdate: function(event){ |
| // Safari sometimes calls waiting and doesn't recover |
| if(this.spinner.style.display == "block") { this.hideSpinners(); } |
| } |
| } |
| ); |
| /* Subtitles |
| ================================================================================ */ |
| VideoJS.player.newBehavior("subtitlesDisplay", function(element){ |
| if (!this.subtitleDisplays) { |
| this.subtitleDisplays = []; |
| this.onCurrentTimeUpdate(this.subtitleDisplaysOnVideoTimeUpdate); |
| this.onEnded(function() { this.lastSubtitleIndex = 0; }.context(this)); |
| } |
| this.subtitleDisplays.push(element); |
| },{ |
| subtitleDisplaysOnVideoTimeUpdate: function(time){ |
| // Assuming all subtitles are in order by time, and do not overlap |
| if (this.subtitles) { |
| // If current subtitle should stay showing, don't do anything. Otherwise, find new subtitle. |
| if (!this.currentSubtitle || this.currentSubtitle.start >= time || this.currentSubtitle.end < time) { |
| var newSubIndex = false, |
| // Loop in reverse if lastSubtitle is after current time (optimization) |
| // Meaning the user is scrubbing in reverse or rewinding |
| reverse = (this.subtitles[this.lastSubtitleIndex].start > time), |
| // If reverse, step back 1 becase we know it's not the lastSubtitle |
| i = this.lastSubtitleIndex - (reverse) ? 1 : 0; |
| while (true) { // Loop until broken |
| if (reverse) { // Looping in reverse |
| // Stop if no more, or this subtitle ends before the current time (no earlier subtitles should apply) |
| if (i < 0 || this.subtitles[i].end < time) { break; } |
| // End is greater than time, so if start is less, show this subtitle |
| if (this.subtitles[i].start < time) { |
| newSubIndex = i; |
| break; |
| } |
| i--; |
| } else { // Looping forward |
| // Stop if no more, or this subtitle starts after time (no later subtitles should apply) |
| if (i >= this.subtitles.length || this.subtitles[i].start > time) { break; } |
| // Start is less than time, so if end is later, show this subtitle |
| if (this.subtitles[i].end > time) { |
| newSubIndex = i; |
| break; |
| } |
| i++; |
| } |
| } |
| |
| // Set or clear current subtitle |
| if (newSubIndex !== false) { |
| this.currentSubtitle = this.subtitles[newSubIndex]; |
| this.lastSubtitleIndex = newSubIndex; |
| this.updateSubtitleDisplays(this.currentSubtitle.text); |
| } else if (this.currentSubtitle) { |
| this.currentSubtitle = false; |
| this.updateSubtitleDisplays(""); |
| } |
| } |
| } |
| }, |
| updateSubtitleDisplays: function(val){ |
| this.each(this.subtitleDisplays, function(disp){ |
| disp.innerHTML = val; |
| }); |
| } |
| } |
| ); |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Convenience Functions (mini library) |
| // Functions not specific to video or VideoJS and could probably be replaced with a library like jQuery |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| VideoJS.extend({ |
| |
| addClass: function(element, classToAdd){ |
| if ((" "+element.className+" ").indexOf(" "+classToAdd+" ") == -1) { |
| element.className = element.className === "" ? classToAdd : element.className + " " + classToAdd; |
| } |
| }, |
| removeClass: function(element, classToRemove){ |
| if (element.className.indexOf(classToRemove) == -1) { return; } |
| var classNames = element.className.split(/\s+/); |
| classNames.splice(classNames.lastIndexOf(classToRemove),1); |
| element.className = classNames.join(" "); |
| }, |
| createElement: function(tagName, attributes){ |
| return this.merge(document.createElement(tagName), attributes); |
| }, |
| |
| // Attempt to block the ability to select text while dragging controls |
| blockTextSelection: function(){ |
| document.body.focus(); |
| document.onselectstart = function () { return false; }; |
| }, |
| // Turn off text selection blocking |
| unblockTextSelection: function(){ document.onselectstart = function () { return true; }; }, |
| |
| // Return seconds as MM:SS |
| formatTime: function(secs) { |
| var seconds = Math.round(secs); |
| var minutes = Math.floor(seconds / 60); |
| minutes = (minutes >= 10) ? minutes : "0" + minutes; |
| seconds = Math.floor(seconds % 60); |
| seconds = (seconds >= 10) ? seconds : "0" + seconds; |
| return minutes + ":" + seconds; |
| }, |
| |
| // Return the relative horizonal position of an event as a value from 0-1 |
| getRelativePosition: function(x, relativeElement){ |
| return Math.max(0, Math.min(1, (x - this.findPosX(relativeElement)) / relativeElement.offsetWidth)); |
| }, |
| // Get an objects position on the page |
| findPosX: function(obj) { |
| var curleft = obj.offsetLeft; |
| while(obj = obj.offsetParent) { |
| curleft += obj.offsetLeft; |
| } |
| return curleft; |
| }, |
| getComputedStyleValue: function(element, style){ |
| return window.getComputedStyle(element, null).getPropertyValue(style); |
| }, |
| |
| round: function(num, dec) { |
| if (!dec) { dec = 0; } |
| return Math.round(num*Math.pow(10,dec))/Math.pow(10,dec); |
| }, |
| |
| addListener: function(element, type, handler){ |
| if (element.addEventListener) { |
| element.addEventListener(type, handler, false); |
| } else if (element.attachEvent) { |
| element.attachEvent("on"+type, handler); |
| } |
| }, |
| removeListener: function(element, type, handler){ |
| if (element.removeEventListener) { |
| element.removeEventListener(type, handler, false); |
| } else if (element.attachEvent) { |
| element.detachEvent("on"+type, handler); |
| } |
| }, |
| |
| get: function(url, onSuccess){ |
| if (typeof XMLHttpRequest == "undefined") { |
| XMLHttpRequest = function () { |
| try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e) {} |
| try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (f) {} |
| try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (g) {} |
| //Microsoft.XMLHTTP points to Msxml2.XMLHTTP.3.0 and is redundant |
| throw new Error("This browser does not support XMLHttpRequest."); |
| }; |
| } |
| var request = new XMLHttpRequest(); |
| request.open("GET",url); |
| request.onreadystatechange = function() { |
| if (request.readyState == 4 && request.status == 200) { |
| onSuccess(request.responseText); |
| } |
| }.context(this); |
| request.send(); |
| }, |
| |
| trim: function(string){ return string.toString().replace(/^\s+/, "").replace(/\s+$/, ""); }, |
| |
| // DOM Ready functionality adapted from jQuery. http://jquery.com/ |
| bindDOMReady: function(){ |
| if (document.readyState === "complete") { |
| return VideoJS.onDOMReady(); |
| } |
| if (document.addEventListener) { |
| document.addEventListener("DOMContentLoaded", VideoJS.DOMContentLoaded, false); |
| window.addEventListener("load", VideoJS.onDOMReady, false); |
| } else if (document.attachEvent) { |
| document.attachEvent("onreadystatechange", VideoJS.DOMContentLoaded); |
| window.attachEvent("onload", VideoJS.onDOMReady); |
| } |
| }, |
| |
| DOMContentLoaded: function(){ |
| if (document.addEventListener) { |
| document.removeEventListener( "DOMContentLoaded", VideoJS.DOMContentLoaded, false); |
| VideoJS.onDOMReady(); |
| } else if ( document.attachEvent ) { |
| if ( document.readyState === "complete" ) { |
| document.detachEvent("onreadystatechange", VideoJS.DOMContentLoaded); |
| VideoJS.onDOMReady(); |
| } |
| } |
| }, |
| |
| // Functions to be run once the DOM is loaded |
| DOMReadyList: [], |
| addToDOMReady: function(fn){ |
| if (VideoJS.DOMIsReady) { |
| fn.call(document); |
| } else { |
| VideoJS.DOMReadyList.push(fn); |
| } |
| }, |
| |
| DOMIsReady: false, |
| onDOMReady: function(){ |
| if (VideoJS.DOMIsReady) { return; } |
| if (!document.body) { return setTimeout(VideoJS.onDOMReady, 13); } |
| VideoJS.DOMIsReady = true; |
| if (VideoJS.DOMReadyList) { |
| for (var i=0; i<VideoJS.DOMReadyList.length; i++) { |
| VideoJS.DOMReadyList[i].call(document); |
| } |
| VideoJS.DOMReadyList = null; |
| } |
| } |
| }); |
| VideoJS.bindDOMReady(); |
| |
| // Allows for binding context to functions |
| // when using in event listeners and timeouts |
| Function.prototype.context = function(obj){ |
| var method = this, |
| temp = function(){ |
| return method.apply(obj, arguments); |
| }; |
| return temp; |
| }; |
| |
| // Like context, in that it creates a closure |
| // But insteaad keep "this" intact, and passes the var as the second argument of the function |
| // Need for event listeners where you need to know what called the event |
| // Only use with event callbacks |
| Function.prototype.evtContext = function(obj){ |
| var method = this, |
| temp = function(){ |
| var origContext = this; |
| return method.call(obj, arguments[0], origContext); |
| }; |
| return temp; |
| }; |
| |
| // Removable Event listener with Context |
| // Replaces the original function with a version that has context |
| // So it can be removed using the original function name. |
| // In order to work, a version of the function must already exist in the player/prototype |
| Function.prototype.rEvtContext = function(obj, funcParent){ |
| if (this.hasContext === true) { return this; } |
| if (!funcParent) { funcParent = obj; } |
| for (var attrname in funcParent) { |
| if (funcParent[attrname] == this) { |
| funcParent[attrname] = this.evtContext(obj); |
| funcParent[attrname].hasContext = true; |
| return funcParent[attrname]; |
| } |
| } |
| return this.evtContext(obj); |
| }; |
| |
| // jQuery Plugin |
| if (window.jQuery) { |
| (function($) { |
| $.fn.VideoJS = function(options) { |
| this.each(function() { |
| VideoJS.setup(this, options); |
| }); |
| return this; |
| }; |
| $.fn.player = function() { |
| return this[0].player; |
| }; |
| })(jQuery); |
| } |
| |
| |
| // Expose to global |
| window.VideoJS = window._V_ = VideoJS; |
| |
| // End self-executing function |
| })(window); |