(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);