/* Control - Base class for all control elements ================================================================================ */ _V_.Control = _V_.Component.extend({ buildCSSClass: function(){ return "vjs-control " + this._super(); } }); /* Button - Base class for all buttons ================================================================================ */ _V_.Button = _V_.Control.extend({ init: function(player, options){ this._super(player, options); this.addEvent("click", this.onClick); this.addEvent("focus", this.onFocus); this.addEvent("blur", this.onBlur); }, createElement: function(type, attrs){ // Add standard Aria and Tabindex info attrs = _V_.merge({ className: this.buildCSSClass(), innerHTML: '
' + (this.buttonText || "Need Text") + '
', role: "button", tabIndex: 0 }, attrs); return this._super(type, attrs); }, // Click - Override with specific functionality for button onClick: function(){}, // Focus - Add keyboard functionality to element onFocus: function(){ _V_.addEvent(document, "keyup", _V_.proxy(this, this.onKeyPress)); }, // KeyPress (document level) - Trigger click when keys are pressed onKeyPress: function(event){ // Check for space bar (32) or enter (13) keys if (event.which == 32 || event.which == 13) { event.preventDefault(); this.onClick(); } }, // Blur - Remove keyboard triggers onBlur: function(){ _V_.removeEvent(document, "keyup", _V_.proxy(this, this.onKeyPress)); } }); /* Play Button ================================================================================ */ _V_.PlayButton = _V_.Button.extend({ buttonText: "Play", buildCSSClass: function(){ return "vjs-play-button " + this._super(); }, onClick: function(){ this.player.play(); } }); /* Pause Button ================================================================================ */ _V_.PauseButton = _V_.Button.extend({ buttonText: "Pause", buildCSSClass: function(){ return "vjs-pause-button " + this._super(); }, onClick: function(){ this.player.pause(); } }); /* Play Toggle - Play or Pause Media ================================================================================ */ _V_.PlayToggle = _V_.Button.extend({ buttonText: "Play", init: function(player, options){ this._super(player, options); player.addEvent("play", _V_.proxy(this, this.onPlay)); player.addEvent("pause", _V_.proxy(this, this.onPause)); }, buildCSSClass: function(){ return "vjs-play-control " + this._super(); }, // OnClick - Toggle between play and pause onClick: function(){ if (this.player.paused()) { this.player.play(); } else { this.player.pause(); } }, // OnPlay - Add the vjs-playing class to the element so it can change appearance onPlay: function(){ _V_.removeClass(this.el, "vjs-paused"); _V_.addClass(this.el, "vjs-playing"); }, // OnPause - Add the vjs-paused class to the element so it can change appearance onPause: function(){ _V_.removeClass(this.el, "vjs-playing"); _V_.addClass(this.el, "vjs-paused"); } }); /* Fullscreen Toggle Behaviors ================================================================================ */ _V_.FullscreenToggle = _V_.Button.extend({ buttonText: "Fullscreen", buildCSSClass: function(){ return "vjs-fullscreen-control " + this._super(); }, onClick: function(){ if (!this.player.isFullScreen) { this.player.requestFullScreen(); } else { this.player.cancelFullScreen(); } } }); /* Big Play Button ================================================================================ */ _V_.BigPlayButton = _V_.Button.extend({ init: function(player, options){ this._super(player, options); player.addEvent("play", _V_.proxy(this, this.hide)); player.addEvent("ended", _V_.proxy(this, this.show)); }, createElement: function(){ return this._super("div", { className: "vjs-big-play-button", innerHTML: "" }); }, onClick: function(){ // Go back to the beginning if big play button is showing at the end. // Have to check for current time otherwise it might throw a 'not ready' error. if(this.player.currentTime()) { this.player.currentTime(0); } this.player.play(); } }); /* Loading Spinner ================================================================================ */ _V_.LoadingSpinner = _V_.Component.extend({ init: function(player, options){ this._super(player, options); player.addEvent("canplay", _V_.proxy(this, this.hide)); player.addEvent("canplaythrough", _V_.proxy(this, this.hide)); player.addEvent("playing", _V_.proxy(this, this.hide)); player.addEvent("seeking", _V_.proxy(this, this.show)); player.addEvent("error", _V_.proxy(this, this.show)); // Not showing spinner on stalled any more. Browsers may stall and then not trigger any events that would remove the spinner. // Checked in Chrome 16 and Safari 5.1.2. http://help.videojs.com/discussions/problems/883-why-is-the-download-progress-showing // player.addEvent("stalled", _V_.proxy(this, this.show)); player.addEvent("waiting", _V_.proxy(this, this.show)); }, createElement: function(){ var classNameSpinner, innerHtmlSpinner; if ( typeof this.player.el.style.WebkitBorderRadius == "string" || typeof this.player.el.style.MozBorderRadius == "string" || typeof this.player.el.style.KhtmlBorderRadius == "string" || typeof this.player.el.style.borderRadius == "string") { classNameSpinner = "vjs-loading-spinner"; innerHtmlSpinner = "
"; } else { classNameSpinner = "vjs-loading-spinner-fallback"; innerHtmlSpinner = ""; } return this._super("div", { className: classNameSpinner, innerHTML: innerHtmlSpinner }); } }); /* Control Bar ================================================================================ */ _V_.ControlBar = _V_.Component.extend({ options: { loadEvent: "play", components: { "playToggle": {}, "fullscreenToggle": {}, "currentTimeDisplay": {}, "timeDivider": {}, "durationDisplay": {}, "remainingTimeDisplay": {}, "progressControl": {}, "volumeControl": {}, "muteToggle": {} } }, init: function(player, options){ this._super(player, options); player.addEvent("play", this.proxy(function(){ this.fadeIn(); this.player.addEvent("mouseover", this.proxy(this.fadeIn)); this.player.addEvent("mouseout", this.proxy(this.fadeOut)); })); }, createElement: function(){ return _V_.createElement("div", { className: "vjs-controls" }); }, fadeIn: function(){ this._super(); this.player.triggerEvent("controlsvisible"); }, fadeOut: function(){ this._super(); this.player.triggerEvent("controlshidden"); } }); /* Time ================================================================================ */ _V_.CurrentTimeDisplay = _V_.Component.extend({ init: function(player, options){ this._super(player, options); player.addEvent("timeupdate", _V_.proxy(this, this.updateContent)); }, createElement: function(){ var el = this._super("div", { className: "vjs-current-time vjs-time-controls vjs-control" }); this.content = _V_.createElement("div", { className: "vjs-current-time-display", innerHTML: '0:00' }); el.appendChild(_V_.createElement("div").appendChild(this.content)); return el; }, updateContent: function(){ // Allows for smooth scrubbing, when player can't keep up. var time = (this.player.scrubbing) ? this.player.values.currentTime : this.player.currentTime(); this.content.innerHTML = _V_.formatTime(time, this.player.duration()); } }); _V_.DurationDisplay = _V_.Component.extend({ init: function(player, options){ this._super(player, options); player.addEvent("timeupdate", _V_.proxy(this, this.updateContent)); }, createElement: function(){ var el = this._super("div", { className: "vjs-duration vjs-time-controls vjs-control" }); this.content = _V_.createElement("div", { className: "vjs-duration-display", innerHTML: '0:00' }); el.appendChild(_V_.createElement("div").appendChild(this.content)); return el; }, updateContent: function(){ if (this.player.duration()) { this.content.innerHTML = _V_.formatTime(this.player.duration()); } } }); // Time Separator (Not used in main skin, but still available, and could be used as a 'spare element') _V_.TimeDivider = _V_.Component.extend({ createElement: function(){ return this._super("div", { className: "vjs-time-divider", innerHTML: '
/
' }); } }); _V_.RemainingTimeDisplay = _V_.Component.extend({ init: function(player, options){ this._super(player, options); player.addEvent("timeupdate", _V_.proxy(this, this.updateContent)); }, createElement: function(){ var el = this._super("div", { className: "vjs-remaining-time vjs-time-controls vjs-control" }); this.content = _V_.createElement("div", { className: "vjs-remaining-time-display", innerHTML: '-0:00' }); el.appendChild(_V_.createElement("div").appendChild(this.content)); return el; }, updateContent: function(){ if (this.player.duration()) { this.content.innerHTML = "-"+_V_.formatTime(this.player.remainingTime()); } // Allows for smooth scrubbing, when player can't keep up. // var time = (this.player.scrubbing) ? this.player.values.currentTime : this.player.currentTime(); // this.content.innerHTML = _V_.formatTime(time, this.player.duration()); } }); /* Slider - Parent for seek bar and volume slider ================================================================================ */ _V_.Slider = _V_.Component.extend({ init: function(player, options){ this._super(player, options); player.addEvent(this.playerEvent, _V_.proxy(this, this.update)); this.addEvent("mousedown", this.onMouseDown); this.addEvent("focus", this.onFocus); this.addEvent("blur", this.onBlur); this.player.addEvent("controlsvisible", this.proxy(this.update)); // This is actually to fix the volume handle position. http://twitter.com/#!/gerritvanaaken/status/159046254519787520 // this.player.one("timeupdate", this.proxy(this.update)); this.update(); }, createElement: function(type, attrs) { attrs = _V_.merge({ role: "slider", "aria-valuenow": 0, "aria-valuemin": 0, "aria-valuemax": 100, tabIndex: 0 }, attrs); return this._super(type, attrs); }, onMouseDown: function(event){ event.preventDefault(); _V_.blockTextSelection(); _V_.addEvent(document, "mousemove", _V_.proxy(this, this.onMouseMove)); _V_.addEvent(document, "mouseup", _V_.proxy(this, this.onMouseUp)); this.onMouseMove(event); }, onMouseUp: function(event) { _V_.unblockTextSelection(); _V_.removeEvent(document, "mousemove", this.onMouseMove, false); _V_.removeEvent(document, "mouseup", this.onMouseUp, false); this.update(); }, update: function(){ // If scrubbing, we could use a cached value to make the handle keep up with the user's mouse. // On HTML5 browsers scrubbing is really smooth, but some flash players are slow, so we might want to utilize this later. // var progress = (this.player.scrubbing) ? this.player.values.currentTime / this.player.duration() : this.player.currentTime() / this.player.duration(); var barProgress, progress = this.getPercent(); handle = this.handle, bar = this.bar; // Protect against no duration and other division issues if (isNaN(progress)) { progress = 0; } barProgress = progress; // If there is a handle, we need to account for the handle in our calculation for progress bar // so that it doesn't fall short of or extend past the handle. if (handle) { var box = this.el, boxWidth = box.offsetWidth, handleWidth = handle.el.offsetWidth, // The width of the handle in percent of the containing box // In IE, widths may not be ready yet causing NaN handlePercent = (handleWidth) ? handleWidth / boxWidth : 0, // Get the adjusted size of the box, considering that the handle's center never touches the left or right side. // There is a margin of half the handle's width on both sides. boxAdjustedPercent = 1 - handlePercent; // Adjust the progress that we'll use to set widths to the new adjusted box width adjustedProgress = progress * boxAdjustedPercent, // The bar does reach the left side, so we need to account for this in the bar's width barProgress = adjustedProgress + (handlePercent / 2); // Move the handle from the left based on the adjected progress handle.el.style.left = _V_.round(adjustedProgress * 100, 2) + "%"; } // Set the new bar width bar.el.style.width = _V_.round(barProgress * 100, 2) + "%"; }, calculateDistance: function(event){ var box = this.el, boxX = _V_.findPosX(box), boxW = box.offsetWidth, handle = this.handle; if (handle) { var handleW = handle.el.offsetWidth; // Adjusted X and Width, so handle doesn't go outside the bar boxX = boxX + (handleW / 2); boxW = boxW - handleW; } // Percent that the click is through the adjusted area return Math.max(0, Math.min(1, (event.pageX - boxX) / boxW)); }, onFocus: function(event){ _V_.addEvent(document, "keyup", _V_.proxy(this, this.onKeyPress)); }, onKeyPress: function(event){ if (event.which == 37) { // Left Arrow event.preventDefault(); this.stepBack(); } else if (event.which == 39) { // Right Arrow event.preventDefault(); this.stepForward(); } }, onBlur: function(event){ _V_.removeEvent(document, "keyup", _V_.proxy(this, this.onKeyPress)); } }); /* Progress ================================================================================ */ // Progress Control: Seek, Load Progress, and Play Progress _V_.ProgressControl = _V_.Component.extend({ options: { components: { "seekBar": {} } }, createElement: function(){ return this._super("div", { className: "vjs-progress-control vjs-control" }); } }); // Seek Bar and holder for the progress bars _V_.SeekBar = _V_.Slider.extend({ options: { components: { "loadProgressBar": {}, // Set property names to bar and handle to match with the parent Slider class is looking for "bar": { componentClass: "PlayProgressBar" }, "handle": { componentClass: "SeekHandle" } } }, playerEvent: "timeupdate", init: function(player, options){ this._super(player, options); }, createElement: function(){ return this._super("div", { className: "vjs-progress-holder" }); }, getPercent: function(){ return this.player.currentTime() / this.player.duration(); }, onMouseDown: function(event){ this._super(event); this.player.scrubbing = true; this.videoWasPlaying = !this.player.paused(); this.player.pause(); }, onMouseMove: function(event){ var newTime = this.calculateDistance(event) * this.player.duration(); // Don't let video end while scrubbing. if (newTime == this.player.duration()) { newTime = newTime - 0.1; } // Set new time (tell player to seek to new time) this.player.currentTime(newTime); }, onMouseUp: function(event){ this._super(event); this.player.scrubbing = false; if (this.videoWasPlaying) { this.player.play(); } }, stepForward: function(){ this.player.currentTime(this.player.currentTime() + 1); }, stepBack: function(){ this.player.currentTime(this.player.currentTime() - 1); } }); // Load Progress Bar _V_.LoadProgressBar = _V_.Component.extend({ init: function(player, options){ this._super(player, options); player.addEvent("progress", _V_.proxy(this, this.update)); }, createElement: function(){ return this._super("div", { className: "vjs-load-progress", innerHTML: 'Loaded: 0%' }); }, update: function(){ if (this.el.style) { this.el.style.width = _V_.round(this.player.bufferedPercent() * 100, 2) + "%"; } } }); // Play Progress Bar _V_.PlayProgressBar = _V_.Component.extend({ createElement: function(){ return this._super("div", { className: "vjs-play-progress", innerHTML: 'Progress: 0%' }); } }); // Seek Handle // SeekBar Behavior includes play progress bar, and seek handle // Needed so it can determine seek position based on handle position/size _V_.SeekHandle = _V_.Component.extend({ createElement: function(){ return this._super("div", { className: "vjs-seek-handle", innerHTML: '00:00' }); } }); /* Volume Scrubber ================================================================================ */ // Progress Control: Seek, Load Progress, and Play Progress _V_.VolumeControl = _V_.Component.extend({ options: { components: { "volumeBar": {} } }, createElement: function(){ return this._super("div", { className: "vjs-volume-control vjs-control" }); } }); _V_.VolumeBar = _V_.Slider.extend({ options: { components: { "bar": { componentClass: "VolumeLevel" }, "handle": { componentClass: "VolumeHandle" } } }, playerEvent: "volumechange", createElement: function(){ return this._super("div", { className: "vjs-volume-bar" }); }, onMouseMove: function(event) { this.player.volume(this.calculateDistance(event)); }, getPercent: function(){ return this.player.volume(); }, stepForward: function(){ this.player.volume(this.player.volume() + 0.1); }, stepBack: function(){ this.player.volume(this.player.volume() - 0.1); } }); _V_.VolumeLevel = _V_.Component.extend({ createElement: function(){ return this._super("div", { className: "vjs-volume-level", innerHTML: '' }); } }); _V_.VolumeHandle = _V_.Component.extend({ createElement: function(){ return this._super("div", { className: "vjs-volume-handle", innerHTML: '' // tabindex: 0, // role: "slider", "aria-valuenow": 0, "aria-valuemin": 0, "aria-valuemax": 100 }); } }); _V_.MuteToggle = _V_.Button.extend({ init: function(player, options){ this._super(player, options); player.addEvent("volumechange", _V_.proxy(this, this.update)); }, createElement: function(){ return this._super("div", { className: "vjs-mute-control vjs-control", innerHTML: '
Mute
' }); }, onClick: function(event){ this.player.muted( this.player.muted() ? false : true ); }, update: function(event){ var vol = this.player.volume(), level = 3; if (vol == 0 || this.player.muted()) { level = 0; } else if (vol < 0.33) { level = 1; } else if (vol < 0.67) { level = 2; } /* TODO improve muted icon classes */ _V_.each.call(this, [0,1,2,3], function(i){ _V_.removeClass(this.el, "vjs-vol-"+i); }); _V_.addClass(this.el, "vjs-vol-"+level); } }); /* Poster Image ================================================================================ */ _V_.Poster = _V_.Button.extend({ init: function(player, options){ this._super(player, options); if (!this.player.options.poster) { this.hide(); } player.addEvent("play", _V_.proxy(this, this.hide)); }, createElement: function(){ return _V_.createElement("img", { className: "vjs-poster", src: this.player.options.poster, // Don't want poster to be tabbable. tabIndex: -1 }); }, onClick: function(){ this.player.play(); } }); /* Text Track Displays ================================================================================ */ // Create a behavior type for each text track type (subtitlesDisplay, captionsDisplay, etc.). // Then you can easily do something like. // player.addBehavior(myDiv, "subtitlesDisplay"); // And the myDiv's content will be updated with the text change. // Base class for all track displays. Should not be instantiated on its own. _V_.TextTrackDisplay = _V_.Component.extend({ init: function(player, options){ this._super(player, options); player.addEvent(this.trackType + "update", _V_.proxy(this, this.update)); }, createElement: function(){ return this._super("div", { className: "vjs-" + this.trackType }); }, update: function(){ this.el.innerHTML = this.player.textTrackValue(this.trackType); } }); _V_.SubtitlesDisplay = _V_.TextTrackDisplay.extend({ trackType: "subtitles" }); _V_.CaptionsDisplay = _V_.TextTrackDisplay.extend({ trackType: "captions" }); _V_.ChaptersDisplay = _V_.TextTrackDisplay.extend({ trackType: "chapters" }); _V_.DescriptionsDisplay = _V_.TextTrackDisplay.extend({ trackType: "descriptions" });