text: (function ($) {
  	// Media events
  	AblePlayer.prototype.onMediaUpdateTime = function (duration, elapsed) {
  
  		// duration and elapsed are passed from callback functions of Vimeo API events
  		// duration is expressed as sss.xxx
  		// elapsed is expressed as sss.xxx
  		var thisObj = this;
  
  		this.getMediaTimes(duration,elapsed).then(function(mediaTimes) {
  			if (typeof duration === 'undefined') {
  				thisObj.duration = mediaTimes['duration'];
  			}
  			if (typeof elapsed === 'undefined') {
  				thisObj.elapsed = mediaTimes['elapsed'];
  			}
  			if (thisObj.swappingSrc && (typeof thisObj.swapTime !== 'undefined')) {
  				if (thisObj.swapTime === thisObj.elapsed) {
  					// described version been swapped and media has scrubbed to time of previous version
  					if (thisObj.playing) {
  						// resume playback
  						thisObj.playMedia();
  						// reset vars
  						thisObj.swappingSrc = false;
  						thisObj.swapTime = null;
  					}
  				}
  			}
  			else if (thisObj.startedPlaying) {
  				// do all the usual time-sync stuff during playback
  				if (thisObj.prefHighlight === 1) {
  					thisObj.highlightTranscript(thisObj.elapsed);
  				}
  				thisObj.updateCaption(thisObj.elapsed);
  				thisObj.showDescription(thisObj.elapsed);
  				thisObj.updateChapter(thisObj.elapsed);
  				thisObj.updateMeta(thisObj.elapsed);
  				thisObj.refreshControls('timeline', thisObj.duration, thisObj.elapsed);
  			}
  		});
  	};
  
  	AblePlayer.prototype.onMediaPause = function () {
  
  		if (this.controlsHidden) {
  			this.fadeControls('in');
  			this.controlsHidden = false;
  		}
  		if (this.hideControlsTimeoutStatus === 'active') {
  			window.clearTimeout(this.hideControlsTimeout);
  			this.hideControlsTimeoutStatus = 'clear';
  
  		}
  		this.refreshControls('playpause');
  	};
  
  	AblePlayer.prototype.onMediaComplete = function () {
  
  		// if there's a playlist, advance to next item and start playing
  		if (this.hasPlaylist && !this.cueingPlaylistItem) {
  			if (this.playlistIndex === (this.$playlist.length - 1)) {
  				// this is the last track in the playlist
  				if (this.loop) {
  					this.playlistIndex = 0;
  					this.cueingPlaylistItem = true; // stopgap to prevent multiple firings
  					this.cuePlaylistItem(0);
  				}
  			}
  			else {
  				// this is not the last track. Play the next one.
  				this.playlistIndex++;
  				this.cueingPlaylistItem = true; // stopgap to prevent multiple firings
  				this.cuePlaylistItem(this.playlistIndex)
  			}
  		}
  		this.refreshControls('init');
  	};
  
  	AblePlayer.prototype.onMediaNewSourceLoad = function () {
  
  		if (this.cueingPlaylistItem) {
  			// this variable was set in order to address bugs caused by multiple firings of media 'end' event
  			// safe to reset now
  			this.cueingPlaylistItem = false;
  		}
  		if (this.swappingSrc === true) {
  			// new source file has just been loaded
  			if (this.swapTime > 0) {
  				// this.swappingSrc will be set to false after seek is complete
  				// see onMediaUpdateTime()
  				this.seekTo(this.swapTime);
  			}
  			else {
  				if (this.playing) {
  					// should be able to resume playback
  					this.playMedia();
  				}
  				this.swappingSrc = false; // swapping is finished
  				this.refreshControls('init');
  			}
  		}
  	};
  
  	// End Media events
  
  	AblePlayer.prototype.onWindowResize = function () {
  
  		if (this.fullscreen) { // replace isFullscreen() with a Boolean. see function for explanation
  
  			var newWidth, newHeight;
  
  			newWidth = $(window).width();
  
  			// haven't isolated why, but some browsers return an innerHeight that's 20px too tall in fullscreen mode
  			// Test results:
  			// Browsers that require a 20px adjustment: Firefox, IE11 (Trident), Edge
  			if (this.isUserAgent('Firefox') || this.isUserAgent('Trident') || this.isUserAgent('Edge')) {
  				newHeight = window.innerHeight - this.$playerDiv.outerHeight() - 20;
  			}
  			else if (window.outerHeight >= window.innerHeight) {
  				// Browsers that do NOT require adjustment: Chrome, Safari, Opera, MSIE 10
  				newHeight = window.innerHeight - this.$playerDiv.outerHeight();
  			}
  			else {
  				// Observed in Safari 9.0.1 on Mac OS X: outerHeight is actually less than innerHeight
  				// Maybe a bug, or maybe window.outerHeight is already adjusted for controller height(?)
  				// No longer observed in Safari 9.0.2
  				newHeight = window.outerHeight;
  			}
  			if (!this.$descDiv.is(':hidden')) {
  				newHeight -= this.$descDiv.height();
  			}
  			this.positionCaptions('overlay');
  		}
  		else { // not fullscreen
  			if (this.restoringAfterFullScreen) {
  				newWidth = this.preFullScreenWidth;
  				newHeight = this.preFullScreenHeight;
  			}
  			else {
  				// not restoring after full screen
  				newWidth = this.$ableWrapper.width();
  				if (typeof this.aspectRatio !== 'undefined') {
  					newHeight = Math.round(newWidth / this.aspectRatio);
  				}
  				else {
  					// not likely, since this.aspectRatio is defined during intialization
  					// however, this is a fallback scenario just in case
  					newHeight = this.$ableWrapper.height();
  				}
  				this.positionCaptions(); // reset with this.prefCaptionsPosition
  			}
  		}
  		this.resizePlayer(newWidth, newHeight);
  	};
  
  	AblePlayer.prototype.addSeekbarListeners = function () {
  
  		var thisObj = this;
  
  		// Handle seek bar events.
  		this.seekBar.bodyDiv.on('startTracking', function (e) {
  			thisObj.pausedBeforeTracking = thisObj.paused;
  			thisObj.pauseMedia();
  		}).on('tracking', function (e, position) {
  			// Scrub transcript, captions, and metadata.
  			thisObj.highlightTranscript(position);
  			thisObj.updateCaption(position);
  			thisObj.showDescription(position);
  			thisObj.updateChapter(thisObj.convertChapterTimeToVideoTime(position));
  			thisObj.updateMeta(position);
  			thisObj.refreshControls('init');
  		}).on('stopTracking', function (e, position) {
  			if (thisObj.useChapterTimes) {
  				thisObj.seekTo(thisObj.convertChapterTimeToVideoTime(position));
  			}
  			else {
  				thisObj.seekTo(position);
  			}
  			if (!thisObj.pausedBeforeTracking) {
  				setTimeout(function () {
  					thisObj.playMedia();
  				}, 200);
  			}
  		});
  	};
  
  	AblePlayer.prototype.onClickPlayerButton = function (el) {
  
  		// TODO: This is super-fragile since we need to know the length of the class name to split off; update this to other way of dispatching?
  		var whichButton = $(el).attr('class').split(' ')[0].substr(20);
  		if (whichButton === 'play') {
  			this.handlePlay();
  		}
  		else if (whichButton === 'restart') {
  			this.seekTrigger = 'restart';
  			this.handleRestart();
  		}
  		else if (whichButton === 'rewind') {
  			this.seekTrigger = 'rewind';
  			this.handleRewind();
  		}
  		else if (whichButton === 'forward') {
  			this.seekTrigger = 'forward';
  			this.handleFastForward();
  		}
  		else if (whichButton === 'mute') {
  			this.handleMute();
  		}
  		else if (whichButton === 'volume') {
  			this.handleVolume();
  		}
  		else if (whichButton === 'faster') {
  			this.handleRateIncrease();
  		}
  		else if (whichButton === 'slower') {
  			this.handleRateDecrease();
  		}
  		else if (whichButton === 'captions') {
  			this.handleCaptionToggle();
  		}
  		else if (whichButton === 'chapters') {
  			this.handleChapters();
  		}
  		else if (whichButton === 'descriptions') {
  			this.handleDescriptionToggle();
  		}
  		else if (whichButton === 'sign') {
  			this.handleSignToggle();
  		}
  		else if (whichButton === 'preferences') {
  			this.handlePrefsClick();
  		}
  		else if (whichButton === 'help') {
  			this.handleHelpClick();
  		}
  		else if (whichButton === 'transcript') {
  			this.handleTranscriptToggle();
  		}
  		else if (whichButton === 'fullscreen') {
  			this.clickedFullscreenButton = true;
  			this.handleFullscreenToggle();
  		}
  	};
  
  	AblePlayer.prototype.okToHandleKeyPress = function () {
  
  		// returns true unless user's focus is on a UI element
  		// that is likely to need supported keystrokes, including space
  
  		var activeElement = AblePlayer.getActiveDOMElement();
  
  		if ($(activeElement).prop('tagName') === 'INPUT') {
  			return false;
  		}
  		else {
  			return true;
  		}
  	};
  
  	AblePlayer.prototype.onPlayerKeyPress = function (e) {
  
  		// handle keystrokes (using DHTML Style Guide recommended key combinations)
  		// https://web.archive.org/web/20130127004544/http://dev.aol.com/dhtml_style_guide/#mediaplayer
  		// Modifier keys Alt + Ctrl are on by default, but can be changed within Preferences
  		// NOTE #1: Style guide only supports Play/Pause, Stop, Mute, Captions, & Volume Up & Down
  		// The rest are reasonable best choices
  		// NOTE #2: If there are multiple players on a single page, keystroke handlers
  		// are only bound to the FIRST player
  		// NOTE #3: The DHTML Style Guide is now the W3C WAI-ARIA Authoring Guide and has undergone many revisions
  		// including removal of the "media player" design pattern. There's an issue about that:
  		// https://github.com/w3c/aria-practices/issues/27
  
  		if (!this.okToHandleKeyPress()) {
  			return false;
  		}
  		// Convert to lower case.
  		var which = e.which;
  
  		if (which >= 65 && which <= 90) {
  			which += 32;
  		}
  
  		// Only use keypress to control player if focus is NOT on a form field or contenteditable element
  		if (!(
  			$(':focus').is('[contenteditable]') ||
  			$(':focus').is('input') ||
  			$(':focus').is('textarea') ||
  			$(':focus').is('select') ||
  			e.target.hasAttribute('contenteditable') ||
  			e.target.tagName === 'INPUT' ||
  			e.target.tagName === 'TEXTAREA' ||
  			e.target.tagName === 'SELECT'
  		)){
  			if (which === 27) { // escape
  				this.closePopups();
  			}
  			else if (which === 32) { // spacebar = play/pause
  				if (this.$ableWrapper.find('.able-controller button:focus').length === 0) {
  					// only toggle play if a button does not have focus
  					// if a button has focus, space should activate that button
  					this.handlePlay();
  				}
  			}
  			else if (which === 112) { // p = play/pause
  				if (this.usingModifierKeys(e)) {
  					this.handlePlay();
  				}
  			}
  			else if (which === 115) { // s = stop (now restart)
  				if (this.usingModifierKeys(e)) {
  					this.handleRestart();
  				}
  			}
  			else if (which === 109) { // m = mute
  				if (this.usingModifierKeys(e)) {
  					this.handleMute();
  				}
  			}
  			else if (which === 118) { // v = volume
  				if (this.usingModifierKeys(e)) {
  					this.handleVolume();
  				}
  			}
  			else if (which >= 49 && which <= 57) { // set volume 1-9
  				if (this.usingModifierKeys(e)) {
  					this.handleVolume(which);
  				}
  			}
  			else if (which === 99) { // c = caption toggle
  				if (this.usingModifierKeys(e)) {
  					this.handleCaptionToggle();
  				}
  			}
  			else if (which === 100) { // d = description
  				if (this.usingModifierKeys(e)) {
  					this.handleDescriptionToggle();
  				}
  			}
  			else if (which === 102) { // f = forward
  				if (this.usingModifierKeys(e)) {
  					this.handleFastForward();
  				}
  			}
  			else if (which === 114) { // r = rewind
  				if (this.usingModifierKeys(e)) {
  					this.handleRewind();
  				}
  			}
  			else if (which === 101) { // e = preferences
  				if (this.usingModifierKeys(e)) {
  					this.handlePrefsClick();
  				}
  			}
  			else if (which === 13) { // Enter
  				var thisElement = $(document.activeElement);
  				if (thisElement.prop('tagName') === 'SPAN') {
  					// register a click on this SPAN
  					// if it's a transcript span the transcript span click handler will take over
  					thisElement.click();
  				}
  				else if (thisElement.prop('tagName') === 'LI') {
  					thisElement.click();
  				}
  			}
  		}
  	};
  
  	AblePlayer.prototype.addHtml5MediaListeners = function () {
  
  		var thisObj = this;
  
  		// NOTE: iOS and some browsers do not support autoplay
  		// and no events are triggered until media begins to play
  		// Able Player gets around this by automatically loading media in some circumstances
  		// (see initialize.js > initPlayer() for details)
  		this.$media
  			.on('emptied',function() {
  				// do something
  			})
  			.on('loadedmetadata',function() {
  				thisObj.onMediaNewSourceLoad();
  			})
  			.on('canplay',function() {
  				// previously handled seeking to startTime here
  				// but it's probably safer to wait for canplaythrough
  				// so we know player can seek ahead to anything
  			})
  			.on('canplaythrough',function() {
  				if (thisObj.userClickedPlaylist) {
  					if (!thisObj.startedPlaying) {
  							// start playing; no further user action is required
  						thisObj.playMedia();
  				 		}
  					thisObj.userClickedPlaylist = false; // reset
  				}
  				if (thisObj.seekTrigger == 'restart' || thisObj.seekTrigger == 'chapter' || thisObj.seekTrigger == 'transcript') {
  					// by clicking on any of these elements, user is likely intending to play
  					// Not included: elements where user might click multiple times in succession
  					// (i.e., 'rewind', 'forward', or seekbar); for these, video remains paused until user initiates play
  					thisObj.playMedia();
  				}
  				else if (!thisObj.startedPlaying) {
  					if (thisObj.startTime > 0) {
  						if (thisObj.seeking) {
  							// a seek has already been initiated
  							// since canplaythrough has been triggered, the seek is complete
  							thisObj.seeking = false;
  							if (thisObj.autoplay || thisObj.okToPlay) {
  								thisObj.playMedia();
  							}
  						}
  						else {
  							// haven't started seeking yet
  							thisObj.seekTo(thisObj.startTime);
  						}
  					}
  					else if (thisObj.defaultChapter && typeof thisObj.selectedChapters !== 'undefined') {
  						thisObj.seekToChapter(thisObj.defaultChapter);
  					}
  					else {
  						// there is no startTime, therefore no seeking required
  						if (thisObj.autoplay || thisObj.okToPlay) {
  							thisObj.playMedia();
  						}
  					}
  				}
  				else if (thisObj.hasPlaylist) {
  					if ((thisObj.playlistIndex !== thisObj.$playlist.length) || thisObj.loop) {
  						// this is not the last track in the playlist (OR playlist is looping so it doesn't matter)
  						thisObj.playMedia();
  					}
  				}
  				else {
  					// already started playing
  					// we're here because a new media source has been loaded and is ready to resume playback
  					thisObj.getPlayerState().then(function(currentState) {
  						if (thisObj.swappingSrc && currentState === 'stopped') {
  							// Safari is the only browser that returns value of 'stopped' (observed in 12.0.1 on MacOS)
  							// This prevents 'timeupdate' events from triggering, which prevents the new media src
  							// from resuming playback at swapTime
  							// This is a hack to jump start Safari
  							thisObj.startedPlaying = false;
  							if (thisObj.swapTime > 0) {
  								thisObj.seekTo(thisObj.swapTime);
  							}
  							else {
  								thisObj.playMedia();
  							}
  						}
  					});
  				}
  			})
  			.on('play',function() {
  				// both 'play' and 'playing' seem to be fired in all browsers (including IE11)
  				// therefore, doing nothing here & doing everything when 'playing' is triggered
  				 thisObj.refreshControls('playpause');
  			})
  			.on('playing',function() {
  				thisObj.playing = true;
  				thisObj.paused = false;
  				thisObj.refreshControls('playpause');
  			})
  			.on('ended',function() {
  				thisObj.playing = false;
  				thisObj.paused = true;
  				thisObj.onMediaComplete();
  			})
  			.on('progress', function() {
  				thisObj.refreshControls('timeline');
  			})
  			.on('waiting',function() {
  				 // do something
  				 // previously called refreshControls() here but this event probably doesn't warrant a refresh
  			})
  			.on('durationchange',function() {
  				// Display new duration.
  				thisObj.refreshControls('timeline');
  			})
  			.on('timeupdate',function() {
  				thisObj.onMediaUpdateTime(); // includes a call to refreshControls()
  			})
  			.on('pause',function() {
  				if (!thisObj.clickedPlay) {
  					// 'pause' was triggered automatically, not initiated by user
  					// this happens in some browsers (not Chrome, as of 70.x)
  					// when swapping source (e.g., between tracks in a playlist, or swapping description)
  					if (thisObj.hasPlaylist || thisObj.swappingSrc) {
  						// do NOT set playing to false.
  						// doing so prevents continual playback after new track is loaded
  					}
  					else {
  						thisObj.playing = false;
  						thisObj.paused = true;
  					}
  				}
  				else {
  					thisObj.playing = false;
  					thisObj.paused = true;
  				}
  				thisObj.clickedPlay = false; // done with this variable
  				thisObj.onMediaPause(); // includes a call to refreshControls()
  			})
  			.on('ratechange',function() {
  				// do something
  			})
  			.on('volumechange',function() {
  				thisObj.volume = thisObj.getVolume();
  				if (thisObj.debug) {
  					console.log('media volume change to ' + thisObj.volume + ' (' + thisObj.volumeButton + ')');
  				}
  			})
  			.on('error',function() {
  				if (thisObj.debug) {
  					switch (thisObj.media.error.code) {
  						case 1:
  							console.log('HTML5 Media Error: MEDIA_ERR_ABORTED');
  							break;
  						case 2:
  							console.log('HTML5 Media Error: MEDIA_ERR_NETWORK ');
  							break;
  						case 3:
  							console.log('HTML5 Media Error: MEDIA_ERR_DECODE ');
  							break;
  						case 4:
  							console.log('HTML5 Media Error: MEDIA_ERR_SRC_NOT_SUPPORTED ');
  							break;
  					}
  				}
  			});
  	};
  
  	AblePlayer.prototype.addVimeoListeners = function () {
  
  // The following content is orphaned. It was in 'canplaythrough' but there's no equivalent event in Vimeo.
  // Maybe it should go under 'loaded' or 'progress' ???
  /*
  				if (thisObj.userClickedPlaylist) {
  					if (!thisObj.startedPlaying) {
  							// start playing; no further user action is required
  						thisObj.playMedia();
  				 		}
  					thisObj.userClickedPlaylist = false; // reset
  				}
  				if (thisObj.seekTrigger == 'restart' || thisObj.seekTrigger == 'chapter' || thisObj.seekTrigger == 'transcript') {
  					// by clicking on any of these elements, user is likely intending to play
  					// Not included: elements where user might click multiple times in succession
  					// (i.e., 'rewind', 'forward', or seekbar); for these, video remains paused until user initiates play
  					thisObj.playMedia();
  				}
  				else if (!thisObj.startedPlaying) {
  					if (thisObj.startTime > 0) {
  						if (thisObj.seeking) {
  							// a seek has already been initiated
  							// since canplaythrough has been triggered, the seek is complete
  							thisObj.seeking = false;
  							if (thisObj.autoplay || thisObj.okToPlay) {
  								thisObj.playMedia();
  							}
  						}
  						else {
  							// haven't started seeking yet
  							thisObj.seekTo(thisObj.startTime);
  						}
  					}
  					else if (thisObj.defaultChapter && typeof thisObj.selectedChapters !== 'undefined') {
  						thisObj.seekToChapter(thisObj.defaultChapter);
  					}
  					else {
  						// there is no startTime, therefore no seeking required
  						if (thisObj.autoplay || thisObj.okToPlay) {
  							thisObj.playMedia();
  						}
  					}
  				}
  				else if (thisObj.hasPlaylist) {
  					if ((thisObj.playlistIndex !== thisObj.$playlist.length) || thisObj.loop) {
  						// this is not the last track in the playlist (OR playlist is looping so it doesn't matter)
  						thisObj.playMedia();
  					}
  				}
  				else {
  					// already started playing
  					// we're here because a new media source has been loaded and is ready to resume playback
  					thisObj.getPlayerState().then(function(currentState) {
  						if (thisObj.swappingSrc && currentState === 'stopped') {
  							// Safari is the only browser that returns value of 'stopped' (observed in 12.0.1 on MacOS)
  							// This prevents 'timeupdate' events from triggering, which prevents the new media src
  							// from resuming playback at swapTime
  							// This is a hack to jump start Safari
  							thisObj.startedPlaying = false;
  							if (thisObj.swapTime > 0) {
  								thisObj.seekTo(thisObj.swapTime);
  							}
  							else {
  								thisObj.playMedia();
  							}
  						}
  					});
  				}
  
  */
  
  		var thisObj = this;
  
  		// Vimeo doesn't seem to support chaining of on() functions
  		// so each event listener must be attached separately
  		this.vimeoPlayer.on('loaded', function(vimeoId) {
  			 // Triggered when a new video is loaded in the player
  			thisObj.onMediaNewSourceLoad();
  		 });
  		this.vimeoPlayer.on('play', function(data) {
  			// Triggered when the video plays
  			thisObj.playing = true;
  			thisObj.startedPlaying = true;
  			thisObj.paused = false;
  			thisObj.refreshControls('playpause');
  		});
  		this.vimeoPlayer.on('ended', function(data) {
  			// Triggered any time the video playback reaches the end.
  			// Note: when loop is turned on, the ended event will not fire.
  			thisObj.playing = false;
  			thisObj.paused = true;
  			thisObj.onMediaComplete();
  		});
  		this.vimeoPlayer.on('bufferstart', function() {
  			// Triggered when buffering starts in the player.
  			// This is also triggered during preload and while seeking.
  			// There is no associated data with this event.
  		});
  		this.vimeoPlayer.on('bufferend', function() {
  			// Triggered when buffering ends in the player.
  			// This is also triggered at the end of preload and seeking.
  			// There is no associated data with this event.
  		});
  		this.vimeoPlayer.on('progress', function(data) {
  			// Triggered as the video is loaded.
  			 // Reports back the amount of the video that has been buffered (NOT the amount played)
  			 // Data has keys duration, percent, and seconds
  	 	});
  		this.vimeoPlayer.on('seeking', function(data) {
  		 	// Triggered when the player starts seeking to a specific time.
  			 // A timeupdate event will also be fired at the same time.
  	 	});
  		this.vimeoPlayer.on('seeked', function(data) {
  			// Triggered when the player seeks to a specific time.
  			// A timeupdate event will also be fired at the same time.
  		});
  		this.vimeoPlayer.on('timeupdate',function(data) {
  			// Triggered as the currentTime of the video updates.
  			 // It generally fires every 250ms, but it may vary depending on the browser.
  			thisObj.onMediaUpdateTime(data['duration'], data['seconds']);
  		});
  		this.vimeoPlayer.on('pause',function(data) {
  			// Triggered when the video pauses
  			if (!thisObj.clickedPlay) {
  					// 'pause' was triggered automatically, not initiated by user
  				// this happens in some browsers (not Chrome, as of 70.x)
  				// when swapping source (e.g., between tracks in a playlist, or swapping description)
  				if (thisObj.hasPlaylist || thisObj.swappingSrc) {
  						// do NOT set playing to false.
  					// doing so prevents continual playback after new track is loaded
  				}
  				else {
  					thisObj.playing = false;
  					thisObj.paused = true;
  				}
  			}
  			else {
  				thisObj.playing = false;
  				thisObj.paused = true;
  			}
  			thisObj.clickedPlay = false; // done with this variable
  			thisObj.onMediaPause();
  			thisObj.refreshControls('playpause');
  		});
  		this.vimeoPlayer.on('playbackratechange',function(data) {
  			// Triggered when the playback rate of the video in the player changes.
  			// The ability to change rate can be disabled by the creator
  			// and the event will not fire for those videos.
  			// data contains one key: 'playbackRate'
  			thisObj.vimeoPlaybackRate = data['playbackRate'];
  		});
  		this.vimeoPlayer.on('texttrackchange', function(data) {
  			// Triggered when the active text track (captions/subtitles) changes.
  			// The values will be null if text tracks are turned off.
  			// data contains three keys: kind, label, language
  		});
  		this.vimeoPlayer.on('volumechange',function(data) {
  			// Triggered when the volume in the player changes.
  			// Some devices do not support setting the volume of the video
  			// independently from the system volume,
  			// so this event will never fire on those devices.
  			thisObj.volume = data['volume'] * 10;
  		});
  		this.vimeoPlayer.on('error',function(data) {
  			// do something with the available data
  			// data contains three keys: message, method, name
  			// message: A user-friendly error message
  			// method: The Vimeo API method call that triggered the error
  			// name: Name of the error (not necesssarily user-friendly)
  		});
  	};
  
  	AblePlayer.prototype.addEventListeners = function () {
  
  		var thisObj, whichButton, thisElement;
  
  		// Save the current object context in thisObj for use with inner functions.
  		thisObj = this;
  
  		// Appropriately resize media player for full screen.
  		$(window).resize(function () {
  			thisObj.onWindowResize();
  		});
  
  		// Refresh player if it changes from hidden to visible
  		// There is no event triggered by a change in visibility
  		// but MutationObserver works in most browsers (but NOT in IE 10 or earlier)
  		// http://caniuse.com/#feat=mutationobserver
  		if (window.MutationObserver) {
  			var target = this.$ableDiv[0];
  			var observer = new MutationObserver(function(mutations) {
  				mutations.forEach(function(mutation) {
  					if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
  						// the player's style attribute has changed. Check to see if it's visible
  						if (thisObj.$ableDiv.is(':visible')) {
  							thisObj.refreshControls('init');
  						}
  					}
  				});
  			});
  			var config = { attributes: true, childList: true, characterData: true };
  			observer.observe(target, config);
  		}
  		else {
  			// browser doesn't support MutationObserver
  			// TODO: Figure out an alternative solution for this rare use case in older browsers
  			// See example in buildplayer.js > useSvg()
  		}
  		if (typeof this.seekBar !== 'undefined') {
  			this.addSeekbarListeners();
  		}
  		else {
  			// wait a bit and try again
  			// TODO: Should set this up to keep trying repeatedly.
  			// Seekbar listeners are critical.
  			setTimeout(function() {
  				if (typeof thisObj.seekBar !== 'undefined') {
  					thisObj.addSeekbarListeners();
  				}
  			},2000);
  		}
  
  		// handle clicks on player buttons
  		this.$controllerDiv.find('button').on('click',function(e){
  			e.stopPropagation();
  			thisObj.onClickPlayerButton(this);
  		});
  
  		// handle clicks (left only) anywhere on the page. If any popups are open, close them.
  		$(document).on('click',function(e) {
  
  			if (e.button !== 0) { // not a left click
  				return false;
  			}
  			if ($('.able-popup:visible').length || $('.able-volume-popup:visible')) {
  				// at least one popup is visible
  				thisObj.closePopups();
  			}
  		});
  
  		// handle mouse movement over player; make controls visible again if hidden
  		this.$ableDiv.on('mousemove',function() {
  			if (thisObj.controlsHidden) {
  				thisObj.fadeControls('in');
  				thisObj.controlsHidden = false;
  				// if there's already an active timeout, clear it and start timer again
  				if (thisObj.hideControlsTimeoutStatus === 'active') {
  					window.clearTimeout(thisObj.hideControlsTimeout);
  					thisObj.hideControlsTimeoutStatus = 'clear';
  				}
  				if (thisObj.hideControls) {
  					// after showing controls, hide them again after a brief timeout
  					thisObj.invokeHideControlsTimeout();
  				}
  			}
  			else {
  				// if there's already an active timeout, clear it and start timer again
  				if (thisObj.hideControlsTimeoutStatus === 'active') {
  					window.clearTimeout(thisObj.hideControlsTimeout);
  					thisObj.hideControlsTimeoutStatus = 'clear';
  					if (thisObj.hideControls) {
  						thisObj.invokeHideControlsTimeout();
  					}
  				}
  			}
  		});
  
  		// if user presses a key from anywhere on the page, show player controls
  		$(document).keydown(function() {
  			if (thisObj.controlsHidden) {
  				thisObj.fadeControls('in');
  				thisObj.controlsHidden = false;
  				if (thisObj.hideControlsTimeoutStatus === 'active') {
  					window.clearTimeout(thisObj.hideControlsTimeout);
  					thisObj.hideControlsTimeoutStatus = 'clear';
  				}
  				if (thisObj.hideControls) {
  					// after showing controls, hide them again after a brief timeout
  					thisObj.invokeHideControlsTimeout();
  				}
  			}
  			else {
  				// controls are visible
  				// if there's already an active timeout, clear it and start timer again
  				if (thisObj.hideControlsTimeoutStatus === 'active') {
  					window.clearTimeout(thisObj.hideControlsTimeout);
  					thisObj.hideControlsTimeoutStatus = 'clear';
  
  					if (thisObj.hideControls) {
  						thisObj.invokeHideControlsTimeout();
  					}
  				}
  			}
  		});
  
  		// handle local keydown events if this isn't the only player on the page;
  		// otherwise these are dispatched by global handler (see ableplayer-base,js)
  		this.$ableDiv.keydown(function (e) {
  			if (AblePlayer.nextIndex > 1) {
  				thisObj.onPlayerKeyPress(e);
  			}
  		});
  
  		// transcript is not a child of this.$ableDiv
  		// therefore, must be added separately
  		if (this.$transcriptArea) {
  			this.$transcriptArea.keydown(function (e) {
  				if (AblePlayer.nextIndex > 1) {
  					thisObj.onPlayerKeyPress(e);
  				}
  			});
  		}
  
  		// handle clicks on playlist items
  		if (this.$playlist) {
  			this.$playlist.click(function(e) {
  				if (!thisObj.userClickedPlaylist) {
  					// stopgap in case multiple clicks are fired on the same playlist item
  					thisObj.userClickedPlaylist = true; // will be set to false after new src is loaded & canplaythrough is triggered
  					thisObj.playlistIndex = $(this).index();
  					thisObj.cuePlaylistItem(thisObj.playlistIndex);
  				}
  			});
  		}
  
  		// Also play/pause when clicking on the media.
  		this.$media.click(function () {
  			thisObj.handlePlay();
  		});
  
  		// add listeners for media events
  		if (this.player === 'html5') {
  			this.addHtml5MediaListeners();
  		}
  		else if (this.player === 'vimeo') {
  			 this.addVimeoListeners();
  		}
  		else if (this.player === 'youtube') {
  			// Youtube doesn't give us time update events, so we just periodically generate them ourselves
  			setInterval(function () {
  				thisObj.onMediaUpdateTime();
  			}, 300);
  		}
  	};
  })(jQuery);
  