(function ($) {

AblePlayer.prototype.injectPlayerCode = function() {

        // create and inject surrounding HTML structure
        // If IOS:
        //      If video:
        //       IOS does not support any of the player's functionality
        //       - everything plays in its own player
        //       Therefore, AblePlayer is not loaded & all functionality is disabled
        //       (this all determined. If this is IOS && video, this function is never called)
        //      If audio:
        //       HTML cannot be injected as a *parent* of the <audio> element
        //       It is therefore injected *after* the <audio> element
        //       This is only a problem in IOS 6 and earlier,
        //       & is a known bug, fixed in IOS 7

        var thisObj, vidcapContainer, prefsGroups, i;
        thisObj = this;

        // create three wrappers and wrap them around the media element. From inner to outer:
        // $mediaContainer - contains the original media element
        // $ableDiv - contains the media player and all its objects (e.g., captions, controls, descriptions)
        // $ableWrapper - contains additional widgets (e.g., transcript window, sign window)
        this.$mediaContainer = this.$media.wrap('<div class="able-media-container"></div>').parent();
        this.$ableDiv = this.$mediaContainer.wrap('<div class="able"></div>').parent();
        this.$ableWrapper = this.$ableDiv.wrap('<div class="able-wrapper"></div>').parent();

        // NOTE: Excluding the following from youtube was resulting in a player
        // that exceeds the width of the YouTube video
        // Unclear why it was originally excluded; commented out in 3.1.20
        // if (this.player !== 'youtube') {
        this.$ableWrapper.css({
                'max-width': this.playerMaxWidth + 'px'
        });

        this.injectOffscreenHeading();

        if (this.mediaType === 'video') {
                // youtube adds its own big play button
                // don't show ours *unless* video has a poster attribute
                // (which obstructs the YouTube poster & big play button)
                if (this.iconType != 'image' && (this.player !== 'youtube' || this.hasPoster)) {
                        this.injectBigPlayButton();
                }

                // add container that captions or description will be appended to
                // Note: new Jquery object must be assigned _after_ wrap, hence the temp vidcapContainer variable
                vidcapContainer = $('<div>',{
                        'class' : 'able-vidcap-container'
                });
                this.$vidcapContainer = this.$mediaContainer.wrap(vidcapContainer).parent();
        }
        this.injectPlayerControlArea();
        this.injectTextDescriptionArea();
        this.injectAlert();
        this.injectPlaylist();
};

AblePlayer.prototype.injectOffscreenHeading = function () {
        // Inject an offscreen heading to the media container.
        // If heading hasn't already been manually defined via data-heading-level,
        // automatically assign a level that is one level deeper than the closest parent heading
        // as determined by getNextHeadingLevel()
        var headingType;
        if (this.playerHeadingLevel == '0') {
                // do NOT inject a heading (at author's request)
        }
        else {
                if (typeof this.playerHeadingLevel === 'undefined') {
                        this.playerHeadingLevel = this.getNextHeadingLevel(this.$ableDiv); // returns in integer 1-6
                }
                headingType = 'h' + this.playerHeadingLevel.toString();
                this.$headingDiv = $('<' + headingType + '>');
                this.$ableDiv.prepend(this.$headingDiv);
                this.$headingDiv.addClass('able-offscreen');
                this.$headingDiv.text(this.tt.playerHeading);
        }
};

AblePlayer.prototype.injectBigPlayButton = function () {

        this.$bigPlayButton = $('<button>', {
                'class': 'able-big-play-button icon-play',
                'aria-hidden': true,
                'tabindex': -1
        });

        var thisObj = this;
        this.$bigPlayButton.click(function () {
                thisObj.handlePlay();
        });

        this.$mediaContainer.append(this.$bigPlayButton);
};

AblePlayer.prototype.injectPlayerControlArea = function () {

        this.$playerDiv = $('<div>', {
                'class' : 'able-player',
                'role' : 'region',
                'aria-label' : this.mediaType + ' player'
        });
        this.$playerDiv.addClass('able-'+this.mediaType);

        // The default skin depends a bit on a Now Playing div
        // so go ahead and add one
        // However, it's only populated if this.showNowPlaying = true
        this.$nowPlayingDiv = $('<div>',{
                'class' : 'able-now-playing',
                'aria-live' : 'assertive',
                'aria-atomic': 'true'
        });

        this.$controllerDiv = $('<div>',{
                'class' : 'able-controller'
        });
        this.$controllerDiv.addClass('able-' + this.iconColor + '-controls');

        this.$statusBarDiv = $('<div>',{
                'class' : 'able-status-bar'
        });
        this.$timer = $('<span>',{
                'class' : 'able-timer'
        });
        this.$elapsedTimeContainer = $('<span>',{
                'class': 'able-elapsedTime',
                text: '0:00'
        });
        this.$durationContainer = $('<span>',{
                'class': 'able-duration'
        });
        this.$timer.append(this.$elapsedTimeContainer).append(this.$durationContainer);

        this.$speed = $('<span>',{
                'class' : 'able-speed',
                'aria-live' : 'assertive'
        }).text(this.tt.speed + ': 1x');

        this.$status = $('<span>',{
                'class' : 'able-status',
                'aria-live' : 'polite'
        });

        // Put everything together.
        this.$statusBarDiv.append(this.$timer, this.$speed, this.$status);
        this.$playerDiv.append(this.$nowPlayingDiv, this.$controllerDiv, this.$statusBarDiv);
        this.$ableDiv.append(this.$playerDiv);
};

AblePlayer.prototype.injectTextDescriptionArea = function () {

        // create a div for exposing description
        // description will be exposed via role="alert" & announced by screen readers
        this.$descDiv = $('<div>',{
                'class': 'able-descriptions',
                'aria-live': 'assertive',
                'aria-atomic': 'true'
        });
        // Start off with description hidden.
        // It will be exposed conditionally within description.js > initDescription()
        this.$descDiv.hide();
        this.$ableDiv.append(this.$descDiv);
};

AblePlayer.prototype.getDefaultWidth = function(which) {

        // return default width of resizable elements
        // these values are somewhat arbitrary, but seem to result in good usability
        // if users disagree, they can resize (and resposition) them
        if (which === 'transcript') {
                return 450;
        }
        else if (which === 'sign') {
                return 400;
        }
};

AblePlayer.prototype.positionDraggableWindow = function (which, width) {

        // which is either 'transcript' or 'sign'

        var cookie, cookiePos, $window, dragged, windowPos, currentWindowPos, firstTime, zIndex;

        cookie = this.getCookie();
        if (which === 'transcript') {
                $window = this.$transcriptArea;
                if (typeof cookie.transcript !== 'undefined') {
                        cookiePos = cookie.transcript;
                }
        }
        else if (which === 'sign') {
                $window = this.$signWindow;
                if (typeof cookie.transcript !== 'undefined') {
                        cookiePos = cookie.sign;
                }
        }
        if (typeof cookiePos !== 'undefined' && !($.isEmptyObject(cookiePos))) {
                // position window using stored values from cookie
                $window.css({
                        'position': cookiePos['position'],
                        'width': cookiePos['width'],
                        'z-index': cookiePos['zindex']
                });
                if (cookiePos['position'] === 'absolute') {
                        $window.css({
                                'top': cookiePos['top'],
                                'left': cookiePos['left']
                        });
                }
                // since cookie is not page-specific, z-index needs may vary across different pages
                this.updateZIndex(which);
        }
        else {
                // position window using default values
                windowPos = this.getOptimumPosition(which, width);
                if (typeof width === 'undefined') {
                        width = this.getDefaultWidth(which);
                }
                $window.css({
                        'position': windowPos[0],
                        'width': width,
                        'z-index': windowPos[3]
                });
                if (windowPos[0] === 'absolute') {
                        $window.css({
                                'top': windowPos[1] + 'px',
                                'left': windowPos[2] + 'px',
                        });
                }
        }
};

AblePlayer.prototype.getOptimumPosition = function (targetWindow, targetWidth) {

        // returns optimum position for targetWindow, as an array with the following structure:
        // 0 - CSS position ('absolute' or 'relative')
        // 1 - top
        // 2 - left
        // 3 - zindex (if not default)
        // targetWindow is either 'transcript' or 'sign'
        // if there is room to the right of the player, position element there
        // else if there is room the left of the player, position element there
        // else position element beneath player

        var gap, position, ableWidth, ableHeight, ableOffset, ableTop, ableLeft,
                 windowWidth, otherWindowWidth, zIndex;

        if (typeof targetWidth === 'undefined') {
                targetWidth = this.getDefaultWidth(targetWindow);
        }

        gap = 5; // number of pixels to preserve between Able Player objects

        position = []; // position, top, left

        ableWidth = this.$ableDiv.width();
        ableHeight = this.$ableDiv.height();
        ableOffset = this.$ableDiv.offset();
        ableTop = ableOffset.top;
        ableLeft = ableOffset.left;
        windowWidth = $(window).width();
        otherWindowWidth = 0; // width of other visiable draggable windows will be added to this

        if (targetWindow === 'transcript') {
                if (typeof this.$signWindow !== 'undefined') {
                        if (this.$signWindow.is(':visible')) {
                                otherWindowWidth = this.$signWindow.width() + gap;
                        }
                }
        }
        else if (targetWindow === 'sign') {
                if (typeof this.$transcriptArea !== 'undefined') {
                        if (this.$transcriptArea.is(':visible')) {
                                otherWindowWidth = this.$transcriptArea.width() + gap;
                        }
                }
        }
        if (targetWidth < (windowWidth - (ableLeft + ableWidth + gap + otherWindowWidth))) {
                // there's room to the left of $ableDiv
                position[0] = 'absolute';
                position[1] = 0;
                position[2] = ableWidth + otherWindowWidth + gap;
        }
        else if (targetWidth + gap < ableLeft) {
                // there's room to the right of $ableDiv
                position[0] = 'absolute';
                position[1] = 0;
                position[2] = ableLeft - targetWidth - gap;
        }
        else {
                // position element below $ableDiv
                position[0] = 'relative';
                // no need to define top, left, or z-index
        }
        return position;
};

AblePlayer.prototype.injectPoster = function ($element, context) {

        // get poster attribute from media element and append that as an img to $element
        // context is either 'youtube' or 'fallback'
        var poster, width, height;

        if (context === 'youtube') {
                if (typeof this.ytWidth !== 'undefined') {
                        width = this.ytWidth;
                        height = this.ytHeight;
                }
                else if (typeof this.playerMaxWidth !== 'undefined') {
                        width = this.playerMaxWidth;
                        height = this.playerMaxHeight;
                }
                else if (typeof this.playerWidth !== 'undefined') {
                        width = this.playerWidth;
                        height = this.playerHeight;
                }
        }
        else if (context === 'fallback') {
                width = '100%';
                height = 'auto';
        }

        if (this.hasPoster) {
                poster = this.$media.attr('poster');
                this.$posterImg = $('<img>',{
                        'class': 'able-poster',
                        'src' : poster,
                        'alt' : "",
                        'role': "presentation",
                        'width': width,
                        'height': height
                });
                $element.append(this.$posterImg);
        }
};

AblePlayer.prototype.injectAlert = function () {

        // inject two alerts, one visible for all users and one for screen reader users only

        var top;

        this.$alertBox = $('<div role="alert"></div>');
        this.$alertBox.addClass('able-alert');
        this.$alertBox.hide();
        this.$alertBox.appendTo(this.$ableDiv);
        if (this.mediaType == 'audio') {
                top = '-10';
        }
        else {
                // position just below the vertical center of the mediaContainer
                // hopefully above captions, but not too far from the controller bar
                top = Math.round(this.$mediaContainer.height() / 3) * 2;
        }
        this.$alertBox.css({
                top: top + 'px'
        });

        this.$srAlertBox = $('<div role="alert"></div>');
        this.$srAlertBox.addClass('able-screenreader-alert');
        this.$srAlertBox.appendTo(this.$ableDiv);
};

AblePlayer.prototype.injectPlaylist = function () {

        if (this.playlistEmbed === true) {
                // move playlist into player, immediately before statusBarDiv
                var playlistClone = this.$playlistDom.clone();
                playlistClone.insertBefore(this.$statusBarDiv);
                // Update to the new playlist copy.
                this.$playlist = playlistClone.find('li');
        }
};

AblePlayer.prototype.createPopup = function (which, tracks) {

        // Create popup menu and append to player
        // 'which' parameter is either 'captions', 'chapters', 'prefs', 'transcript-window' or 'sign-window'
        // 'tracks', if provided, is a list of tracks to be used as menu items

        var thisObj, $menu, prefCats, i, $menuItem, prefCat, whichPref,
                hasDefault, track, windowOptions, whichPref, whichMenu,
                $thisItem, $prevItem, $nextItem;

        thisObj = this;

        $menu = $('<ul>',{
                'id': this.mediaId + '-' + which + '-menu',
                'class': 'able-popup',
                'role': 'menu'
        }).hide();

        if (which === 'captions') {
                $menu.addClass('able-popup-captions');
        }

        // Populate menu with menu items
        if (which === 'prefs') {
                prefCats = this.getPreferencesGroups();
                for (i = 0; i < prefCats.length; i++) {
                        $menuItem = $('<li></li>',{
                                'role': 'menuitem',
                                'tabindex': '-1'
                        });
                        prefCat = prefCats[i];
                        if (prefCat === 'captions') {
                                $menuItem.text(this.tt.prefMenuCaptions);
                        }
                        else if (prefCat === 'descriptions') {
                                $menuItem.text(this.tt.prefMenuDescriptions);
                        }
                        else if (prefCat === 'keyboard') {
                                $menuItem.text(this.tt.prefMenuKeyboard);
                        }
                        else if (prefCat === 'transcript') {
                                $menuItem.text(this.tt.prefMenuTranscript);
                        }
                        $menuItem.on('click',function() {
                                whichPref = $(this).text();
                                thisObj.setFullscreen(false);
                                if (whichPref === thisObj.tt.prefMenuCaptions) {
                                        thisObj.captionPrefsDialog.show();
                                }
                                else if (whichPref === thisObj.tt.prefMenuDescriptions) {
                                        thisObj.descPrefsDialog.show();
                                }
                                else if (whichPref === thisObj.tt.prefMenuKeyboard) {
                                        thisObj.keyboardPrefsDialog.show();
                                }
                                else if (whichPref === thisObj.tt.prefMenuTranscript) {
                                        thisObj.transcriptPrefsDialog.show();
                                }
                                thisObj.closePopups();
                        });
                        $menu.append($menuItem);
                }
        }
        else if (which === 'captions' || which === 'chapters') {
                hasDefault = false;
                for (i = 0; i < tracks.length; i++) {
                        track = tracks[i];
                        $menuItem = $('<li></li>',{
                                'role': 'menuitemradio',
                                'tabindex': '-1',
                                'lang': track.language
                        });
                        if (track.def) {
                                $menuItem.attr('aria-checked','true');
                                hasDefault = true;
                        }
                        else {
                                $menuItem.attr('aria-checked','false');
                        }
                        // Get a label using track data
                        if (which == 'captions') {
                                $menuItem.text(track.label);
                                $menuItem.on('click',this.getCaptionClickFunction(track));
                        }
                        else if (which == 'chapters') {
                                $menuItem.text(this.flattenCueForCaption(track) + ' - ' + this.formatSecondsAsColonTime(track.start));
                                $menuItem.on('click',this.getChapterClickFunction(track.start));
                        }
                        $menu.append($menuItem);
                }
                if (which === 'captions') {
                        // add a 'captions off' menu item
                        $menuItem = $('<li></li>',{
                                'role': 'menuitemradio',
                                'tabindex': '-1',
                        }).text(this.tt.captionsOff);
                        if (this.prefCaptions === 0) {
                                $menuItem.attr('aria-checked','true');
                                hasDefault = true;
                        }
                        $menuItem.on('click',this.getCaptionOffFunction());
                        $menu.append($menuItem);
                }
        }
        else if (which === 'transcript-window' || which === 'sign-window') {
                windowOptions = [];
                windowOptions.push({
                        'name': 'move',
                        'label': this.tt.windowMove
                });
                windowOptions.push({
                        'name': 'resize',
                        'label': this.tt.windowResize
                });
                windowOptions.push({
                        'name': 'close',
                        'label': this.tt.windowClose
                });
                for (i = 0; i < windowOptions.length; i++) {
                        $menuItem = $('<li></li>',{
                                'role': 'menuitem',
                                'tabindex': '-1',
                                'data-choice': windowOptions[i].name
                        });
                        $menuItem.text(windowOptions[i].label);
                        $menuItem.on('click mousedown',function(e) {
                                e.stopPropagation();
                                if (e.button !== 0) { // not a left click
                                        return false;
                                }
                                if (!thisObj.windowMenuClickRegistered && !thisObj.finishingDrag) {
                                        thisObj.windowMenuClickRegistered = true;
                                        thisObj.handleMenuChoice(which.substr(0, which.indexOf('-')), $(this).attr('data-choice'), e);
                                }
                        });
                        $menu.append($menuItem);
                }
        }
        // assign default item, if there isn't one already
        if (which === 'captions' && !hasDefault) {
                // check the menu item associated with the default language
                // as determined in control.js > syncTrackLanguages()
                if ($menu.find('li[lang=' + this.captionLang + ']')) {
                        // a track exists for the default language. Check that item in the menu
                        $menu.find('li[lang=' + this.captionLang + ']').attr('aria-checked','true');
                }
                else {
                        // check the last item (captions off)
                        $menu.find('li').last().attr('aria-checked','true');
                }
        }
        else if (which === 'chapters') {
                if ($menu.find('li:contains("' + this.defaultChapter + '")')) {
                        $menu.find('li:contains("' + this.defaultChapter + '")').attr('aria-checked','true').addClass('able-focus');
                }
                else {
                        $menu.find('li').first().attr('aria-checked','true').addClass('able-focus');
                }
        }
        // add keyboard handlers for navigating within popups
        $menu.on('keydown',function (e) {
                whichMenu = $(this).attr('id').split('-')[1];
                $thisItem = $(this).find('li:focus');
                if ($thisItem.is(':first-child')) {
                        // this is the first item in the menu
                        $prevItem = $(this).find('li').last(); // wrap to bottom
                        $nextItem = $thisItem.next();
                }
                else if ($thisItem.is(':last-child')) {
                        // this is the last Item
                        $prevItem = $thisItem.prev();
                        $nextItem = $(this).find('li').first(); // wrap to top
                }
                else {
                        $prevItem = $thisItem.prev();
                        $nextItem = $thisItem.next();
                }
                if (e.which === 9) { // Tab
                        if (e.shiftKey) {
                                $thisItem.removeClass('able-focus');
                                $prevItem.focus().addClass('able-focus');
                        }
                        else {
                                $thisItem.removeClass('able-focus');
                                $nextItem.focus().addClass('able-focus');
                        }
                }
                else if (e.which === 40 || e.which === 39) { // down or right arrow
                        $thisItem.removeClass('able-focus');
                        $nextItem.focus().addClass('able-focus');
                }
                else if (e.which == 38 || e.which === 37) { // up or left arrow
                        $thisItem.removeClass('able-focus');
                        $prevItem.focus().addClass('able-focus');
                }
                else if (e.which === 32 || e.which === 13) { // space or enter
                        $thisItem.click();
                }
                else if (e.which === 27) {      // Escape
                        $thisItem.removeClass('able-focus');
                        thisObj.closePopups();
                }
                e.preventDefault();
        });
        this.$controllerDiv.append($menu);
        return $menu;
};

AblePlayer.prototype.closePopups = function () {

        if (this.chaptersPopup && this.chaptersPopup.is(':visible')) {
                this.chaptersPopup.hide();
                this.$chaptersButton.attr('aria-expanded','false').focus();
        }
        if (this.captionsPopup && this.captionsPopup.is(':visible')) {
                this.captionsPopup.hide();
                this.$ccButton.attr('aria-expanded','false').focus();
        }
        if (this.prefsPopup && this.prefsPopup.is(':visible')) {
                this.prefsPopup.hide();
                // restore menu items to their original state
                this.prefsPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
                this.$prefsButton.attr('aria-expanded','false').focus();
        }
        if (this.$volumeSlider && this.$volumeSlider.is(':visible')) {
                this.$volumeSlider.hide().attr('aria-hidden','true');
                this.$volumeAlert.text(this.tt.volumeSliderClosed);
                this.$volumeButton.attr('aria-expanded','false').focus();
        }
        if (this.$transcriptPopup && this.$transcriptPopup.is(':visible')) {
                this.$transcriptPopup.hide();
                // restore menu items to their original state
                this.$transcriptPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
                this.$transcriptPopupButton.attr('aria-expanded','false').focus();
        }
        if (this.$signPopup && this.$signPopup.is(':visible')) {
                this.$signPopup.hide();
                // restore menu items to their original state
                this.$signPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
                this.$signPopupButton.attr('aria-expanded','false').focus();
        }
};

AblePlayer.prototype.setupPopups = function (which) {

        // Create and fill in the popup menu forms for various controls.
        // parameter 'which' is passed if refreshing content of an existing popup ('captions' or 'chapters')
        // If which is undefined, automatically setup 'captions', 'chapters', and 'prefs' popups
        // However, only setup 'transcript-window' and 'sign-window' popups if passed as value of which
        var popups, thisObj, hasDefault, i, j,
                        tracks, track, $trackButton, $trackLabel,
                        radioName, radioId, $menu, $menuItem,
                        prefCats, prefCat, prefLabel;

        popups = [];
        if (typeof which === 'undefined') {
                popups.push('prefs');
        }

        if (which === 'captions' || (typeof which === 'undefined')) {
                if (this.captions.length > 0) {
                        popups.push('captions');
                }
        }
        if (which === 'chapters' || (typeof which === 'undefined')) {
                if (this.chapters.length > 0 && this.useChaptersButton) {
                        popups.push('chapters');
                }
        }
        if (which === 'transcript-window' && this.transcriptType === 'popup') {
                popups.push('transcript-window');
        }
        if (which === 'sign-window' && this.hasSignLanguage) {
                popups.push('sign-window');
        }
        if (popups.length > 0) {
                thisObj = this;
                for (var i=0; i<popups.length; i++) {
                        var popup = popups[i];
                        hasDefault = false;
                        if (popup == 'prefs') {
                                this.prefsPopup = this.createPopup('prefs');
                        }
                        else if (popup == 'captions') {
                                if (typeof this.captionsPopup === 'undefined' || !this.captionsPopup) {
                                        this.captionsPopup = this.createPopup('captions',this.captions);
                                }
                        }
                        else if (popup == 'chapters') {
                                if (this.selectedChapters) {
                                        tracks = this.selectedChapters.cues;
                                }
                                else if (this.chapters.length >= 1) {
                                        tracks = this.chapters[0].cues;
                                }
                                else {
                                        tracks = [];
                                }
                                if (typeof this.chaptersPopup === 'undefined' || !this.chaptersPopup) {
                                        this.chaptersPopup = this.createPopup('chapters',tracks);
                                }
                        }
                        else if (popup == 'transcript-window') {
                                return this.createPopup('transcript-window');
                        }
                        else if (popup == 'sign-window') {
                                return this.createPopup('sign-window');
                        }
                }
        }
};

AblePlayer.prototype.provideFallback = function() {

        // provide ultimate fallback for users who are unable to play the media
        // If there is HTML content nested within the media element, display that
        // Otherwise, display standard localized error text

        var $fallbackDiv, width, mediaClone, fallback, fallbackText,
        showBrowserList, browsers, i, b, browserList;

        // Could show list of supporting browsers if 99.9% confident the error is truly an outdated browser
        // Too many sites say "You need to update your browser" when in fact I'm using a current version
        showBrowserList = false;

        $fallbackDiv = $('<div>',{
                'class' : 'able-fallback',
                'role' : 'alert',
        });
        // override default width of .able-fallback with player width, if known
        if (typeof this.playerMaxWidth !== 'undefined') {
                width = this.playerMaxWidth + 'px';
        }
        else if (this.$media.attr('width')) {
                width = parseInt(this.$media.attr('width'), 10) + 'px';
        }
        else {
                width = '100%';
        }
        $fallbackDiv.css('max-width',width);

        // use fallback content that's nested inside the HTML5 media element, if there is any
        mediaClone = this.$media.clone();
        $('source, track', mediaClone).remove();
        fallback = mediaClone.html().trim();
        if (fallback.length) {
                $fallbackDiv.html(fallback);
        }
        else {
                // use standard localized error message
                fallbackText =  this.tt.fallbackError1 + ' ' + this.tt[this.mediaType] + '. ';
                fallbackText += this.tt.fallbackError2 + ':';
                fallback = $('<p>').text(fallbackText);
                $fallbackDiv.html(fallback);
                showBrowserList = true;
        }

        if (showBrowserList) {
                browserList = $('<ul>');
                browsers = this.getSupportingBrowsers();
                for (i=0; i<browsers.length; i++) {
                        b = $('<li>');
                        b.text(browsers[i].name + ' ' + browsers[i].minVersion + ' ' + this.tt.orHigher);
                        browserList.append(b);
                }
                $fallbackDiv.append(browserList);
        }

        // if there's a poster, show that as well
        this.injectPoster($fallbackDiv, 'fallback');

        // inject $fallbackDiv into the DOM and remove broken content
        if (typeof this.$ableWrapper !== 'undefined') {
                this.$ableWrapper.before($fallbackDiv);
                this.$ableWrapper.remove();
        }
        else if (typeof this.$media !== 'undefined') {
                this.$media.before($fallbackDiv);
                this.$media.remove();
        }
        else {
                $('body').prepend($fallbackDiv);
        }
};

AblePlayer.prototype.getSupportingBrowsers = function() {

        var browsers = [];
        browsers[0] = {
                name:'Chrome',
                minVersion: '31'
        };
        browsers[1] = {
                name:'Firefox',
                minVersion: '34'
        };
        browsers[2] = {
                name:'Internet Explorer',
                minVersion: '10'
        };
        browsers[3] = {
                name:'Opera',
                minVersion: '26'
        };
        browsers[4] = {
                name:'Safari for Mac OS X',
                minVersion: '7.1'
        };
        browsers[5] = {
                name:'Safari for iOS',
                minVersion: '7.1'
        };
        browsers[6] = {
                name:'Android Browser',
                minVersion: '4.1'
        };
        browsers[7] = {
                name:'Chrome for Android',
                minVersion: '40'
        };
        return browsers;
}

AblePlayer.prototype.calculateControlLayout = function () {

        // Calculates the layout for controls based on media and options.
        // Returns an object with keys 'ul', 'ur', 'bl', 'br' for upper-left, etc.
        // Each associated value is array of control names to put at that location.

        var controlLayout = {
                'ul': ['play','restart','rewind','forward'],
                'ur': ['seek'],
                'bl': [],
                'br': []
        }

        // test for browser support for volume before displaying volume button
        if (this.browserSupportsVolume()) {
                // volume buttons are: 'mute','volume-soft','volume-medium','volume-loud'
                // previously supported button were: 'volume-up','volume-down'
                this.volumeButton = 'volume-' + this.getVolumeName(this.volume);
                controlLayout['ur'].push('volume');
        }
        else {
                this.volume = false;
        }

        // Calculate the two sides of the bottom-left grouping to see if we need separator pipe.
        var bll = [];
        var blr = [];

        if (this.isPlaybackRateSupported()) {
                bll.push('slower');
                bll.push('faster');
        }

        if (this.mediaType === 'video') {
                if (this.hasCaptions) {
                        bll.push('captions'); //closed captions
                }
                if (this.hasSignLanguage) {
                        bll.push('sign'); // sign language
                }
                if ((this.hasOpenDesc || this.hasClosedDesc) && (this.useDescriptionsButton)) {
                        bll.push('descriptions'); //audio description
                }
        }
        if (this.transcriptType === 'popup') {
                bll.push('transcript');
        }

        if (this.mediaType === 'video' && this.hasChapters && this.useChaptersButton) {
                bll.push('chapters');
        }

        controlLayout['br'].push('preferences');

        if (this.mediaType === 'video' && this.allowFullScreen) {
                controlLayout['br'].push('fullscreen');
        }

        // Include the pipe only if we need to.
        if (bll.length > 0 && blr.length > 0) {
                controlLayout['bl'] = bll;
                controlLayout['bl'].push('pipe');
                controlLayout['bl'] = controlLayout['bl'].concat(blr);
        }
        else {
                controlLayout['bl'] = bll.concat(blr);
        }

        return controlLayout;
};

AblePlayer.prototype.addControls = function() {

        // determine which controls to show based on several factors:
        // mediaType (audio vs video)
        // availability of tracks (e.g., for closed captions & audio description)
        // browser support (e.g., for sliders and speedButtons)
        // user preferences (???)
        // some controls are aligned on the left, and others on the right
        var thisObj, baseSliderWidth, controlLayout, sectionByOrder, useSpeedButtons, useFullScreen,
        i, j, k, controls, $controllerSpan, $sliderDiv, sliderLabel, mediaTimes, duration, $pipe, $pipeImg,
        tooltipId, tooltipX, tooltipY, control,
        buttonImg, buttonImgSrc, buttonTitle, $newButton, iconClass, buttonIcon, buttonUse, svgPath,
        leftWidth, rightWidth, totalWidth, leftWidthStyle, rightWidthStyle,
        controllerStyles, vidcapStyles, captionLabel, popupMenuId;

        thisObj = this;

        baseSliderWidth = 100; // arbitrary value, will be recalculated in refreshControls()

        // Initialize the layout into the this.controlLayout variable.
        controlLayout = this.calculateControlLayout();

        sectionByOrder = {0: 'ul', 1:'ur', 2:'bl', 3:'br'};

        // add an empty div to serve as a tooltip
        tooltipId = this.mediaId + '-tooltip';
        this.$tooltipDiv = $('<div>',{
                'id': tooltipId,
                'class': 'able-tooltip'
        }).hide();
        this.$controllerDiv.append(this.$tooltipDiv);

        // step separately through left and right controls
        for (i = 0; i <= 3; i++) {
                controls = controlLayout[sectionByOrder[i]];
                if ((i % 2) === 0) {
                        $controllerSpan = $('<div>',{
                                'class': 'able-left-controls'
                        });
                }
                else {
                        $controllerSpan = $('<div>',{
                                'class': 'able-right-controls'
                        });
                }
                this.$controllerDiv.append($controllerSpan);
                for (j=0; j<controls.length; j++) {
                        control = controls[j];
                        if (control === 'seek') {
                                $sliderDiv = $('<div class="able-seekbar"></div>');
                                sliderLabel = this.mediaType + ' ' + this.tt.seekbarLabel;
                                $controllerSpan.append($sliderDiv);
                                if (typeof this.duration === 'undefined' || this.duration === 0) {
                                        // set arbitrary starting duration, and change it when duration is known
                                        this.duration = 100;
                                        // also set elapsed to 0
                                        this.elapsed = 0;
                                }
                                this.seekBar = new AccessibleSlider(this.mediaType, $sliderDiv, 'horizontal', baseSliderWidth, 0, this.duration, this.seekInterval, sliderLabel, 'seekbar', true, 'visible');
                        }
                        else if (control === 'pipe') {
                                // TODO: Unify this with buttons somehow to avoid code duplication
                                $pipe = $('<span>', {
                                        'tabindex': '-1',
                                        'aria-hidden': 'true'
                                });
                                if (this.iconType === 'font') {
                                        $pipe.addClass('icon-pipe');
                                }
                                else {
                                        $pipeImg = $('<img>', {
                                                src: this.rootPath + 'button-icons/' + this.iconColor + '/pipe.png',
                                                alt: '',
                                                role: 'presentation'
                                        });
                                        $pipe.append($pipeImg);
                                }
                                $controllerSpan.append($pipe);
                        }
                        else {
                                // this control is a button
                                if (control === 'volume') {
                                        buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/' + this.volumeButton + '.png';
                                }
                                else if (control === 'fullscreen') {
                                        buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/fullscreen-expand.png';
                                }
                                else if (control === 'slower') {
                                        if (this.speedIcons === 'animals') {
                                                buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/turtle.png';
                                        }
                                        else {
                                                buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/slower.png';
                                        }
                                }
                                else if (control === 'faster') {
                                        if (this.speedIcons === 'animals') {
                                                buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/rabbit.png';
                                        }
                                        else {
                                                buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/faster.png';
                                        }
                                }
                                else {
                                        buttonImgSrc = this.rootPath + 'button-icons/' + this.iconColor + '/' + control + '.png';
                                }
                                buttonTitle = this.getButtonTitle(control);

                                // icomoon documentation recommends the following markup for screen readers:
                                // 1. link element (or in our case, button). Nested inside this element:
                                // 2. span that contains the icon font (in our case, buttonIcon)
                                // 3. span that contains a visually hidden label for screen readers (buttonLabel)
                                // In addition, we are adding aria-label to the button (but not title)
                                // And if iconType === 'image', we are replacing #2 with an image (with alt="" and role="presentation")
                                // This has been thoroughly tested and works well in all screen reader/browser combinations
                                // See https://github.com/ableplayer/ableplayer/issues/81
                                $newButton = $('<button>',{
                                        'type': 'button',
                                        'tabindex': '0',
                                        'aria-label': buttonTitle,
                                        'class': 'able-button-handler-' + control
                                });
                                if (control === 'volume' || control === 'preferences') {
                                        if (control == 'preferences') {
                                                popupMenuId = this.mediaId + '-prefs-menu';
                                        }
                                        else if (control === 'volume') {
                                                popupMenuId = this.mediaId + '-volume-slider';
                                        }
                                        $newButton.attr({
                                                'aria-controls': popupMenuId,
                                                'aria-expanded': 'false'
                                        });
                                }
                                if (this.iconType === 'font') {
                                        if (control === 'volume') {
                                                iconClass = 'icon-' + this.volumeButton;
                                        }
                                        else if (control === 'slower') {
                                                if (this.speedIcons === 'animals') {
                                                        iconClass = 'icon-turtle';
                                                }
                                                else {
                                                        iconClass = 'icon-slower';
                                                }
                                        }
                                        else if (control === 'faster') {
                                                if (this.speedIcons === 'animals') {
                                                        iconClass = 'icon-rabbit';
                                                }
                                                else {
                                                        iconClass = 'icon-faster';
                                                }
                                        }
                                        else {
                                                iconClass = 'icon-' + control;
                                        }
                                        buttonIcon = $('<span>',{
                                                'class': iconClass,
                                                'aria-hidden': 'true'
                                        });
                                        $newButton.append(buttonIcon);
                                }
                                else if (this.iconType === 'svg') {

                                /*
                                        // Unused option for adding SVG:
                                        // Use <use> element to link to button-icons/able-icons.svg
                                        // Advantage: SVG file can be cached
                                        // Disadvantage: Not supported by Safari 6, IE 6-11, or Edge 12
                                        // Instead, adding <svg> element within each <button>
                                        if (control === 'volume') {
                                                iconClass = 'svg-' + this.volumeButton;
                                        }
                                        else if (control === 'fullscreen') {
                                                iconClass = 'svg-fullscreen-expand';
                                        }
                                        else if (control === 'slower') {
                                                if (this.speedIcons === 'animals') {
                                                        iconClass = 'svg-turtle';
                                                }
                                                else {
                                                        iconClass = 'svg-slower';
                                                }
                                        }
                                        else if (control === 'faster') {
                                                if (this.speedIcons === 'animals') {
                                                        iconClass = 'svg-rabbit';
                                                }
                                                else {
                                                        iconClass = 'svg-faster';
                                                }
                                        }
                                        else {
                                                iconClass = 'svg-' + control;
                                        }
                                        buttonIcon = $('<svg>',{
                                                'class': iconClass
                                        });
                                        buttonUse = $('<use>',{
                                                'xlink:href': this.rootPath + 'button-icons/able-icons.svg#' + iconClass
                                        });
                                        buttonIcon.append(buttonUse);
                                        */
                                        var svgData;
                                        if (control === 'volume') {
                                                svgData = this.getSvgData(this.volumeButton);
                                        }
                                        else if (control === 'fullscreen') {
                                                svgData = this.getSvgData('fullscreen-expand');
                                        }
                                        else if (control === 'slower') {
                                                if (this.speedIcons === 'animals') {
                                                        svgData = this.getSvgData('turtle');
                                                }
                                                else {
                                                        svgData = this.getSvgData('slower');
                                                }
                                        }
                                        else if (control === 'faster') {
                                                if (this.speedIcons === 'animals') {
                                                        svgData = this.getSvgData('rabbit');
                                                }
                                                else {
                                                        svgData = this.getSvgData('faster');
                                                }
                                        }
                                        else {
                                                svgData = this.getSvgData(control);
                                        }
                                        buttonIcon = $('<svg>',{
                                                'focusable': 'false',
                                                'aria-hidden': 'true',
                                                'viewBox': svgData[0]
                                        });
                                        svgPath = $('<path>',{
                                                'd': svgData[1]
                                        });
                                        buttonIcon.append(svgPath);
                                        $newButton.html(buttonIcon);

                                        // Final step: Need to refresh the DOM in order for browser to process & display the SVG
                                        $newButton.html($newButton.html());
                                }
                                else {
                                        // use images
                                        buttonImg = $('<img>',{
                                                'src': buttonImgSrc,
                                                'alt': '',
                                                'role': 'presentation'
                                        });
                                        $newButton.append(buttonImg);
                                }
                                // add the visibly-hidden label for screen readers that don't support aria-label on the button
                                var buttonLabel = $('<span>',{
                                        'class': 'able-clipped'
                                }).text(buttonTitle);
                                $newButton.append(buttonLabel);
                                // add an event listener that displays a tooltip on mouseenter or focus
                                $newButton.on('mouseenter focus',function(e) {
                                        var label = $(this).attr('aria-label');
                                        // get position of this button
                                        var position = $(this).position();
                                        var buttonHeight = $(this).height();
                                        var buttonWidth = $(this).width();
                                        var tooltipY = position.top - buttonHeight - 15;
                                        var centerTooltip = true;
                                        if ($(this).closest('div').hasClass('able-right-controls')) {
                                                // this control is on the right side
                                                if ($(this).closest('div').find('button:last').get(0) == $(this).get(0)) {
                                                        // this is the last control on the right
                                                        // position tooltip using the "right" property
                                                        centerTooltip = false;
                                                        var tooltipX = 0;
                                                        var tooltipStyle = {
                                                                left: '',
                                                                right: tooltipX + 'px',
                                                                top: tooltipY + 'px'
                                                        };
                                                }
                                        }
                                        else {
                                                // this control is on the left side
                                                if ($(this).is(':first-child')) {
                                                        // this is the first control on the left
                                                        centerTooltip = false;
                                                        var tooltipX = position.left;
                                                        var tooltipStyle = {
                                                                left: tooltipX + 'px',
                                                                right: '',
                                                                top: tooltipY + 'px'
                                                        };
                                                }
                                        }
                                        if (centerTooltip) {
                                                // populate tooltip, then calculate its width before showing it
                                                var tooltipWidth = AblePlayer.localGetElementById($newButton[0], tooltipId).text(label).width();
                                                // center the tooltip horizontally over the button
                                                var tooltipX = position.left - tooltipWidth/2;
                                                var tooltipStyle = {
                                                        left: tooltipX + 'px',
                                                        right: '',
                                                        top: tooltipY + 'px'
                                                };
                                        }
                                        var tooltip = AblePlayer.localGetElementById($newButton[0], tooltipId).text(label).css(tooltipStyle);
                                        thisObj.showTooltip(tooltip);
                                        $(this).on('mouseleave blur',function() {
                                                AblePlayer.localGetElementById($newButton[0], tooltipId).text('').hide();
                                        })
                                });

                                if (control === 'captions') {
                                        if (!this.prefCaptions || this.prefCaptions !== 1) {
                                                // captions are available, but user has them turned off
                                                if (this.captions.length > 1) {
                                                        captionLabel = this.tt.captions;
                                                }
                                                else {
                                                        captionLabel = this.tt.showCaptions;
                                                }
                                                $newButton.addClass('buttonOff').attr('title',captionLabel);
                                        }
                                }
                                else if (control === 'descriptions') {
                                        if (!this.prefDesc || this.prefDesc !== 1) {
                                                // user prefer non-audio described version
                                                // Therefore, load media without description
                                                // Description can be toggled on later with this button
                                                $newButton.addClass('buttonOff').attr('title',this.tt.turnOnDescriptions);
                                        }
                                }

                                $controllerSpan.append($newButton);

                                // create variables of buttons that are referenced throughout the AblePlayer object
                                if (control === 'play') {
                                        this.$playpauseButton = $newButton;
                                }
                                else if (control === 'captions') {
                                        this.$ccButton = $newButton;
                                }
                                else if (control === 'sign') {
                                        this.$signButton = $newButton;
                                        // gray out sign button if sign language window is not active
                                        if (!(this.$signWindow.is(':visible'))) {
                                                this.$signButton.addClass('buttonOff');
                                        }
                                }
                                else if (control === 'descriptions') {
                                        this.$descButton = $newButton;
                                        // button will be enabled or disabled in description.js > initDescription()
                                }
                                else if (control === 'mute') {
                                        this.$muteButton = $newButton;
                                }
                                else if (control === 'transcript') {
                                        this.$transcriptButton = $newButton;
                                        // gray out transcript button if transcript is not active
                                        if (!(this.$transcriptDiv.is(':visible'))) {
                                                this.$transcriptButton.addClass('buttonOff').attr('title',this.tt.showTranscript);
                                        }
                                }
                                else if (control === 'fullscreen') {
                                        this.$fullscreenButton = $newButton;
                                }
                                else if (control === 'chapters') {
                                        this.$chaptersButton = $newButton;
                                }
                                else if (control === 'preferences') {
                                        this.$prefsButton = $newButton;
                                }
                                else if (control === 'volume') {
                                        this.$volumeButton = $newButton;
                                }
                        }
                        if (control === 'volume') {
                                // in addition to the volume button, add a hidden slider
                                this.addVolumeSlider($controllerSpan);
                        }
                }
                if ((i % 2) == 1) {
                        this.$controllerDiv.append('<div style="clear:both;"></div>');
                }
        }

        if (this.mediaType === 'video') {

                if (typeof this.$captionsDiv !== 'undefined') {
                        // stylize captions based on user prefs
                        this.stylizeCaptions(this.$captionsDiv);
                }
                if (typeof this.$descDiv !== 'undefined') {
                        // stylize descriptions based on user's caption prefs
                        this.stylizeCaptions(this.$descDiv);
                }
        }

        // combine left and right controls arrays for future reference
        this.controls = [];
        for (var sec in controlLayout) if (controlLayout.hasOwnProperty(sec)) {
                this.controls = this.controls.concat(controlLayout[sec]);
        }

        // Update state-based display of controls.
        this.refreshControls('init');
};

AblePlayer.prototype.useSvg = function () {

        // Modified from IcoMoon.io svgxuse
        // @copyright Copyright (c) 2016 IcoMoon.io
        // @license             Licensed under MIT license
        // See https://github.com/Keyamoon/svgxuse
        // @version             1.1.16

        var cache = Object.create(null); // holds xhr objects to prevent multiple requests
        var checkUseElems,
                        tid; // timeout id
        var debouncedCheck = function () {
                clearTimeout(tid);
                tid = setTimeout(checkUseElems, 100);
        };
        var unobserveChanges = function () {
                return;
        };
        var observeChanges = function () {
                var observer;
                window.addEventListener('resize', debouncedCheck, false);
                window.addEventListener('orientationchange', debouncedCheck, false);
                if (window.MutationObserver) {
                        observer = new MutationObserver(debouncedCheck);
                        observer.observe(document.documentElement, {
                                childList: true,
                                subtree: true,
                                attributes: true
                        });
                        unobserveChanges = function () {
                                try {
                                        observer.disconnect();
                                        window.removeEventListener('resize', debouncedCheck, false);
                                        window.removeEventListener('orientationchange', debouncedCheck, false);
                                } catch (ignore) {}
                        };
                }
                else {
                        document.documentElement.addEventListener('DOMSubtreeModified', debouncedCheck, false);
                        unobserveChanges = function () {
                                document.documentElement.removeEventListener('DOMSubtreeModified', debouncedCheck, false);
                                window.removeEventListener('resize', debouncedCheck, false);
                                window.removeEventListener('orientationchange', debouncedCheck, false);
                        };
                }
        };
        var xlinkNS = 'http://www.w3.org/1999/xlink';
        checkUseElems = function () {
                var base,
                                bcr,
                                fallback = '', // optional fallback URL in case no base path to SVG file was given and no symbol definition was found.
                                hash,
                                i,
                                Request,
                                inProgressCount = 0,
                                isHidden,
                                url,
                                uses,
                                xhr;
                if (window.XMLHttpRequest) {
                        Request = new XMLHttpRequest();
                        if (Request.withCredentials !== undefined) {
                                Request = XMLHttpRequest;
                        }
                        else {
                                Request = XDomainRequest || undefined;
                        }
                }
                if (Request === undefined) {
                        return;
                }
                function observeIfDone() {
                        // If done with making changes, start watching for chagnes in DOM again
                        inProgressCount -= 1;
                        if (inProgressCount === 0) { // if all xhrs were resolved
                                observeChanges(); // watch for changes to DOM
                        }
                }
                function attrUpdateFunc(spec) {
                        return function () {
                                if (cache[spec.base] !== true) {
                                        spec.useEl.setAttributeNS(xlinkNS, 'xlink:href', '#' + spec.hash);
                                }
                        };
                }
                function onloadFunc(xhr) {
                        return function () {
                                var body = document.body;
                                var x = document.createElement('x');
                                var svg;
                                xhr.onload = null;
                                x.innerHTML = xhr.responseText;
                                svg = x.getElementsByTagName('svg')[0];
                                if (svg) {
                                        svg.setAttribute('aria-hidden', 'true');
                                        svg.style.position = 'absolute';
                                        svg.style.width = 0;
                                        svg.style.height = 0;
                                        svg.style.overflow = 'hidden';
                                        body.insertBefore(svg, body.firstChild);
                                }
                                observeIfDone();
                        };
                }
                function onErrorTimeout(xhr) {
                        return function () {
                                xhr.onerror = null;
                                xhr.ontimeout = null;
                                observeIfDone();
                        };
                }
                unobserveChanges(); // stop watching for changes to DOM
                // find all use elements
                uses = document.getElementsByTagName('use');
                for (i = 0; i < uses.length; i += 1) {
                        try {
                                bcr = uses[i].getBoundingClientRect();
                        } catch (ignore) {
                                // failed to get bounding rectangle of the use element
                                bcr = false;
                        }
                        url = uses[i].getAttributeNS(xlinkNS, 'href').split('#');
                        base = url[0];
                        hash = url[1];
                        isHidden = bcr && bcr.left === 0 && bcr.right === 0 && bcr.top === 0 && bcr.bottom === 0;
                        if (bcr && bcr.width === 0 && bcr.height === 0 && !isHidden) {
                                // the use element is empty
                                // if there is a reference to an external SVG, try to fetch it
                                // use the optional fallback URL if there is no reference to an external SVG
                                if (fallback && !base.length && hash && !document.getElementById(hash)) {
                                        base = fallback;
                                }
                                if (base.length) {
                                        // schedule updating xlink:href
                                        xhr = cache[base];
                                        if (xhr !== true) {
                                                // true signifies that prepending the SVG was not required
                                                setTimeout(attrUpdateFunc({
                                                        useEl: uses[i],
                                                        base: base,
                                                        hash: hash
                                                }), 0);
                                        }
                                        if (xhr === undefined) {
                                                xhr = new Request();
                                                cache[base] = xhr;
                                                xhr.onload = onloadFunc(xhr);
                                                xhr.onerror = onErrorTimeout(xhr);
                                                xhr.ontimeout = onErrorTimeout(xhr);
                                                xhr.open('GET', base);
                                                xhr.send();
                                                inProgressCount += 1;
                                        }
                                }
                        }
                        else {
                                if (!isHidden) {
                                        if (cache[base] === undefined) {
                                                // remember this URL if the use element was not empty and no request was sent
                                                cache[base] = true;
                                        }
                                        else if (cache[base].onload) {
                                                // if it turns out that prepending the SVG is not necessary,
                                                // abort the in-progress xhr.
                                                cache[base].abort();
                                                cache[base].onload = undefined;
                                                cache[base] = true;
                                        }
                                }
                        }
                }
                uses = '';
                inProgressCount += 1;
                observeIfDone();
        };

/*

// The load event fires when all resources have finished loading, which allows detecting whether SVG use elements are empty.
window.addEventListener('load', function winLoad() {
        window.removeEventListener('load', winLoad, false); // to prevent memory leaks
        tid = setTimeout(checkUseElems, 0);
}, false);

*/

};

AblePlayer.prototype.cuePlaylistItem = function(sourceIndex) {

        // Move to a new item in a playlist.
        // NOTE: Swapping source for audio description is handled elsewhere;
        // see description.js > swapDescription()

        /*
                // Decided against preventing a reload of the current item in the playlist.
                // If it's clickable, users should be able to click on it and expect something to happen.
                // Leaving here though in case it's determined to be desirable.
        if (sourceIndex === this.playlistItemIndex) {
                // user has requested the item that's currently playing
                // just ignore the request
                return;
        }
        this.playlistItemIndex = sourceIndex;
        */

        var $newItem, prevPlayer, newPlayer, itemTitle, itemLang, sources, s, i, $newSource, nowPlayingSpan;

        var thisObj = this;

        prevPlayer = this.player;

        if (this.initializing) { // this is the first track - user hasn't pressed play yet
                // do nothing.
        }
        else {
                        if (this.playerCreated) {
                        // remove the old
                        this.deletePlayer();
                }
        }

        // Determine appropriate player to play this media
        $newItem = this.$playlist.eq(sourceIndex);
        if (this.hasAttr($newItem,'data-youtube-id')) {
                this.youTubeId = $newItem.attr('data-youtube-id');
                newPlayer = 'youtube';
        }
        else {
                        newPlayer = 'html5';
        }

        if (newPlayer === 'youtube') {
                if (prevPlayer === 'html5') {
                        // pause and hide the previous media
                        if (this.playing) {
                                this.pauseMedia();
                        }
                        this.$media.hide();
                }
        }
        else {
                        // the new player is not youtube
                this.youTubeId = false;
                if (prevPlayer === 'youtube') {
                        // unhide the media element
                        this.$media.show();
                }
        }
        this.player = newPlayer;

        // set swappingSrc; needs to be true within recreatePlayer(), called below
        this.swappingSrc = true;

        // transfer media attributes from playlist to media element
        if (this.hasAttr($newItem,'data-poster')) {
                this.$media.attr('poster',$newItem.attr('data-poster'));
        }
        if (this.hasAttr($newItem,'data-width')) {
                this.$media.attr('width',$newItem.attr('data-width'));
        }
        if (this.hasAttr($newItem,'data-height')) {
                this.$media.attr('height',$newItem.attr('data-height'));
        }
        if (this.hasAttr($newItem,'data-youtube-desc-id')) {
                this.$media.attr('data-youtube-desc-id',$newItem.attr('data-youtube-desc-id'));
        }
        if (this.youTubeId) {
                this.$media.attr('data-youtube-id',$newItem.attr('data-youtube-id'));
        }

        // add new <source> elements from playlist data
        var $sourceSpans = $newItem.children('span.able-source');
        if ($sourceSpans.length) {
                $sourceSpans.each(function() {
                        if (thisObj.hasAttr($(this),'data-src')) {
                                // this is the only required attribute
                                var $newSource = $('<source>',{
                                        'src': $(this).attr('data-src')
                                });
                                if (thisObj.hasAttr($(this),'data-type')) {
                                        $newSource.attr('type',$(this).attr('data-type'));
                                }
                                if (thisObj.hasAttr($(this),'data-desc-src')) {
                                        $newSource.attr('data-desc-src',$(this).attr('data-desc-src'));
                                }
                                if (thisObj.hasAttr($(this),'data-sign-src')) {
                                        $newSource.attr('data-sign-src',$(this).attr('data-sign-src'));
                                }
                                thisObj.$media.append($newSource);
                        }
                });
        }

        // add new <track> elements from playlist data
        var $trackSpans = $newItem.children('span.able-track');
        if ($trackSpans.length) {
                 // for each element in $trackSpans, create a new <track> element
                $trackSpans.each(function() {
                        if (thisObj.hasAttr($(this),'data-src') &&
                                thisObj.hasAttr($(this),'data-kind') &&
                                thisObj.hasAttr($(this),'data-srclang')) {
                                // all required attributes are present
                                var $newTrack = $('<track>',{
                                        'src': $(this).attr('data-src'),
                                        'kind': $(this).attr('data-kind'),
                                        'srclang': $(this).attr('data-srclang')
                                });
                                if (thisObj.hasAttr($(this),'data-label')) {
                                        $newTrack.attr('label',$(this).attr('data-label'));
                                }
                                thisObj.$media.append($newTrack);
                        }
                });
        }

        itemTitle = $newItem.text();
        if (this.hasAttr($newItem,'lang')) {
                itemLang = $newItem.attr('lang');
        }
        // Update relevant arrays
        this.$sources = this.$media.find('source');

        // recreate player, informed by new attributes and track elements
        this.recreatePlayer();

        // update playlist to indicate which item is playing
        //$('.able-playlist li').removeClass('able-current');
        this.$playlist.removeClass('able-current');
        this.$playlist.eq(sourceIndex).addClass('able-current');

        // update Now Playing div
        if (this.showNowPlaying === true) {
                if (typeof this.$nowPlayingDiv !== 'undefined') {
                        nowPlayingSpan = $('<span>');
                        if (typeof itemLang !== 'undefined') {
                                nowPlayingSpan.attr('lang',itemLang);
                        }
                        nowPlayingSpan.html('<span>' + this.tt.selectedTrack + ':</span>' + itemTitle);
                        this.$nowPlayingDiv.html(nowPlayingSpan);
                }
        }

        // finished swapping src, now reload the new source file.
        this.swappingSrc = false;

        if (this.player === 'html5') {
                this.media.load();
        }
        else if (this.player === 'youtube') {
                // TODO: Load new youTubeId
        }

        // if this.swappingSrc is true, media will autoplay when ready
        if (this.initializing) { // this is the first track - user hasn't pressed play yet
                this.swappingSrc = false;
        }
        else {
                this.swappingSrc = true;
                if (this.player === 'html5') {
                        this.media.load();
                }
                else if (this.player === 'youtube') {
                        this.okToPlay = true;
                }
        }
};

AblePlayer.prototype.deletePlayer = function() {

        // remove previous video's attributes and child elements from media element
        if (this.player == 'youtube') {
                        var $youTubeIframe = this.$mediaContainer.find('iframe');
                        $youTubeIframe.remove();
        }
        this.$media.removeAttr('poster width height');
        this.$media.empty();

        // Empty elements that will be rebuilt
        this.$controllerDiv.empty();
        // this.$statusBarDiv.empty();
        // this.$timer.empty();
        this.$elapsedTimeContainer.empty().text('0:00'); // span.able-elapsedTime
        this.$durationContainer.empty(); // span.able-duration

        // Remove popup windows and modal dialogs; these too will be rebuilt
        if (this.$signWindow) {
                        this.$signWindow.remove();
        }
        if (this.$transcriptArea) {
                        this.$transcriptArea.remove();
        }
        $('.able-modal-dialog').remove();

        // reset key variables
        this.hasCaptions = false;
        this.hasChapters = false;
        this.captionsPopup = null;
        this.chaptersPopup = null;
};

AblePlayer.prototype.getButtonTitle = function(control) {

        if (control === 'playpause') {
                return this.tt.play;
        }
        else if (control === 'play') {
                return this.tt.play;
        }
        else if (control === 'pause') {
                return this.tt.pause;
        }
        else if (control === 'restart') {
                return this.tt.restart;
        }
        else if (control === 'rewind') {
                return this.tt.rewind;
        }
        else if (control === 'forward') {
                return this.tt.forward;
        }
        else if (control === 'captions') {
                if (this.captions.length > 1) {
                        return this.tt.captions;
                }
                else {
                        if (this.captionsOn) {
                                return this.tt.hideCaptions;
                        }
                        else {
                                return this.tt.showCaptions;
                        }
                }
        }
        else if (control === 'descriptions') {
                if (this.descOn) {
                        return this.tt.turnOffDescriptions;
                }
                else {
                        return this.tt.turnOnDescriptions;
                }
        }
        else if (control === 'transcript') {
                if (this.$transcriptDiv.is(':visible')) {
                        return this.tt.hideTranscript;
                }
                else {
                        return this.tt.showTranscript;
                }
        }
        else if (control === 'chapters') {
                return this.tt.chapters;
        }
        else if (control === 'sign') {
                return this.tt.sign;
        }
        else if (control === 'volume') {
                return this.tt.volume;
        }
        else if (control === 'faster') {
                return this.tt.faster;
        }
        else if (control === 'slower') {
                return this.tt.slower;
        }
        else if (control === 'preferences') {
                return this.tt.preferences;
        }
        else if (control === 'help') {
                // return this.tt.help;
        }
        else {
                // there should be no other controls, but just in case:
                // return the name of the control with first letter in upper case
                // ultimately will need to get a translated label from this.tt
                if (this.debug) {
                        console.log('Found an untranslated label: ' + control);
                }
                return control.charAt(0).toUpperCase() + control.slice(1);
        }
};

})(jQuery);