/* 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 . */ // 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= 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 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
00:00 / 00:00
*/ // 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: "" }); 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: "
" }); 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: "
" }); 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
*/ this.bigPlayButton = _V_.createElement("div", { className: "vjs-big-play-button", innerHTML: "" }); this.box.appendChild(this.bigPlayButton); this.activateElement(this.bigPlayButton, "bigPlayButton"); }, /* Spinner (Loading) ================================================================================ */ buildAndActivateSpinner: function(){ this.spinner = _V_.createElement("div", { className: "vjs-spinner", innerHTML: "
" }); 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 "); subtitle.start = this.parseSubtitleTime(time[0]); subtitle.end = this.parseSubtitleTime(time[1]); // Additional lines - Subtitle Text text = []; for (var j=i; j'); // 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
*/ 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