/**
 * @file player.js
 */
// Subclasses Component
import Component from './component.js';

import {version} from '../../package.json';
import document from 'global/document';
import window from 'global/window';
import evented from './mixins/evented';
import {isEvented, addEventedCallback} from './mixins/evented';
import * as Events from './utils/events.js';
import * as Dom from './utils/dom.js';
import * as Fn from './utils/fn.js';
import * as Guid from './utils/guid.js';
import * as browser from './utils/browser.js';
import {IS_CHROME, IS_WINDOWS} from './utils/browser.js';
import log, { createLogger } from './utils/log.js';
import {toTitleCase, titleCaseEquals} from './utils/str.js';
import { createTimeRange } from './utils/time.js';
import { bufferedPercent } from './utils/buffer.js';
import * as stylesheet from './utils/stylesheet.js';
import FullscreenApi from './fullscreen-api.js';
import MediaError from './media-error.js';
import safeParseTuple from 'safe-json-parse/tuple';
import {merge} from './utils/obj';
import {silencePromise, isPromise} from './utils/promise';
import textTrackConverter from './tracks/text-track-list-converter.js';
import ModalDialog from './modal-dialog';
import Tech from './tech/tech.js';
import * as middleware from './tech/middleware.js';
import {ALL as TRACK_TYPES} from './tracks/track-types';
import filterSource from './utils/filter-source';
import {getMimetype, findMimetype} from './utils/mimetypes';
import {hooks} from './utils/hooks';
import {isObject} from './utils/obj';
import keycode from 'keycode';

// The following imports are used only to ensure that the corresponding modules
// are always included in the video.js package. Importing the modules will
// execute them and they will register themselves with video.js.
import './tech/loader.js';
import './poster-image.js';
import './tracks/text-track-display.js';
import './loading-spinner.js';
import './big-play-button.js';
import './close-button.js';
import './control-bar/control-bar.js';
import './error-display.js';
import './tracks/text-track-settings.js';
import './resize-manager.js';
import './live-tracker.js';
import './title-bar.js';

// Import Html5 tech, at least for disposing the original video tag.
import './tech/html5.js';

// The following tech events are simply re-triggered
// on the player when they happen
const TECH_EVENTS_RETRIGGER = [
  /**
   * Fired while the user agent is downloading media data.
   *
   * @event Player#progress
   * @type {Event}
   */
  /**
   * Retrigger the `progress` event that was triggered by the {@link Tech}.
   *
   * @private
   * @method Player#handleTechProgress_
   * @fires Player#progress
   * @listens Tech#progress
   */
  'progress',

  /**
   * Fires when the loading of an audio/video is aborted.
   *
   * @event Player#abort
   * @type {Event}
   */
  /**
   * Retrigger the `abort` event that was triggered by the {@link Tech}.
   *
   * @private
   * @method Player#handleTechAbort_
   * @fires Player#abort
   * @listens Tech#abort
   */
  'abort',

  /**
   * Fires when the browser is intentionally not getting media data.
   *
   * @event Player#suspend
   * @type {Event}
   */
  /**
   * Retrigger the `suspend` event that was triggered by the {@link Tech}.
   *
   * @private
   * @method Player#handleTechSuspend_
   * @fires Player#suspend
   * @listens Tech#suspend
   */
  'suspend',

  /**
   * Fires when the current playlist is empty.
   *
   * @event Player#emptied
   * @type {Event}
   */
  /**
   * Retrigger the `emptied` event that was triggered by the {@link Tech}.
   *
   * @private
   * @method Player#handleTechEmptied_
   * @fires Player#emptied
   * @listens Tech#emptied
   */
  'emptied',
  /**
   * Fires when the browser is trying to get media data, but data is not available.
   *
   * @event Player#stalled
   * @type {Event}
   */
  /**
   * Retrigger the `stalled` event that was triggered by the {@link Tech}.
   *
   * @private
   * @method Player#handleTechStalled_
   * @fires Player#stalled
   * @listens Tech#stalled
   */
  'stalled',

  /**
   * Fires when the browser has loaded meta data for the audio/video.
   *
   * @event Player#loadedmetadata
   * @type {Event}
   */
  /**
   * Retrigger the `loadedmetadata` event that was triggered by the {@link Tech}.
   *
   * @private
   * @method Player#handleTechLoadedmetadata_
   * @fires Player#loadedmetadata
   * @listens Tech#loadedmetadata
   */
  'loadedmetadata',

  /**
   * Fires when the browser has loaded the current frame of the audio/video.
   *
   * @event Player#loadeddata
   * @type {event}
   */
  /**
   * Retrigger the `loadeddata` event that was triggered by the {@link Tech}.
   *
   * @private
   * @method Player#handleTechLoaddeddata_
   * @fires Player#loadeddata
   * @listens Tech#loadeddata
   */
  'loadeddata',

  /**
   * Fires when the current playback position has changed.
   *
   * @event Player#timeupdate
   * @type {event}
   */
  /**
   * Retrigger the `timeupdate` event that was triggered by the {@link Tech}.
   *
   * @private
   * @method Player#handleTechTimeUpdate_
   * @fires Player#timeupdate
   * @listens Tech#timeupdate
   */
  'timeupdate',

  /**
   * Fires when the video's intrinsic dimensions change
   *
   * @event Player#resize
   * @type {event}
   */
  /**
   * Retrigger the `resize` event that was triggered by the {@link Tech}.
   *
   * @private
   * @method Player#handleTechResize_
   * @fires Player#resize
   * @listens Tech#resize
   */
  'resize',

  /**
   * Fires when the volume has been changed
   *
   * @event Player#volumechange
   * @type {event}
   */
  /**
   * Retrigger the `volumechange` event that was triggered by the {@link Tech}.
   *
   * @private
   * @method Player#handleTechVolumechange_
   * @fires Player#volumechange
   * @listens Tech#volumechange
   */
  'volumechange',

  /**
   * Fires when the text track has been changed
   *
   * @event Player#texttrackchange
   * @type {event}
   */
  /**
   * Retrigger the `texttrackchange` event that was triggered by the {@link Tech}.
   *
   * @private
   * @method Player#handleTechTexttrackchange_
   * @fires Player#texttrackchange
   * @listens Tech#texttrackchange
   */
  'texttrackchange'
];

// events to queue when playback rate is zero
// this is a hash for the sole purpose of mapping non-camel-cased event names
// to camel-cased function names
const TECH_EVENTS_QUEUE = {
  canplay: 'CanPlay',
  canplaythrough: 'CanPlayThrough',
  playing: 'Playing',
  seeked: 'Seeked'
};

const BREAKPOINT_ORDER = [
  'tiny',
  'xsmall',
  'small',
  'medium',
  'large',
  'xlarge',
  'huge'
];

const BREAKPOINT_CLASSES = {};

// grep: vjs-layout-tiny
// grep: vjs-layout-x-small
// grep: vjs-layout-small
// grep: vjs-layout-medium
// grep: vjs-layout-large
// grep: vjs-layout-x-large
// grep: vjs-layout-huge
BREAKPOINT_ORDER.forEach(k => {
  const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k;

  BREAKPOINT_CLASSES[k] = `vjs-layout-${v}`;
});

const DEFAULT_BREAKPOINTS = {
  tiny: 210,
  xsmall: 320,
  small: 425,
  medium: 768,
  large: 1440,
  xlarge: 2560,
  huge: Infinity
};

/**
 * An instance of the `Player` class is created when any of the Video.js setup methods
 * are used to initialize a video.
 *
 * After an instance has been created it can be accessed globally in three ways:
 * 1. By calling `videojs.getPlayer('example_video_1');`
 * 2. By calling `videojs('example_video_1');` (not recomended)
 * 2. By using it directly via  `videojs.players.example_video_1;`
 *
 * @extends Component
 * @global
 */
class Player extends Component {

  /**
   * Create an instance of this class.
   *
   * @param {Element} tag
   *        The original video DOM element used for configuring options.
   *
   * @param {Object} [options]
   *        Object of option names and values.
   *
   * @param {Function} [ready]
   *        Ready callback function.
   */
  constructor(tag, options, ready) {
    // Make sure tag ID exists
    tag.id = tag.id || options.id || `vjs_video_${Guid.newGUID()}`;

    // Set Options
    // The options argument overrides options set in the video tag
    // which overrides globally set options.
    // This latter part coincides with the load order
    // (tag must exist before Player)
    options = Object.assign(Player.getTagSettings(tag), options);

    // Delay the initialization of children because we need to set up
    // player properties first, and can't use `this` before `super()`
    options.initChildren = false;

    // Same with creating the element
    options.createEl = false;

    // don't auto mixin the evented mixin
    options.evented = false;

    // we don't want the player to report touch activity on itself
    // see enableTouchActivity in Component
    options.reportTouchActivity = false;

    // If language is not set, get the closest lang attribute
    if (!options.language) {
      const closest = tag.closest('[lang]');

      if (closest) {
        options.language = closest.getAttribute('lang');
      }
    }

    // Run base component initializing with new options
    super(null, options, ready);

    // Create bound methods for document listeners.
    this.boundDocumentFullscreenChange_ = (e) => this.documentFullscreenChange_(e);
    this.boundFullWindowOnEscKey_ = (e) => this.fullWindowOnEscKey(e);

    this.boundUpdateStyleEl_ = (e) => this.updateStyleEl_(e);
    this.boundApplyInitTime_ = (e) => this.applyInitTime_(e);
    this.boundUpdateCurrentBreakpoint_ = (e) => this.updateCurrentBreakpoint_(e);

    this.boundHandleTechClick_ = (e) => this.handleTechClick_(e);
    this.boundHandleTechDoubleClick_ = (e) => this.handleTechDoubleClick_(e);
    this.boundHandleTechTouchStart_ = (e) => this.handleTechTouchStart_(e);
    this.boundHandleTechTouchMove_ = (e) => this.handleTechTouchMove_(e);
    this.boundHandleTechTouchEnd_ = (e) => this.handleTechTouchEnd_(e);
    this.boundHandleTechTap_ = (e) => this.handleTechTap_(e);

    // default isFullscreen_ to false
    this.isFullscreen_ = false;

    // create logger
    this.log = createLogger(this.id_);

    // Hold our own reference to fullscreen api so it can be mocked in tests
    this.fsApi_ = FullscreenApi;

    // Tracks when a tech changes the poster
    this.isPosterFromTech_ = false;

    // Holds callback info that gets queued when playback rate is zero
    // and a seek is happening
    this.queuedCallbacks_ = [];

    // Turn off API access because we're loading a new tech that might load asynchronously
    this.isReady_ = false;

    // Init state hasStarted_
    this.hasStarted_ = false;

    // Init state userActive_
    this.userActive_ = false;

    // Init debugEnabled_
    this.debugEnabled_ = false;

    // Init state audioOnlyMode_
    this.audioOnlyMode_ = false;

    // Init state audioPosterMode_
    this.audioPosterMode_ = false;

    // Init state audioOnlyCache_
    this.audioOnlyCache_ = {
      playerHeight: null,
      hiddenChildren: []
    };

    // if the global option object was accidentally blown away by
    // someone, bail early with an informative error
    if (!this.options_ ||
        !this.options_.techOrder ||
        !this.options_.techOrder.length) {
      throw new Error('No techOrder specified. Did you overwrite ' +
                      'videojs.options instead of just changing the ' +
                      'properties you want to override?');
    }

    // Store the original tag used to set options
    this.tag = tag;

    // Store the tag attributes used to restore html5 element
    this.tagAttributes = tag && Dom.getAttributes(tag);

    // Update current language
    this.language(this.options_.language);

    // Update Supported Languages
    if (options.languages) {
      // Normalise player option languages to lowercase
      const languagesToLower = {};

      Object.getOwnPropertyNames(options.languages).forEach(function(name) {
        languagesToLower[name.toLowerCase()] = options.languages[name];
      });
      this.languages_ = languagesToLower;
    } else {
      this.languages_ = Player.prototype.options_.languages;
    }

    this.resetCache_();

    // Set poster
    this.poster_ = options.poster || '';

    // Set controls
    this.controls_ = !!options.controls;

    // Original tag settings stored in options
    // now remove immediately so native controls don't flash.
    // May be turned back on by HTML5 tech if nativeControlsForTouch is true
    tag.controls = false;
    tag.removeAttribute('controls');

    this.changingSrc_ = false;
    this.playCallbacks_ = [];
    this.playTerminatedQueue_ = [];

    // the attribute overrides the option
    if (tag.hasAttribute('autoplay')) {
      this.autoplay(true);
    } else {
      // otherwise use the setter to validate and
      // set the correct value.
      this.autoplay(this.options_.autoplay);
    }

    // check plugins
    if (options.plugins) {
      Object.keys(options.plugins).forEach((name) => {
        if (typeof this[name] !== 'function') {
          throw new Error(`plugin "${name}" does not exist`);
        }
      });
    }

    /*
     * Store the internal state of scrubbing
     *
     * @private
     * @return {Boolean} True if the user is scrubbing
     */
    this.scrubbing_ = false;

    this.el_ = this.createEl();

    // Make this an evented object and use `el_` as its event bus.
    evented(this, {eventBusKey: 'el_'});

    // listen to document and player fullscreenchange handlers so we receive those events
    // before a user can receive them so we can update isFullscreen appropriately.
    // make sure that we listen to fullscreenchange events before everything else to make sure that
    // our isFullscreen method is updated properly for internal components as well as external.
    if (this.fsApi_.requestFullscreen) {
      Events.on(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
      this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
    }

    if (this.fluid_) {
      this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
    }
    // We also want to pass the original player options to each component and plugin
    // as well so they don't need to reach back into the player for options later.
    // We also need to do another copy of this.options_ so we don't end up with
    // an infinite loop.
    const playerOptionsCopy = merge(this.options_);

    // Load plugins
    if (options.plugins) {
      Object.keys(options.plugins).forEach((name) => {
        this[name](options.plugins[name]);
      });
    }

    // Enable debug mode to fire debugon event for all plugins.
    if (options.debug) {
      this.debug(true);
    }

    this.options_.playerOptions = playerOptionsCopy;

    this.middleware_ = [];

    this.playbackRates(options.playbackRates);

    this.initChildren();

    // Set isAudio based on whether or not an audio tag was used
    this.isAudio(tag.nodeName.toLowerCase() === 'audio');

    // Update controls className. Can't do this when the controls are initially
    // set because the element doesn't exist yet.
    if (this.controls()) {
      this.addClass('vjs-controls-enabled');
    } else {
      this.addClass('vjs-controls-disabled');
    }

    // Set ARIA label and region role depending on player type
    this.el_.setAttribute('role', 'region');
    if (this.isAudio()) {
      this.el_.setAttribute('aria-label', this.localize('Audio Player'));
    } else {
      this.el_.setAttribute('aria-label', this.localize('Video Player'));
    }

    if (this.isAudio()) {
      this.addClass('vjs-audio');
    }

    // TODO: Make this smarter. Toggle user state between touching/mousing
    // using events, since devices can have both touch and mouse events.
    // TODO: Make this check be performed again when the window switches between monitors
    // (See https://github.com/videojs/video.js/issues/5683)
    if (browser.TOUCH_ENABLED) {
      this.addClass('vjs-touch-enabled');
    }

    // iOS Safari has broken hover handling
    if (!browser.IS_IOS) {
      this.addClass('vjs-workinghover');
    }

    // Make player easily findable by ID
    Player.players[this.id_] = this;

    // Add a major version class to aid css in plugins
    const majorVersion = version.split('.')[0];

    this.addClass(`vjs-v${majorVersion}`);

    // When the player is first initialized, trigger activity so components
    // like the control bar show themselves if needed
    this.userActive(true);
    this.reportUserActivity();

    this.one('play', (e) => this.listenForUserActivity_(e));
    this.on('keydown', (e) => this.handleKeyDown(e));
    this.on('languagechange', (e) => this.handleLanguagechange(e));

    this.breakpoints(this.options_.breakpoints);
    this.responsive(this.options_.responsive);

    // Calling both the audio mode methods after the player is fully
    // setup to be able to listen to the events triggered by them
    this.on('ready', () => {
      // Calling the audioPosterMode method first so that
      // the audioOnlyMode can take precedence when both options are set to true
      this.audioPosterMode(this.options_.audioPosterMode);
      this.audioOnlyMode(this.options_.audioOnlyMode);
    });
  }

  /**
   * Destroys the video player and does any necessary cleanup.
   *
   * This is especially helpful if you are dynamically adding and removing videos
   * to/from the DOM.
   *
   * @fires Player#dispose
   */
  dispose() {
    /**
     * Called when the player is being disposed of.
     *
     * @event Player#dispose
     * @type {Event}
     */
    this.trigger('dispose');
    // prevent dispose from being called twice
    this.off('dispose');

    // Make sure all player-specific document listeners are unbound. This is
    Events.off(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
    Events.off(document, 'keydown', this.boundFullWindowOnEscKey_);

    if (this.styleEl_ && this.styleEl_.parentNode) {
      this.styleEl_.parentNode.removeChild(this.styleEl_);
      this.styleEl_ = null;
    }

    // Kill reference to this player
    Player.players[this.id_] = null;

    if (this.tag && this.tag.player) {
      this.tag.player = null;
    }

    if (this.el_ && this.el_.player) {
      this.el_.player = null;
    }

    if (this.tech_) {
      this.tech_.dispose();
      this.isPosterFromTech_ = false;
      this.poster_ = '';
    }

    if (this.playerElIngest_) {
      this.playerElIngest_ = null;
    }

    if (this.tag) {
      this.tag = null;
    }

    middleware.clearCacheForPlayer(this);

    // remove all event handlers for track lists
    // all tracks and track listeners are removed on
    // tech dispose
    TRACK_TYPES.names.forEach((name) => {
      const props = TRACK_TYPES[name];
      const list = this[props.getterName]();

      // if it is not a native list
      // we have to manually remove event listeners
      if (list && list.off) {
        list.off();
      }
    });

    // the actual .el_ is removed here, or replaced if
    super.dispose({restoreEl: this.options_.restoreEl});
  }

  /**
   * Create the `Player`'s DOM element.
   *
   * @return {Element}
   *         The DOM element that gets created.
   */
  createEl() {
    let tag = this.tag;
    let el;
    let playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player');
    const divEmbed = this.tag.tagName.toLowerCase() === 'video-js';

    if (playerElIngest) {
      el = this.el_ = tag.parentNode;
    } else if (!divEmbed) {
      el = this.el_ = super.createEl('div');
    }

    // Copy over all the attributes from the tag, including ID and class
    // ID will now reference player box, not the video tag
    const attrs = Dom.getAttributes(tag);

    if (divEmbed) {
      el = this.el_ = tag;
      tag = this.tag = document.createElement('video');
      while (el.children.length) {
        tag.appendChild(el.firstChild);
      }

      if (!Dom.hasClass(el, 'video-js')) {
        Dom.addClass(el, 'video-js');
      }

      el.appendChild(tag);

      playerElIngest = this.playerElIngest_ = el;
      // move properties over from our custom `video-js` element
      // to our new `video` element. This will move things like
      // `src` or `controls` that were set via js before the player
      // was initialized.
      Object.keys(el).forEach((k) => {
        try {
          tag[k] = el[k];
        } catch (e) {
          // we got a a property like outerHTML which we can't actually copy, ignore it
        }
      });
    }

    // set tabindex to -1 to remove the video element from the focus order
    tag.setAttribute('tabindex', '-1');
    attrs.tabindex = '-1';

    // Workaround for #4583 on Chrome (on Windows) with JAWS.
    // See https://github.com/FreedomScientific/VFO-standards-support/issues/78
    // Note that we can't detect if JAWS is being used, but this ARIA attribute
    // doesn't change behavior of Chrome if JAWS is not being used
    if (IS_CHROME && IS_WINDOWS) {
      tag.setAttribute('role', 'application');
      attrs.role = 'application';
    }

    // Remove width/height attrs from tag so CSS can make it 100% width/height
    tag.removeAttribute('width');
    tag.removeAttribute('height');

    if ('width' in attrs) {
      delete attrs.width;
    }
    if ('height' in attrs) {
      delete attrs.height;
    }

    Object.getOwnPropertyNames(attrs).forEach(function(attr) {
      // don't copy over the class attribute to the player element when we're in a div embed
      // the class is already set up properly in the divEmbed case
      // and we want to make sure that the `video-js` class doesn't get lost
      if (!(divEmbed && attr === 'class')) {
        el.setAttribute(attr, attrs[attr]);
      }

      if (divEmbed) {
        tag.setAttribute(attr, attrs[attr]);
      }
    });

    // Update tag id/class for use as HTML5 playback tech
    // Might think we should do this after embedding in container so .vjs-tech class
    // doesn't flash 100% width/height, but class only applies with .video-js parent
    tag.playerId = tag.id;
    tag.id += '_html5_api';
    tag.className = 'vjs-tech';

    // Make player findable on elements
    tag.player = el.player = this;
    // Default state of video is paused
    this.addClass('vjs-paused');

    // Add a style element in the player that we'll use to set the width/height
    // of the player in a way that's still overridable by CSS, just like the
    // video element
    if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true) {
      this.styleEl_ = stylesheet.createStyleElement('vjs-styles-dimensions');
      const defaultsStyleEl = Dom.$('.vjs-styles-defaults');
      const head = Dom.$('head');

      head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild);
    }

    this.fill_ = false;
    this.fluid_ = false;

    // Pass in the width/height/aspectRatio options which will update the style el
    this.width(this.options_.width);
    this.height(this.options_.height);
    this.fill(this.options_.fill);
    this.fluid(this.options_.fluid);
    this.aspectRatio(this.options_.aspectRatio);
    // support both crossOrigin and crossorigin to reduce confusion and issues around the name
    this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin);

    // Hide any links within the video/audio tag,
    // because IE doesn't hide them completely from screen readers.
    const links = tag.getElementsByTagName('a');

    for (let i = 0; i < links.length; i++) {
      const linkEl = links.item(i);

      Dom.addClass(linkEl, 'vjs-hidden');
      linkEl.setAttribute('hidden', 'hidden');
    }

    // insertElFirst seems to cause the networkState to flicker from 3 to 2, so
    // keep track of the original for later so we can know if the source originally failed
    tag.initNetworkState_ = tag.networkState;

    // Wrap video tag in div (el/box) container
    if (tag.parentNode && !playerElIngest) {
      tag.parentNode.insertBefore(el, tag);
    }

    // insert the tag as the first child of the player element
    // then manually add it to the children array so that this.addChild
    // will work properly for other components
    //
    // Breaks iPhone, fixed in HTML5 setup.
    Dom.prependTo(tag, el);
    this.children_.unshift(tag);

    // Set lang attr on player to ensure CSS :lang() in consistent with player
    // if it's been set to something different to the doc
    this.el_.setAttribute('lang', this.language_);

    this.el_.setAttribute('translate', 'no');

    this.el_ = el;

    return el;
  }

  /**
   * Get or set the `Player`'s crossOrigin option. For the HTML5 player, this
   * sets the `crossOrigin` property on the `<video>` tag to control the CORS
   * behavior.
   *
   * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
   *
   * @param {string|null} [value]
   *        The value to set the `Player`'s crossOrigin to. If an argument is
   *        given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
   *
   * @return {string|null|undefined}
   *         - The current crossOrigin value of the `Player` when getting.
   *         - undefined when setting
   */
  crossOrigin(value) {
    // `null` can be set to unset a value
    if (typeof value === 'undefined') {
      return this.techGet_('crossOrigin');
    }

    if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
      log.warn(`crossOrigin must be null,  "anonymous" or "use-credentials", given "${value}"`);
      return;
    }

    this.techCall_('setCrossOrigin', value);
    if (this.posterImage) {
      this.posterImage.crossOrigin(value);
    }

    return;
  }

  /**
   * A getter/setter for the `Player`'s width. Returns the player's configured value.
   * To get the current width use `currentWidth()`.
   *
   * @param {number} [value]
   *        The value to set the `Player`'s width to.
   *
   * @return {number}
   *         The current width of the `Player` when getting.
   */
  width(value) {
    return this.dimension('width', value);
  }

  /**
   * A getter/setter for the `Player`'s height. Returns the player's configured value.
   * To get the current height use `currentheight()`.
   *
   * @param {number} [value]
   *        The value to set the `Player`'s height to.
   *
   * @return {number}
   *         The current height of the `Player` when getting.
   */
  height(value) {
    return this.dimension('height', value);
  }

  /**
   * A getter/setter for the `Player`'s width & height.
   *
   * @param {string} dimension
   *        This string can be:
   *        - 'width'
   *        - 'height'
   *
   * @param {number} [value]
   *        Value for dimension specified in the first argument.
   *
   * @return {number}
   *         The dimension arguments value when getting (width/height).
   */
  dimension(dimension, value) {
    const privDimension = dimension + '_';

    if (value === undefined) {
      return this[privDimension] || 0;
    }

    if (value === '' || value === 'auto') {
      // If an empty string is given, reset the dimension to be automatic
      this[privDimension] = undefined;
      this.updateStyleEl_();
      return;
    }

    const parsedVal = parseFloat(value);

    if (isNaN(parsedVal)) {
      log.error(`Improper value "${value}" supplied for for ${dimension}`);
      return;
    }

    this[privDimension] = parsedVal;
    this.updateStyleEl_();
  }

  /**
   * A getter/setter/toggler for the vjs-fluid `className` on the `Player`.
   *
   * Turning this on will turn off fill mode.
   *
   * @param {boolean} [bool]
   *        - A value of true adds the class.
   *        - A value of false removes the class.
   *        - No value will be a getter.
   *
   * @return {boolean|undefined}
   *         - The value of fluid when getting.
   *         - `undefined` when setting.
   */
  fluid(bool) {
    if (bool === undefined) {
      return !!this.fluid_;
    }

    this.fluid_ = !!bool;

    if (isEvented(this)) {
      this.off(['playerreset', 'resize'], this.boundUpdateStyleEl_);
    }
    if (bool) {
      this.addClass('vjs-fluid');
      this.fill(false);
      addEventedCallback(this, () => {
        this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
      });
    } else {
      this.removeClass('vjs-fluid');
    }

    this.updateStyleEl_();
  }

  /**
   * A getter/setter/toggler for the vjs-fill `className` on the `Player`.
   *
   * Turning this on will turn off fluid mode.
   *
   * @param {boolean} [bool]
   *        - A value of true adds the class.
   *        - A value of false removes the class.
   *        - No value will be a getter.
   *
   * @return {boolean|undefined}
   *         - The value of fluid when getting.
   *         - `undefined` when setting.
   */
  fill(bool) {
    if (bool === undefined) {
      return !!this.fill_;
    }

    this.fill_ = !!bool;

    if (bool) {
      this.addClass('vjs-fill');
      this.fluid(false);
    } else {
      this.removeClass('vjs-fill');
    }
  }

  /**
   * Get/Set the aspect ratio
   *
   * @param {string} [ratio]
   *        Aspect ratio for player
   *
   * @return {string|undefined}
   *         returns the current aspect ratio when getting
   */

  /**
   * A getter/setter for the `Player`'s aspect ratio.
   *
   * @param {string} [ratio]
   *        The value to set the `Player`'s aspect ratio to.
   *
   * @return {string|undefined}
   *         - The current aspect ratio of the `Player` when getting.
   *         - undefined when setting
   */
  aspectRatio(ratio) {
    if (ratio === undefined) {
      return this.aspectRatio_;
    }

    // Check for width:height format
    if (!(/^\d+\:\d+$/).test(ratio)) {
      throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.');
    }
    this.aspectRatio_ = ratio;

    // We're assuming if you set an aspect ratio you want fluid mode,
    // because in fixed mode you could calculate width and height yourself.
    this.fluid(true);

    this.updateStyleEl_();
  }

  /**
   * Update styles of the `Player` element (height, width and aspect ratio).
   *
   * @private
   * @listens Tech#loadedmetadata
   */
  updateStyleEl_() {
    if (window.VIDEOJS_NO_DYNAMIC_STYLE === true) {
      const width = typeof this.width_ === 'number' ? this.width_ : this.options_.width;
      const height = typeof this.height_ === 'number' ? this.height_ : this.options_.height;
      const techEl = this.tech_ && this.tech_.el();

      if (techEl) {
        if (width >= 0) {
          techEl.width = width;
        }
        if (height >= 0) {
          techEl.height = height;
        }
      }

      return;
    }

    let width;
    let height;
    let aspectRatio;
    let idClass;

    // The aspect ratio is either used directly or to calculate width and height.
    if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') {
      // Use any aspectRatio that's been specifically set
      aspectRatio = this.aspectRatio_;
    } else if (this.videoWidth() > 0) {
      // Otherwise try to get the aspect ratio from the video metadata
      aspectRatio = this.videoWidth() + ':' + this.videoHeight();
    } else {
      // Or use a default. The video element's is 2:1, but 16:9 is more common.
      aspectRatio = '16:9';
    }

    // Get the ratio as a decimal we can use to calculate dimensions
    const ratioParts = aspectRatio.split(':');
    const ratioMultiplier = ratioParts[1] / ratioParts[0];

    if (this.width_ !== undefined) {
      // Use any width that's been specifically set
      width = this.width_;
    } else if (this.height_ !== undefined) {
      // Or calculate the width from the aspect ratio if a height has been set
      width = this.height_ / ratioMultiplier;
    } else {
      // Or use the video's metadata, or use the video el's default of 300
      width = this.videoWidth() || 300;
    }

    if (this.height_ !== undefined) {
      // Use any height that's been specifically set
      height = this.height_;
    } else {
      // Otherwise calculate the height from the ratio and the width
      height = width * ratioMultiplier;
    }

    // Ensure the CSS class is valid by starting with an alpha character
    if ((/^[^a-zA-Z]/).test(this.id())) {
      idClass = 'dimensions-' + this.id();
    } else {
      idClass = this.id() + '-dimensions';
    }

    // Ensure the right class is still on the player for the style element
    this.addClass(idClass);

    stylesheet.setTextContent(this.styleEl_, `
      .${idClass} {
        width: ${width}px;
        height: ${height}px;
      }

      .${idClass}.vjs-fluid:not(.vjs-audio-only-mode) {
        padding-top: ${ratioMultiplier * 100}%;
      }
    `);
  }

  /**
   * Load/Create an instance of playback {@link Tech} including element
   * and API methods. Then append the `Tech` element in `Player` as a child.
   *
   * @param {string} techName
   *        name of the playback technology
   *
   * @param {string} source
   *        video source
   *
   * @private
   */
  loadTech_(techName, source) {

    // Pause and remove current playback technology
    if (this.tech_) {
      this.unloadTech_();
    }

    const titleTechName = toTitleCase(techName);
    const camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1);

    // get rid of the HTML5 video tag as soon as we are using another tech
    if (titleTechName !== 'Html5' && this.tag) {
      Tech.getTech('Html5').disposeMediaElement(this.tag);
      this.tag.player = null;
      this.tag = null;
    }

    this.techName_ = titleTechName;

    // Turn off API access because we're loading a new tech that might load asynchronously
    this.isReady_ = false;

    let autoplay = this.autoplay();

    // if autoplay is a string (or `true` with normalizeAutoplay: true) we pass false to the tech
    // because the player is going to handle autoplay on `loadstart`
    if (typeof this.autoplay() === 'string' || this.autoplay() === true && this.options_.normalizeAutoplay) {
      autoplay = false;
    }

    // Grab tech-specific options from player options and add source and parent element to use.
    const techOptions = {
      source,
      autoplay,
      'nativeControlsForTouch': this.options_.nativeControlsForTouch,
      'playerId': this.id(),
      'techId': `${this.id()}_${camelTechName}_api`,
      'playsinline': this.options_.playsinline,
      'preload': this.options_.preload,
      'loop': this.options_.loop,
      'disablePictureInPicture': this.options_.disablePictureInPicture,
      'muted': this.options_.muted,
      'poster': this.poster(),
      'language': this.language(),
      'playerElIngest': this.playerElIngest_ || false,
      'vtt.js': this.options_['vtt.js'],
      'canOverridePoster': !!this.options_.techCanOverridePoster,
      'enableSourceset': this.options_.enableSourceset
    };

    TRACK_TYPES.names.forEach((name) => {
      const props = TRACK_TYPES[name];

      techOptions[props.getterName] = this[props.privateName];
    });

    Object.assign(techOptions, this.options_[titleTechName]);
    Object.assign(techOptions, this.options_[camelTechName]);
    Object.assign(techOptions, this.options_[techName.toLowerCase()]);

    if (this.tag) {
      techOptions.tag = this.tag;
    }

    if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) {
      techOptions.startTime = this.cache_.currentTime;
    }

    // Initialize tech instance
    const TechClass = Tech.getTech(techName);

    if (!TechClass) {
      throw new Error(`No Tech named '${titleTechName}' exists! '${titleTechName}' should be registered using videojs.registerTech()'`);
    }

    this.tech_ = new TechClass(techOptions);

    // player.triggerReady is always async, so don't need this to be async
    this.tech_.ready(Fn.bind_(this, this.handleTechReady_), true);

    textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_);

    // Listen to all HTML5-defined events and trigger them on the player
    TECH_EVENTS_RETRIGGER.forEach((event) => {
      this.on(this.tech_, event, (e) => this[`handleTech${toTitleCase(event)}_`](e));
    });

    Object.keys(TECH_EVENTS_QUEUE).forEach((event) => {
      this.on(this.tech_, event, (eventObj) => {
        if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) {
          this.queuedCallbacks_.push({
            callback: this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this),
            event: eventObj
          });
          return;
        }
        this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj);
      });
    });

    this.on(this.tech_, 'loadstart', (e) => this.handleTechLoadStart_(e));
    this.on(this.tech_, 'sourceset', (e) => this.handleTechSourceset_(e));
    this.on(this.tech_, 'waiting', (e) => this.handleTechWaiting_(e));
    this.on(this.tech_, 'ended', (e) => this.handleTechEnded_(e));
    this.on(this.tech_, 'seeking', (e) => this.handleTechSeeking_(e));
    this.on(this.tech_, 'play', (e) => this.handleTechPlay_(e));
    this.on(this.tech_, 'pause', (e) => this.handleTechPause_(e));
    this.on(this.tech_, 'durationchange', (e) => this.handleTechDurationChange_(e));
    this.on(this.tech_, 'fullscreenchange', (e, data) => this.handleTechFullscreenChange_(e, data));
    this.on(this.tech_, 'fullscreenerror', (e, err) => this.handleTechFullscreenError_(e, err));
    this.on(this.tech_, 'enterpictureinpicture', (e) => this.handleTechEnterPictureInPicture_(e));
    this.on(this.tech_, 'leavepictureinpicture', (e) => this.handleTechLeavePictureInPicture_(e));
    this.on(this.tech_, 'error', (e) => this.handleTechError_(e));
    this.on(this.tech_, 'posterchange', (e) => this.handleTechPosterChange_(e));
    this.on(this.tech_, 'textdata', (e) => this.handleTechTextData_(e));
    this.on(this.tech_, 'ratechange', (e) => this.handleTechRateChange_(e));
    this.on(this.tech_, 'loadedmetadata', this.boundUpdateStyleEl_);

    this.usingNativeControls(this.techGet_('controls'));

    if (this.controls() && !this.usingNativeControls()) {
      this.addTechControlsListeners_();
    }

    // Add the tech element in the DOM if it was not already there
    // Make sure to not insert the original video element if using Html5
    if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) {
      Dom.prependTo(this.tech_.el(), this.el());
    }

    // Get rid of the original video tag reference after the first tech is loaded
    if (this.tag) {
      this.tag.player = null;
      this.tag = null;
    }
  }

  /**
   * Unload and dispose of the current playback {@link Tech}.
   *
   * @private
   */
  unloadTech_() {
    // Save the current text tracks so that we can reuse the same text tracks with the next tech
    TRACK_TYPES.names.forEach((name) => {
      const props = TRACK_TYPES[name];

      this[props.privateName] = this[props.getterName]();
    });
    this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_);

    this.isReady_ = false;

    this.tech_.dispose();

    this.tech_ = false;

    if (this.isPosterFromTech_) {
      this.poster_ = '';
      this.trigger('posterchange');
    }

    this.isPosterFromTech_ = false;
  }

  /**
   * Return a reference to the current {@link Tech}.
   * It will print a warning by default about the danger of using the tech directly
   * but any argument that is passed in will silence the warning.
   *
   * @param {*} [safety]
   *        Anything passed in to silence the warning
   *
   * @return {Tech}
   *         The Tech
   */
  tech(safety) {
    if (safety === undefined) {
      log.warn('Using the tech directly can be dangerous. I hope you know what you\'re doing.\n' +
        'See https://github.com/videojs/video.js/issues/2617 for more info.\n');
    }

    return this.tech_;
  }

  /**
   * Set up click and touch listeners for the playback element
   *
   * - On desktops: a click on the video itself will toggle playback
   * - On mobile devices: a click on the video toggles controls
   *   which is done by toggling the user state between active and
   *   inactive
   * - A tap can signal that a user has become active or has become inactive
   *   e.g. a quick tap on an iPhone movie should reveal the controls. Another
   *   quick tap should hide them again (signaling the user is in an inactive
   *   viewing state)
   * - In addition to this, we still want the user to be considered inactive after
   *   a few seconds of inactivity.
   *
   * > Note: the only part of iOS interaction we can't mimic with this setup
   * is a touch and hold on the video element counting as activity in order to
   * keep the controls showing, but that shouldn't be an issue. A touch and hold
   * on any controls will still keep the user active
   *
   * @private
   */
  addTechControlsListeners_() {
    // Make sure to remove all the previous listeners in case we are called multiple times.
    this.removeTechControlsListeners_();

    this.on(this.tech_, 'click', this.boundHandleTechClick_);
    this.on(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);

    // If the controls were hidden we don't want that to change without a tap event
    // so we'll check if the controls were already showing before reporting user
    // activity
    this.on(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
    this.on(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
    this.on(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);

    // The tap listener needs to come after the touchend listener because the tap
    // listener cancels out any reportedUserActivity when setting userActive(false)
    this.on(this.tech_, 'tap', this.boundHandleTechTap_);
  }

  /**
   * Remove the listeners used for click and tap controls. This is needed for
   * toggling to controls disabled, where a tap/touch should do nothing.
   *
   * @private
   */
  removeTechControlsListeners_() {
    // We don't want to just use `this.off()` because there might be other needed
    // listeners added by techs that extend this.
    this.off(this.tech_, 'tap', this.boundHandleTechTap_);
    this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
    this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
    this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
    this.off(this.tech_, 'click', this.boundHandleTechClick_);
    this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
  }

  /**
   * Player waits for the tech to be ready
   *
   * @private
   */
  handleTechReady_() {
    this.triggerReady();

    // Keep the same volume as before
    if (this.cache_.volume) {
      this.techCall_('setVolume', this.cache_.volume);
    }

    // Look if the tech found a higher resolution poster while loading
    this.handleTechPosterChange_();

    // Update the duration if available
    this.handleTechDurationChange_();
  }

  /**
   * Retrigger the `loadstart` event that was triggered by the {@link Tech}.
   *
   * @fires Player#loadstart
   * @listens Tech#loadstart
   * @private
   */
  handleTechLoadStart_() {
    // TODO: Update to use `emptied` event instead. See #1277.

    this.removeClass('vjs-ended', 'vjs-seeking');

    // reset the error state
    this.error(null);

    // Update the duration
    this.handleTechDurationChange_();

    if (!this.paused()) {
      /**
       * Fired when the user agent begins looking for media data
       *
       * @event Player#loadstart
       * @type {Event}
       */
      this.trigger('loadstart');
    } else {
      // reset the hasStarted state
      this.hasStarted(false);
      this.trigger('loadstart');
    }

    // autoplay happens after loadstart for the browser,
    // so we mimic that behavior
    this.manualAutoplay_(this.autoplay() === true && this.options_.normalizeAutoplay ? 'play' : this.autoplay());
  }

  /**
   * Handle autoplay string values, rather than the typical boolean
   * values that should be handled by the tech. Note that this is not
   * part of any specification. Valid values and what they do can be
   * found on the autoplay getter at Player#autoplay()
   */
  manualAutoplay_(type) {
    if (!this.tech_ || typeof type !== 'string') {
      return;
    }

    // Save original muted() value, set muted to true, and attempt to play().
    // On promise rejection, restore muted from saved value
    const resolveMuted = () => {
      const previouslyMuted = this.muted();

      this.muted(true);

      const restoreMuted = () => {
        this.muted(previouslyMuted);
      };

      // restore muted on play terminatation
      this.playTerminatedQueue_.push(restoreMuted);

      const mutedPromise = this.play();

      if (!isPromise(mutedPromise)) {
        return;
      }

      return mutedPromise.catch(err => {
        restoreMuted();
        throw new Error(`Rejection at manualAutoplay. Restoring muted value. ${err ? err : ''}`);
      });
    };

    let promise;

    // if muted defaults to true
    // the only thing we can do is call play
    if (type === 'any' && !this.muted()) {
      promise = this.play();

      if (isPromise(promise)) {
        promise = promise.catch(resolveMuted);
      }
    } else if (type === 'muted' && !this.muted()) {
      promise = resolveMuted();
    } else {
      promise = this.play();
    }

    if (!isPromise(promise)) {
      return;
    }

    return promise.then(() => {
      this.trigger({type: 'autoplay-success', autoplay: type});
    }).catch(() => {
      this.trigger({type: 'autoplay-failure', autoplay: type});
    });
  }

  /**
   * Update the internal source caches so that we return the correct source from
   * `src()`, `currentSource()`, and `currentSources()`.
   *
   * > Note: `currentSources` will not be updated if the source that is passed in exists
   *         in the current `currentSources` cache.
   *
   *
   * @param {Tech~SourceObject} srcObj
   *        A string or object source to update our caches to.
   */
  updateSourceCaches_(srcObj = '') {

    let src = srcObj;
    let type = '';

    if (typeof src !== 'string') {
      src = srcObj.src;
      type = srcObj.type;
    }

    // make sure all the caches are set to default values
    // to prevent null checking
    this.cache_.source = this.cache_.source || {};
    this.cache_.sources = this.cache_.sources || [];

    // try to get the type of the src that was passed in
    if (src && !type) {
      type = findMimetype(this, src);
    }

    // update `currentSource` cache always
    this.cache_.source = merge({}, srcObj, {src, type});

    const matchingSources = this.cache_.sources.filter((s) => s.src && s.src === src);
    const sourceElSources = [];
    const sourceEls = this.$$('source');
    const matchingSourceEls = [];

    for (let i = 0; i < sourceEls.length; i++) {
      const sourceObj = Dom.getAttributes(sourceEls[i]);

      sourceElSources.push(sourceObj);

      if (sourceObj.src && sourceObj.src === src) {
        matchingSourceEls.push(sourceObj.src);
      }
    }

    // if we have matching source els but not matching sources
    // the current source cache is not up to date
    if (matchingSourceEls.length && !matchingSources.length) {
      this.cache_.sources = sourceElSources;
    // if we don't have matching source or source els set the
    // sources cache to the `currentSource` cache
    } else if (!matchingSources.length) {
      this.cache_.sources = [this.cache_.source];
    }

    // update the tech `src` cache
    this.cache_.src = src;
  }

  /**
   * *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech}
   * causing the media element to reload.
   *
   * It will fire for the initial source and each subsequent source.
   * This event is a custom event from Video.js and is triggered by the {@link Tech}.
   *
   * The event object for this event contains a `src` property that will contain the source
   * that was available when the event was triggered. This is generally only necessary if Video.js
   * is switching techs while the source was being changed.
   *
   * It is also fired when `load` is called on the player (or media element)
   * because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`}
   * says that the resource selection algorithm needs to be aborted and restarted.
   * In this case, it is very likely that the `src` property will be set to the
   * empty string `""` to indicate we do not know what the source will be but
   * that it is changing.
   *
   * *This event is currently still experimental and may change in minor releases.*
   * __To use this, pass `enableSourceset` option to the player.__
   *
   * @event Player#sourceset
   * @type {Event}
   * @prop {string} src
   *                The source url available when the `sourceset` was triggered.
   *                It will be an empty string if we cannot know what the source is
   *                but know that the source will change.
   */
  /**
   * Retrigger the `sourceset` event that was triggered by the {@link Tech}.
   *
   * @fires Player#sourceset
   * @listens Tech#sourceset
   * @private
   */
  handleTechSourceset_(event) {
    // only update the source cache when the source
    // was not updated using the player api
    if (!this.changingSrc_) {
      let updateSourceCaches = (src) => this.updateSourceCaches_(src);
      const playerSrc = this.currentSource().src;
      const eventSrc = event.src;

      // if we have a playerSrc that is not a blob, and a tech src that is a blob
      if (playerSrc && !(/^blob:/).test(playerSrc) && (/^blob:/).test(eventSrc)) {

        // if both the tech source and the player source were updated we assume
        // something like @videojs/http-streaming did the sourceset and skip updating the source cache.
        if (!this.lastSource_ || (this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc)) {
          updateSourceCaches = () => {};
        }
      }

      // update the source to the initial source right away
      // in some cases this will be empty string
      updateSourceCaches(eventSrc);

      // if the `sourceset` `src` was an empty string
      // wait for a `loadstart` to update the cache to `currentSrc`.
      // If a sourceset happens before a `loadstart`, we reset the state
      if (!event.src) {
        this.tech_.any(['sourceset', 'loadstart'], (e) => {
          // if a sourceset happens before a `loadstart` there
          // is nothing to do as this `handleTechSourceset_`
          // will be called again and this will be handled there.
          if (e.type === 'sourceset') {
            return;
          }

          const techSrc = this.techGet('currentSrc');

          this.lastSource_.tech = techSrc;
          this.updateSourceCaches_(techSrc);
        });
      }
    }
    this.lastSource_ = {player: this.currentSource().src, tech: event.src};

    this.trigger({
      src: event.src,
      type: 'sourceset'
    });
  }

  /**
   * Add/remove the vjs-has-started class
   *
   *
   * @param {boolean} request
   *        - true: adds the class
   *        - false: remove the class
   *
   * @return {boolean}
   *         the boolean value of hasStarted_
   */
  hasStarted(request) {
    if (request === undefined) {
      // act as getter, if we have no request to change
      return this.hasStarted_;
    }

    if (request === this.hasStarted_) {
      return;
    }

    this.hasStarted_ = request;

    if (this.hasStarted_) {
      this.addClass('vjs-has-started');
    } else {
      this.removeClass('vjs-has-started');
    }
  }

  /**
   * Fired whenever the media begins or resumes playback
   *
   * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play}
   * @fires Player#play
   * @listens Tech#play
   * @private
   */
  handleTechPlay_() {
    this.removeClass('vjs-ended', 'vjs-paused');
    this.addClass('vjs-playing');

    // hide the poster when the user hits play
    this.hasStarted(true);
    /**
     * Triggered whenever an {@link Tech#play} event happens. Indicates that
     * playback has started or resumed.
     *
     * @event Player#play
     * @type {Event}
     */
    this.trigger('play');
  }

  /**
   * Retrigger the `ratechange` event that was triggered by the {@link Tech}.
   *
   * If there were any events queued while the playback rate was zero, fire
   * those events now.
   *
   * @private
   * @method Player#handleTechRateChange_
   * @fires Player#ratechange
   * @listens Tech#ratechange
   */
  handleTechRateChange_() {
    if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) {
      this.queuedCallbacks_.forEach((queued) => queued.callback(queued.event));
      this.queuedCallbacks_ = [];
    }
    this.cache_.lastPlaybackRate = this.tech_.playbackRate();
    /**
     * Fires when the playing speed of the audio/video is changed
     *
     * @event Player#ratechange
     * @type {event}
     */
    this.trigger('ratechange');
  }

  /**
   * Retrigger the `waiting` event that was triggered by the {@link Tech}.
   *
   * @fires Player#waiting
   * @listens Tech#waiting
   * @private
   */
  handleTechWaiting_() {
    this.addClass('vjs-waiting');
    /**
     * A readyState change on the DOM element has caused playback to stop.
     *
     * @event Player#waiting
     * @type {Event}
     */
    this.trigger('waiting');

    // Browsers may emit a timeupdate event after a waiting event. In order to prevent
    // premature removal of the waiting class, wait for the time to change.
    const timeWhenWaiting = this.currentTime();
    const timeUpdateListener = () => {
      if (timeWhenWaiting !== this.currentTime()) {
        this.removeClass('vjs-waiting');
        this.off('timeupdate', timeUpdateListener);
      }
    };

    this.on('timeupdate', timeUpdateListener);
  }

  /**
   * Retrigger the `canplay` event that was triggered by the {@link Tech}.
   * > Note: This is not consistent between browsers. See #1351
   *
   * @fires Player#canplay
   * @listens Tech#canplay
   * @private
   */
  handleTechCanPlay_() {
    this.removeClass('vjs-waiting');
    /**
     * The media has a readyState of HAVE_FUTURE_DATA or greater.
     *
     * @event Player#canplay
     * @type {Event}
     */
    this.trigger('canplay');
  }

  /**
   * Retrigger the `canplaythrough` event that was triggered by the {@link Tech}.
   *
   * @fires Player#canplaythrough
   * @listens Tech#canplaythrough
   * @private
   */
  handleTechCanPlayThrough_() {
    this.removeClass('vjs-waiting');
    /**
     * The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the
     * entire media file can be played without buffering.
     *
     * @event Player#canplaythrough
     * @type {Event}
     */
    this.trigger('canplaythrough');
  }

  /**
   * Retrigger the `playing` event that was triggered by the {@link Tech}.
   *
   * @fires Player#playing
   * @listens Tech#playing
   * @private
   */
  handleTechPlaying_() {
    this.removeClass('vjs-waiting');
    /**
     * The media is no longer blocked from playback, and has started playing.
     *
     * @event Player#playing
     * @type {Event}
     */
    this.trigger('playing');
  }

  /**
   * Retrigger the `seeking` event that was triggered by the {@link Tech}.
   *
   * @fires Player#seeking
   * @listens Tech#seeking
   * @private
   */
  handleTechSeeking_() {
    this.addClass('vjs-seeking');
    /**
     * Fired whenever the player is jumping to a new time
     *
     * @event Player#seeking
     * @type {Event}
     */
    this.trigger('seeking');
  }

  /**
   * Retrigger the `seeked` event that was triggered by the {@link Tech}.
   *
   * @fires Player#seeked
   * @listens Tech#seeked
   * @private
   */
  handleTechSeeked_() {
    this.removeClass('vjs-seeking', 'vjs-ended');
    /**
     * Fired when the player has finished jumping to a new time
     *
     * @event Player#seeked
     * @type {Event}
     */
    this.trigger('seeked');
  }

  /**
   * Retrigger the `pause` event that was triggered by the {@link Tech}.
   *
   * @fires Player#pause
   * @listens Tech#pause
   * @private
   */
  handleTechPause_() {
    this.removeClass('vjs-playing');
    this.addClass('vjs-paused');
    /**
     * Fired whenever the media has been paused
     *
     * @event Player#pause
     * @type {Event}
     */
    this.trigger('pause');
  }

  /**
   * Retrigger the `ended` event that was triggered by the {@link Tech}.
   *
   * @fires Player#ended
   * @listens Tech#ended
   * @private
   */
  handleTechEnded_() {
    this.addClass('vjs-ended');
    this.removeClass('vjs-waiting');
    if (this.options_.loop) {
      this.currentTime(0);
      this.play();
    } else if (!this.paused()) {
      this.pause();
    }

    /**
     * Fired when the end of the media resource is reached (currentTime == duration)
     *
     * @event Player#ended
     * @type {Event}
     */
    this.trigger('ended');
  }

  /**
   * Fired when the duration of the media resource is first known or changed
   *
   * @listens Tech#durationchange
   * @private
   */
  handleTechDurationChange_() {
    this.duration(this.techGet_('duration'));
  }

  /**
   * Handle a click on the media element to play/pause
   *
   * @param {Event} event
   *        the event that caused this function to trigger
   *
   * @listens Tech#click
   * @private
   */
  handleTechClick_(event) {
    // When controls are disabled a click should not toggle playback because
    // the click is considered a control
    if (!this.controls_) {
      return;
    }

    if (
      this.options_ === undefined ||
      this.options_.userActions === undefined ||
      this.options_.userActions.click === undefined ||
      this.options_.userActions.click !== false
    ) {

      if (
        this.options_ !== undefined &&
        this.options_.userActions !== undefined &&
        typeof this.options_.userActions.click === 'function'
      ) {

        this.options_.userActions.click.call(this, event);

      } else if (this.paused()) {
        silencePromise(this.play());
      } else {
        this.pause();
      }
    }
  }

  /**
   * Handle a double-click on the media element to enter/exit fullscreen
   *
   * @param {Event} event
   *        the event that caused this function to trigger
   *
   * @listens Tech#dblclick
   * @private
   */
  handleTechDoubleClick_(event) {
    if (!this.controls_) {
      return;
    }

    // we do not want to toggle fullscreen state
    // when double-clicking inside a control bar or a modal
    const inAllowedEls = Array.prototype.some.call(
      this.$$('.vjs-control-bar, .vjs-modal-dialog'),
      el => el.contains(event.target)
    );

    if (!inAllowedEls) {
      /*
       * options.userActions.doubleClick
       *
       * If `undefined` or `true`, double-click toggles fullscreen if controls are present
       * Set to `false` to disable double-click handling
       * Set to a function to substitute an external double-click handler
       */
      if (
        this.options_ === undefined ||
        this.options_.userActions === undefined ||
        this.options_.userActions.doubleClick === undefined ||
        this.options_.userActions.doubleClick !== false
      ) {

        if (
          this.options_ !== undefined &&
          this.options_.userActions !== undefined &&
          typeof this.options_.userActions.doubleClick === 'function'
        ) {

          this.options_.userActions.doubleClick.call(this, event);

        } else if (this.isFullscreen()) {
          this.exitFullscreen();
        } else {
          this.requestFullscreen();
        }
      }
    }
  }

  /**
   * Handle a tap on the media element. It will toggle the user
   * activity state, which hides and shows the controls.
   *
   * @listens Tech#tap
   * @private
   */
  handleTechTap_() {
    this.userActive(!this.userActive());
  }

  /**
   * Handle touch to start
   *
   * @listens Tech#touchstart
   * @private
   */
  handleTechTouchStart_() {
    this.userWasActive = this.userActive();
  }

  /**
   * Handle touch to move
   *
   * @listens Tech#touchmove
   * @private
   */
  handleTechTouchMove_() {
    if (this.userWasActive) {
      this.reportUserActivity();
    }
  }

  /**
   * Handle touch to end
   *
   * @param {Event} event
   *        the touchend event that triggered
   *        this function
   *
   * @listens Tech#touchend
   * @private
   */
  handleTechTouchEnd_(event) {
    // Stop the mouse events from also happening
    if (event.cancelable) {
      event.preventDefault();
    }
  }

  /**
   * @private
   */
  toggleFullscreenClass_() {
    if (this.isFullscreen()) {
      this.addClass('vjs-fullscreen');
    } else {
      this.removeClass('vjs-fullscreen');
    }
  }

  /**
   * when the document fschange event triggers it calls this
   */
  documentFullscreenChange_(e) {
    const targetPlayer = e.target.player;

    // if another player was fullscreen
    // do a null check for targetPlayer because older firefox's would put document as e.target
    if (targetPlayer && targetPlayer !== this) {
      return;
    }

    const el = this.el();
    let isFs = document[this.fsApi_.fullscreenElement] === el;

    if (!isFs && el.matches) {
      isFs = el.matches(':' + this.fsApi_.fullscreen);
    } else if (!isFs && el.msMatchesSelector) {
      isFs = el.msMatchesSelector(':' + this.fsApi_.fullscreen);
    }

    this.isFullscreen(isFs);
  }

  /**
   * Handle Tech Fullscreen Change
   *
   * @param {Event} event
   *        the fullscreenchange event that triggered this function
   *
   * @param {Object} data
   *        the data that was sent with the event
   *
   * @private
   * @listens Tech#fullscreenchange
   * @fires Player#fullscreenchange
   */
  handleTechFullscreenChange_(event, data) {
    if (data) {
      if (data.nativeIOSFullscreen) {
        this.addClass('vjs-ios-native-fs');
        this.tech_.one('webkitendfullscreen', () => {
          this.removeClass('vjs-ios-native-fs');
        });
      }
      this.isFullscreen(data.isFullscreen);
    }
  }

  handleTechFullscreenError_(event, err) {
    this.trigger('fullscreenerror', err);
  }

  /**
   * @private
   */
  togglePictureInPictureClass_() {
    if (this.isInPictureInPicture()) {
      this.addClass('vjs-picture-in-picture');
    } else {
      this.removeClass('vjs-picture-in-picture');
    }
  }

  /**
   * Handle Tech Enter Picture-in-Picture.
   *
   * @param {Event} event
   *        the enterpictureinpicture event that triggered this function
   *
   * @private
   * @listens Tech#enterpictureinpicture
   */
  handleTechEnterPictureInPicture_(event) {
    this.isInPictureInPicture(true);
  }

  /**
   * Handle Tech Leave Picture-in-Picture.
   *
   * @param {Event} event
   *        the leavepictureinpicture event that triggered this function
   *
   * @private
   * @listens Tech#leavepictureinpicture
   */
  handleTechLeavePictureInPicture_(event) {
    this.isInPictureInPicture(false);
  }

  /**
   * Fires when an error occurred during the loading of an audio/video.
   *
   * @private
   * @listens Tech#error
   */
  handleTechError_() {
    const error = this.tech_.error();

    this.error(error);
  }

  /**
   * Retrigger the `textdata` event that was triggered by the {@link Tech}.
   *
   * @fires Player#textdata
   * @listens Tech#textdata
   * @private
   */
  handleTechTextData_() {
    let data = null;

    if (arguments.length > 1) {
      data = arguments[1];
    }

    /**
     * Fires when we get a textdata event from tech
     *
     * @event Player#textdata
     * @type {Event}
     */
    this.trigger('textdata', data);
  }

  /**
   * Get object for cached values.
   *
   * @return {Object}
   *         get the current object cache
   */
  getCache() {
    return this.cache_;
  }

  /**
   * Resets the internal cache object.
   *
   * Using this function outside the player constructor or reset method may
   * have unintended side-effects.
   *
   * @private
   */
  resetCache_() {
    this.cache_ = {

      // Right now, the currentTime is not _really_ cached because it is always
      // retrieved from the tech (see: currentTime). However, for completeness,
      // we set it to zero here to ensure that if we do start actually caching
      // it, we reset it along with everything else.
      currentTime: 0,
      initTime: 0,
      inactivityTimeout: this.options_.inactivityTimeout,
      duration: NaN,
      lastVolume: 1,
      lastPlaybackRate: this.defaultPlaybackRate(),
      media: null,
      src: '',
      source: {},
      sources: [],
      playbackRates: [],
      volume: 1
    };
  }

  /**
   * Pass values to the playback tech
   *
   * @param {string} [method]
   *        the method to call
   *
   * @param {Object} arg
   *        the argument to pass
   *
   * @private
   */
  techCall_(method, arg) {
    // If it's not ready yet, call method when it is

    this.ready(function() {
      if (method in middleware.allowedSetters) {
        return middleware.set(this.middleware_, this.tech_, method, arg);

      } else if (method in middleware.allowedMediators) {
        return middleware.mediate(this.middleware_, this.tech_, method, arg);
      }

      try {
        if (this.tech_) {
          this.tech_[method](arg);
        }
      } catch (e) {
        log(e);
        throw e;
      }
    }, true);
  }

  /**
   * Mediate attempt to call playback tech method
   * and return the value of the method called.
   *
   * @param {string} method
   *        Tech method
   *
   * @return {*}
   *         Value returned by the tech method called, undefined if tech
   *         is not ready or tech method is not present
   *
   * @private
   */
  techGet_(method) {
    if (!this.tech_ || !this.tech_.isReady_) {
      return;
    }

    if (method in middleware.allowedGetters) {
      return middleware.get(this.middleware_, this.tech_, method);

    } else if (method in middleware.allowedMediators) {
      return middleware.mediate(this.middleware_, this.tech_, method);
    }

    // Log error when playback tech object is present but method
    // is undefined or unavailable
    try {
      return this.tech_[method]();
    } catch (e) {

      // When building additional tech libs, an expected method may not be defined yet
      if (this.tech_[method] === undefined) {
        log(`Video.js: ${method} method not defined for ${this.techName_} playback technology.`, e);
        throw e;
      }

      // When a method isn't available on the object it throws a TypeError
      if (e.name === 'TypeError') {
        log(`Video.js: ${method} unavailable on ${this.techName_} playback technology element.`, e);
        this.tech_.isReady_ = false;
        throw e;
      }

      // If error unknown, just log and throw
      log(e);
      throw e;
    }
  }

  /**
   * Attempt to begin playback at the first opportunity.
   *
   * @return {Promise|undefined}
   *         Returns a promise if the browser supports Promises (or one
   *         was passed in as an option). This promise will be resolved on
   *         the return value of play. If this is undefined it will fulfill the
   *         promise chain otherwise the promise chain will be fulfilled when
   *         the promise from play is fulfilled.
   */
  play() {
    return new Promise((resolve) => {
      this.play_(resolve);
    });
  }

  /**
   * The actual logic for play, takes a callback that will be resolved on the
   * return value of play. This allows us to resolve to the play promise if there
   * is one on modern browsers.
   *
   * @private
   * @param {Function} [callback]
   *        The callback that should be called when the techs play is actually called
   */
  play_(callback = silencePromise) {
    this.playCallbacks_.push(callback);

    const isSrcReady = Boolean(!this.changingSrc_ && (this.src() || this.currentSrc()));
    const isSafariOrIOS = Boolean(browser.IS_ANY_SAFARI || browser.IS_IOS);

    // treat calls to play_ somewhat like the `one` event function
    if (this.waitToPlay_) {
      this.off(['ready', 'loadstart'], this.waitToPlay_);
      this.waitToPlay_ = null;
    }

    // if the player/tech is not ready or the src itself is not ready
    // queue up a call to play on `ready` or `loadstart`
    if (!this.isReady_ || !isSrcReady) {
      this.waitToPlay_ = (e) => {
        this.play_();
      };
      this.one(['ready', 'loadstart'], this.waitToPlay_);

      // if we are in Safari, there is a high chance that loadstart will trigger after the gesture timeperiod
      // in that case, we need to prime the video element by calling load so it'll be ready in time
      if (!isSrcReady && isSafariOrIOS) {
        this.load();
      }
      return;
    }

    // If the player/tech is ready and we have a source, we can attempt playback.
    const val = this.techGet_('play');

    // For native playback, reset the progress bar if we get a play call from a replay.
    const isNativeReplay = isSafariOrIOS && this.hasClass('vjs-ended');

    if (isNativeReplay) {
      this.resetProgressBar_();
    }
    // play was terminated if the returned value is null
    if (val === null) {
      this.runPlayTerminatedQueue_();
    } else {
      this.runPlayCallbacks_(val);
    }
  }

  /**
   * These functions will be run when if play is terminated. If play
   * runPlayCallbacks_ is run these function will not be run. This allows us
   * to differentiate between a terminated play and an actual call to play.
   */
  runPlayTerminatedQueue_() {
    const queue = this.playTerminatedQueue_.slice(0);

    this.playTerminatedQueue_ = [];

    queue.forEach(function(q) {
      q();
    });
  }

  /**
   * When a callback to play is delayed we have to run these
   * callbacks when play is actually called on the tech. This function
   * runs the callbacks that were delayed and accepts the return value
   * from the tech.
   *
   * @param {undefined|Promise} val
   *        The return value from the tech.
   */
  runPlayCallbacks_(val) {
    const callbacks = this.playCallbacks_.slice(0);

    this.playCallbacks_ = [];
    // clear play terminatedQueue since we finished a real play
    this.playTerminatedQueue_ = [];

    callbacks.forEach(function(cb) {
      cb(val);
    });
  }

  /**
   * Pause the video playback
   *
   * @return {Player}
   *         A reference to the player object this function was called on
   */
  pause() {
    this.techCall_('pause');
  }

  /**
   * Check if the player is paused or has yet to play
   *
   * @return {boolean}
   *         - false: if the media is currently playing
   *         - true: if media is not currently playing
   */
  paused() {
    // The initial state of paused should be true (in Safari it's actually false)
    return (this.techGet_('paused') === false) ? false : true;
  }

  /**
   * Get a TimeRange object representing the current ranges of time that the user
   * has played.
   *
   * @return { import('./utils/time').TimeRange }
   *         A time range object that represents all the increments of time that have
   *         been played.
   */
  played() {
    return this.techGet_('played') || createTimeRange(0, 0);
  }

  /**
   * Returns whether or not the user is "scrubbing". Scrubbing is
   * when the user has clicked the progress bar handle and is
   * dragging it along the progress bar.
   *
   * @param {boolean} [isScrubbing]
   *        whether the user is or is not scrubbing
   *
   * @return {boolean}
   *         The value of scrubbing when getting
   */
  scrubbing(isScrubbing) {
    if (typeof isScrubbing === 'undefined') {
      return this.scrubbing_;
    }
    this.scrubbing_ = !!isScrubbing;
    this.techCall_('setScrubbing', this.scrubbing_);

    if (isScrubbing) {
      this.addClass('vjs-scrubbing');
    } else {
      this.removeClass('vjs-scrubbing');
    }
  }

  /**
   * Get or set the current time (in seconds)
   *
   * @param {number|string} [seconds]
   *        The time to seek to in seconds
   *
   * @return {number}
   *         - the current time in seconds when getting
   */
  currentTime(seconds) {
    if (typeof seconds !== 'undefined') {
      if (seconds < 0) {
        seconds = 0;
      }
      if (!this.isReady_ || this.changingSrc_ || !this.tech_ || !this.tech_.isReady_) {
        this.cache_.initTime = seconds;
        this.off('canplay', this.boundApplyInitTime_);
        this.one('canplay', this.boundApplyInitTime_);
        return;
      }
      this.techCall_('setCurrentTime', seconds);
      this.cache_.initTime = 0;
      return;
    }

    // cache last currentTime and return. default to 0 seconds
    //
    // Caching the currentTime is meant to prevent a massive amount of reads on the tech's
    // currentTime when scrubbing, but may not provide much performance benefit after all.
    // Should be tested. Also something has to read the actual current time or the cache will
    // never get updated.
    this.cache_.currentTime = (this.techGet_('currentTime') || 0);
    return this.cache_.currentTime;
  }

  /**
   * Apply the value of initTime stored in cache as currentTime.
   *
   * @private
   */
  applyInitTime_() {
    this.currentTime(this.cache_.initTime);
  }

  /**
   * Normally gets the length in time of the video in seconds;
   * in all but the rarest use cases an argument will NOT be passed to the method
   *
   * > **NOTE**: The video must have started loading before the duration can be
   * known, and depending on preload behaviour may not be known until the video starts
   * playing.
   *
   * @fires Player#durationchange
   *
   * @param {number} [seconds]
   *        The duration of the video to set in seconds
   *
   * @return {number}
   *         - The duration of the video in seconds when getting
   */
  duration(seconds) {
    if (seconds === undefined) {
      // return NaN if the duration is not known
      return this.cache_.duration !== undefined ? this.cache_.duration : NaN;
    }

    seconds = parseFloat(seconds);

    // Standardize on Infinity for signaling video is live
    if (seconds < 0) {
      seconds = Infinity;
    }

    if (seconds !== this.cache_.duration) {
      // Cache the last set value for optimized scrubbing
      this.cache_.duration = seconds;

      if (seconds === Infinity) {
        this.addClass('vjs-live');
      } else {
        this.removeClass('vjs-live');
      }
      if (!isNaN(seconds)) {
        // Do not fire durationchange unless the duration value is known.
        // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}

        /**
         * @event Player#durationchange
         * @type {Event}
         */
        this.trigger('durationchange');
      }
    }
  }

  /**
   * Calculates how much time is left in the video. Not part
   * of the native video API.
   *
   * @return {number}
   *         The time remaining in seconds
   */
  remainingTime() {
    return this.duration() - this.currentTime();
  }

  /**
   * A remaining time function that is intended to be used when
   * the time is to be displayed directly to the user.
   *
   * @return {number}
   *         The rounded time remaining in seconds
   */
  remainingTimeDisplay() {
    return Math.floor(this.duration()) - Math.floor(this.currentTime());
  }

  //
  // Kind of like an array of portions of the video that have been downloaded.

  /**
   * Get a TimeRange object with an array of the times of the video
   * that have been downloaded. If you just want the percent of the
   * video that's been downloaded, use bufferedPercent.
   *
   * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered}
   *
   * @return { import('./utils/time').TimeRange }
   *         A mock {@link TimeRanges} object (following HTML spec)
   */
  buffered() {
    let buffered = this.techGet_('buffered');

    if (!buffered || !buffered.length) {
      buffered = createTimeRange(0, 0);
    }

    return buffered;
  }

  /**
   * Get the percent (as a decimal) of the video that's been downloaded.
   * This method is not a part of the native HTML video API.
   *
   * @return {number}
   *         A decimal between 0 and 1 representing the percent
   *         that is buffered 0 being 0% and 1 being 100%
   */
  bufferedPercent() {
    return bufferedPercent(this.buffered(), this.duration());
  }

  /**
   * Get the ending time of the last buffered time range
   * This is used in the progress bar to encapsulate all time ranges.
   *
   * @return {number}
   *         The end of the last buffered time range
   */
  bufferedEnd() {
    const buffered = this.buffered();
    const duration = this.duration();
    let end = buffered.end(buffered.length - 1);

    if (end > duration) {
      end = duration;
    }

    return end;
  }

  /**
   * Get or set the current volume of the media
   *
   * @param  {number} [percentAsDecimal]
   *         The new volume as a decimal percent:
   *         - 0 is muted/0%/off
   *         - 1.0 is 100%/full
   *         - 0.5 is half volume or 50%
   *
   * @return {number}
   *         The current volume as a percent when getting
   */
  volume(percentAsDecimal) {
    let vol;

    if (percentAsDecimal !== undefined) {
      // Force value to between 0 and 1
      vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal)));
      this.cache_.volume = vol;
      this.techCall_('setVolume', vol);

      if (vol > 0) {
        this.lastVolume_(vol);
      }

      return;
    }

    // Default to 1 when returning current volume.
    vol = parseFloat(this.techGet_('volume'));
    return (isNaN(vol)) ? 1 : vol;
  }

  /**
   * Get the current muted state, or turn mute on or off
   *
   * @param {boolean} [muted]
   *        - true to mute
   *        - false to unmute
   *
   * @return {boolean}
   *         - true if mute is on and getting
   *         - false if mute is off and getting
   */
  muted(muted) {
    if (muted !== undefined) {
      this.techCall_('setMuted', muted);
      return;
    }
    return this.techGet_('muted') || false;
  }

  /**
   * Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted
   * indicates the state of muted on initial playback.
   *
   * ```js
   *   var myPlayer = videojs('some-player-id');
   *
   *   myPlayer.src("http://www.example.com/path/to/video.mp4");
   *
   *   // get, should be false
   *   console.log(myPlayer.defaultMuted());
   *   // set to true
   *   myPlayer.defaultMuted(true);
   *   // get should be true
   *   console.log(myPlayer.defaultMuted());
   * ```
   *
   * @param {boolean} [defaultMuted]
   *        - true to mute
   *        - false to unmute
   *
   * @return {boolean|Player}
   *         - true if defaultMuted is on and getting
   *         - false if defaultMuted is off and getting
   *         - A reference to the current player when setting
   */
  defaultMuted(defaultMuted) {
    if (defaultMuted !== undefined) {
      return this.techCall_('setDefaultMuted', defaultMuted);
    }
    return this.techGet_('defaultMuted') || false;
  }

  /**
   * Get the last volume, or set it
   *
   * @param  {number} [percentAsDecimal]
   *         The new last volume as a decimal percent:
   *         - 0 is muted/0%/off
   *         - 1.0 is 100%/full
   *         - 0.5 is half volume or 50%
   *
   * @return {number}
   *         the current value of lastVolume as a percent when getting
   *
   * @private
   */
  lastVolume_(percentAsDecimal) {
    if (percentAsDecimal !== undefined && percentAsDecimal !== 0) {
      this.cache_.lastVolume = percentAsDecimal;
      return;
    }
    return this.cache_.lastVolume;
  }

  /**
   * Check if current tech can support native fullscreen
   * (e.g. with built in controls like iOS)
   *
   * @return {boolean}
   *         if native fullscreen is supported
   */
  supportsFullScreen() {
    return this.techGet_('supportsFullScreen') || false;
  }

  /**
   * Check if the player is in fullscreen mode or tell the player that it
   * is or is not in fullscreen mode.
   *
   * > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official
   * property and instead document.fullscreenElement is used. But isFullscreen is
   * still a valuable property for internal player workings.
   *
   * @param  {boolean} [isFS]
   *         Set the players current fullscreen state
   *
   * @return {boolean}
   *         - true if fullscreen is on and getting
   *         - false if fullscreen is off and getting
   */
  isFullscreen(isFS) {
    if (isFS !== undefined) {
      const oldValue = this.isFullscreen_;

      this.isFullscreen_ = Boolean(isFS);

      // if we changed fullscreen state and we're in prefixed mode, trigger fullscreenchange
      // this is the only place where we trigger fullscreenchange events for older browsers
      // fullWindow mode is treated as a prefixed event and will get a fullscreenchange event as well
      if (this.isFullscreen_ !== oldValue && this.fsApi_.prefixed) {
        /**
           * @event Player#fullscreenchange
           * @type {Event}
           */
        this.trigger('fullscreenchange');
      }

      this.toggleFullscreenClass_();
      return;
    }
    return this.isFullscreen_;
  }

  /**
   * Increase the size of the video to full screen
   * In some browsers, full screen is not supported natively, so it enters
   * "full window mode", where the video fills the browser window.
   * In browsers and devices that support native full screen, sometimes the
   * browser's default controls will be shown, and not the Video.js custom skin.
   * This includes most mobile devices (iOS, Android) and older versions of
   * Safari.
   *
   * @param  {Object} [fullscreenOptions]
   *         Override the player fullscreen options
   *
   * @fires Player#fullscreenchange
   */
  requestFullscreen(fullscreenOptions) {
    if (this.isInPictureInPicture()) {
      this.exitPictureInPicture();
    }

    const self = this;

    return new Promise((resolve, reject) => {
      function offHandler() {
        self.off('fullscreenerror', errorHandler);
        self.off('fullscreenchange', changeHandler);
      }
      function changeHandler() {
        offHandler();
        resolve();
      }
      function errorHandler(e, err) {
        offHandler();
        reject(err);
      }

      self.one('fullscreenchange', changeHandler);
      self.one('fullscreenerror', errorHandler);

      const promise = self.requestFullscreenHelper_(fullscreenOptions);

      if (promise) {
        promise.then(offHandler, offHandler);
        promise.then(resolve, reject);
      }
    });
  }

  requestFullscreenHelper_(fullscreenOptions) {
    let fsOptions;

    // Only pass fullscreen options to requestFullscreen in spec-compliant browsers.
    // Use defaults or player configured option unless passed directly to this method.
    if (!this.fsApi_.prefixed) {
      fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {};
      if (fullscreenOptions !== undefined) {
        fsOptions = fullscreenOptions;
      }
    }

    // This method works as follows:
    // 1. if a fullscreen api is available, use it
    //   1. call requestFullscreen with potential options
    //   2. if we got a promise from above, use it to update isFullscreen()
    // 2. otherwise, if the tech supports fullscreen, call `enterFullScreen` on it.
    //   This is particularly used for iPhone, older iPads, and non-safari browser on iOS.
    // 3. otherwise, use "fullWindow" mode
    if (this.fsApi_.requestFullscreen) {
      const promise = this.el_[this.fsApi_.requestFullscreen](fsOptions);

      // Even on browsers with promise support this may not return a promise
      if (promise) {
        promise.then(() => this.isFullscreen(true), () => this.isFullscreen(false));
      }

      return promise;
    } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
      // we can't take the video.js controls fullscreen but we can go fullscreen
      // with native controls
      this.techCall_('enterFullScreen');
    } else {
      // fullscreen isn't supported so we'll just stretch the video element to
      // fill the viewport
      this.enterFullWindow();
    }
  }

  /**
   * Return the video to its normal size after having been in full screen mode
   *
   * @fires Player#fullscreenchange
   */
  exitFullscreen() {
    const self = this;

    return new Promise((resolve, reject) => {
      function offHandler() {
        self.off('fullscreenerror', errorHandler);
        self.off('fullscreenchange', changeHandler);
      }
      function changeHandler() {
        offHandler();
        resolve();
      }
      function errorHandler(e, err) {
        offHandler();
        reject(err);
      }

      self.one('fullscreenchange', changeHandler);
      self.one('fullscreenerror', errorHandler);

      const promise = self.exitFullscreenHelper_();

      if (promise) {
        promise.then(offHandler, offHandler);
        // map the promise to our resolve/reject methods
        promise.then(resolve, reject);
      }
    });
  }

  exitFullscreenHelper_() {
    if (this.fsApi_.requestFullscreen) {
      const promise = document[this.fsApi_.exitFullscreen]();

      // Even on browsers with promise support this may not return a promise
      if (promise) {
        // we're splitting the promise here, so, we want to catch the
        // potential error so that this chain doesn't have unhandled errors
        silencePromise(promise.then(() => this.isFullscreen(false)));
      }

      return promise;
    } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
      this.techCall_('exitFullScreen');
    } else {
      this.exitFullWindow();
    }
  }

  /**
   * When fullscreen isn't supported we can stretch the
   * video container to as wide as the browser will let us.
   *
   * @fires Player#enterFullWindow
   */
  enterFullWindow() {
    this.isFullscreen(true);
    this.isFullWindow = true;

    // Storing original doc overflow value to return to when fullscreen is off
    this.docOrigOverflow = document.documentElement.style.overflow;

    // Add listener for esc key to exit fullscreen
    Events.on(document, 'keydown', this.boundFullWindowOnEscKey_);

    // Hide any scroll bars
    document.documentElement.style.overflow = 'hidden';

    // Apply fullscreen styles
    Dom.addClass(document.body, 'vjs-full-window');

    /**
     * @event Player#enterFullWindow
     * @type {Event}
     */
    this.trigger('enterFullWindow');
  }

  /**
   * Check for call to either exit full window or
   * full screen on ESC key
   *
   * @param {string} event
   *        Event to check for key press
   */
  fullWindowOnEscKey(event) {
    if (keycode.isEventKey(event, 'Esc')) {
      if (this.isFullscreen() === true) {
        if (!this.isFullWindow) {
          this.exitFullscreen();
        } else {
          this.exitFullWindow();
        }
      }
    }
  }

  /**
   * Exit full window
   *
   * @fires Player#exitFullWindow
   */
  exitFullWindow() {
    this.isFullscreen(false);
    this.isFullWindow = false;
    Events.off(document, 'keydown', this.boundFullWindowOnEscKey_);

    // Unhide scroll bars.
    document.documentElement.style.overflow = this.docOrigOverflow;

    // Remove fullscreen styles
    Dom.removeClass(document.body, 'vjs-full-window');

    // Resize the box, controller, and poster to original sizes
    // this.positionAll();
    /**
     * @event Player#exitFullWindow
     * @type {Event}
     */
    this.trigger('exitFullWindow');
  }

  /**
   * Disable Picture-in-Picture mode.
   *
   * @param {boolean} value
   *                  - true will disable Picture-in-Picture mode
   *                  - false will enable Picture-in-Picture mode
   */
  disablePictureInPicture(value) {
    if (value === undefined) {
      return this.techGet_('disablePictureInPicture');
    }
    this.techCall_('setDisablePictureInPicture', value);
    this.options_.disablePictureInPicture = value;
    this.trigger('disablepictureinpicturechanged');
  }

  /**
   * Check if the player is in Picture-in-Picture mode or tell the player that it
   * is or is not in Picture-in-Picture mode.
   *
   * @param  {boolean} [isPiP]
   *         Set the players current Picture-in-Picture state
   *
   * @return {boolean}
   *         - true if Picture-in-Picture is on and getting
   *         - false if Picture-in-Picture is off and getting
   */
  isInPictureInPicture(isPiP) {
    if (isPiP !== undefined) {
      this.isInPictureInPicture_ = !!isPiP;
      this.togglePictureInPictureClass_();
      return;
    }
    return !!this.isInPictureInPicture_;
  }

  /**
   * Create a floating video window always on top of other windows so that users may
   * continue consuming media while they interact with other content sites, or
   * applications on their device.
   *
   * This can use document picture-in-picture or element picture in picture
   *
   * Set `enableDocumentPictureInPicture` to `true` to use docPiP on a supported browser
   * Else set `disablePictureInPicture` to `false` to disable elPiP on a supported browser
   *
   *
   * @see [Spec]{@link https://w3c.github.io/picture-in-picture/}
   * @see [Spec]{@link https://wicg.github.io/document-picture-in-picture/}
   *
   * @fires Player#enterpictureinpicture
   *
   * @return {Promise}
   *         A promise with a Picture-in-Picture window.
   */
  requestPictureInPicture() {
    if (this.options_.enableDocumentPictureInPicture && window.documentPictureInPicture) {
      const pipContainer = document.createElement(this.el().tagName);

      pipContainer.classList = this.el().classList;
      pipContainer.classList.add('vjs-pip-container');
      if (this.posterImage) {
        pipContainer.appendChild(this.posterImage.el().cloneNode(true));
      }
      if (this.titleBar) {
        pipContainer.appendChild(this.titleBar.el().cloneNode(true));
      }
      pipContainer.appendChild(Dom.createEl('p', { className: 'vjs-pip-text' }, {}, this.localize('Playing in picture-in-picture')));

      return window.documentPictureInPicture.requestWindow({
        // The aspect ratio won't be correct, Chrome bug https://crbug.com/1407629
        initialAspectRatio: this.videoWidth() / this.videoHeight(),
        copyStyleSheets: true
      }).then(pipWindow => {
        this.el_.parentNode.insertBefore(pipContainer, this.el_);

        pipWindow.document.body.append(this.el_);
        pipWindow.document.body.classList.add('vjs-pip-window');

        this.player_.isInPictureInPicture(true);
        this.player_.trigger('enterpictureinpicture');

        // Listen for the PiP closing event to move the video back.
        pipWindow.addEventListener('unload', (event) => {
          const pipVideo = event.target.querySelector('.video-js');

          pipContainer.replaceWith(pipVideo);
          this.player_.isInPictureInPicture(false);
          this.player_.trigger('leavepictureinpicture');
        });

        return pipWindow;
      });
    }
    if ('pictureInPictureEnabled' in document && this.disablePictureInPicture() === false) {
      /**
       * This event fires when the player enters picture in picture mode
       *
       * @event Player#enterpictureinpicture
       * @type {Event}
       */
      return this.techGet_('requestPictureInPicture');
    }
    return Promise.reject('No PiP mode is available');
  }

  /**
   * Exit Picture-in-Picture mode.
   *
   * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
   *
   * @fires Player#leavepictureinpicture
   *
   * @return {Promise}
   *         A promise.
   */
  exitPictureInPicture() {
    if (window.documentPictureInPicture && window.documentPictureInPicture.window) {
      // With documentPictureInPicture, Player#leavepictureinpicture is fired in the unload handler
      window.documentPictureInPicture.window.close();
      return Promise.resolve();
    }
    if ('pictureInPictureEnabled' in document) {

      /**
       * This event fires when the player leaves picture in picture mode
       *
       * @event Player#leavepictureinpicture
       * @type {Event}
       */
      return document.exitPictureInPicture();
    }
  }

  /**
   * Called when this Player has focus and a key gets pressed down, or when
   * any Component of this player receives a key press that it doesn't handle.
   * This allows player-wide hotkeys (either as defined below, or optionally
   * by an external function).
   *
   * @param {Event} event
   *        The `keydown` event that caused this function to be called.
   *
   * @listens keydown
   */
  handleKeyDown(event) {
    const {userActions} = this.options_;

    // Bail out if hotkeys are not configured.
    if (!userActions || !userActions.hotkeys) {
      return;
    }

    // Function that determines whether or not to exclude an element from
    // hotkeys handling.
    const excludeElement = (el) => {
      const tagName = el.tagName.toLowerCase();

      // The first and easiest test is for `contenteditable` elements.
      if (el.isContentEditable) {
        return true;
      }

      // Inputs matching these types will still trigger hotkey handling as
      // they are not text inputs.
      const allowedInputTypes = [
        'button',
        'checkbox',
        'hidden',
        'radio',
        'reset',
        'submit'
      ];

      if (tagName === 'input') {
        return allowedInputTypes.indexOf(el.type) === -1;
      }

      // The final test is by tag name. These tags will be excluded entirely.
      const excludedTags = ['textarea'];

      return excludedTags.indexOf(tagName) !== -1;
    };

    // Bail out if the user is focused on an interactive form element.
    if (excludeElement(this.el_.ownerDocument.activeElement)) {
      return;
    }

    if (typeof userActions.hotkeys === 'function') {
      userActions.hotkeys.call(this, event);
    } else {
      this.handleHotkeys(event);
    }
  }

  /**
   * Called when this Player receives a hotkey keydown event.
   * Supported player-wide hotkeys are:
   *
   *   f          - toggle fullscreen
   *   m          - toggle mute
   *   k or Space - toggle play/pause
   *
   * @param {Event} event
   *        The `keydown` event that caused this function to be called.
   */
  handleHotkeys(event) {
    const hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {};

    // set fullscreenKey, muteKey, playPauseKey from `hotkeys`, use defaults if not set
    const {
      fullscreenKey = keydownEvent => keycode.isEventKey(keydownEvent, 'f'),
      muteKey = keydownEvent => keycode.isEventKey(keydownEvent, 'm'),
      playPauseKey = keydownEvent => (keycode.isEventKey(keydownEvent, 'k') || keycode.isEventKey(keydownEvent, 'Space'))
    } = hotkeys;

    if (fullscreenKey.call(this, event)) {
      event.preventDefault();
      event.stopPropagation();

      const FSToggle = Component.getComponent('FullscreenToggle');

      if (document[this.fsApi_.fullscreenEnabled] !== false) {
        FSToggle.prototype.handleClick.call(this, event);
      }

    } else if (muteKey.call(this, event)) {
      event.preventDefault();
      event.stopPropagation();

      const MuteToggle = Component.getComponent('MuteToggle');

      MuteToggle.prototype.handleClick.call(this, event);

    } else if (playPauseKey.call(this, event)) {
      event.preventDefault();
      event.stopPropagation();

      const PlayToggle = Component.getComponent('PlayToggle');

      PlayToggle.prototype.handleClick.call(this, event);
    }
  }

  /**
   * Check whether the player can play a given mimetype
   *
   * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype
   *
   * @param {string} type
   *        The mimetype to check
   *
   * @return {string}
   *         'probably', 'maybe', or '' (empty string)
   */
  canPlayType(type) {
    let can;

    // Loop through each playback technology in the options order
    for (let i = 0, j = this.options_.techOrder; i < j.length; i++) {
      const techName = j[i];
      let tech = Tech.getTech(techName);

      // Support old behavior of techs being registered as components.
      // Remove once that deprecated behavior is removed.
      if (!tech) {
        tech = Component.getComponent(techName);
      }

      // Check if the current tech is defined before continuing
      if (!tech) {
        log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
        continue;
      }

      // Check if the browser supports this technology
      if (tech.isSupported()) {
        can = tech.canPlayType(type);

        if (can) {
          return can;
        }
      }
    }

    return '';
  }

  /**
   * Select source based on tech-order or source-order
   * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise,
   * defaults to tech-order selection
   *
   * @param {Array} sources
   *        The sources for a media asset
   *
   * @return {Object|boolean}
   *         Object of source and tech order or false
   */
  selectSource(sources) {
    // Get only the techs specified in `techOrder` that exist and are supported by the
    // current platform
    const techs =
      this.options_.techOrder
        .map((techName) => {
          return [techName, Tech.getTech(techName)];
        })
        .filter(([techName, tech]) => {
          // Check if the current tech is defined before continuing
          if (tech) {
            // Check if the browser supports this technology
            return tech.isSupported();
          }

          log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
          return false;
        });

    // Iterate over each `innerArray` element once per `outerArray` element and execute
    // `tester` with both. If `tester` returns a non-falsy value, exit early and return
    // that value.
    const findFirstPassingTechSourcePair = function(outerArray, innerArray, tester) {
      let found;

      outerArray.some((outerChoice) => {
        return innerArray.some((innerChoice) => {
          found = tester(outerChoice, innerChoice);

          if (found) {
            return true;
          }
        });
      });

      return found;
    };

    let foundSourceAndTech;
    const flip = (fn) => (a, b) => fn(b, a);
    const finder = ([techName, tech], source) => {
      if (tech.canPlaySource(source, this.options_[techName.toLowerCase()])) {
        return {source, tech: techName};
      }
    };

    // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources
    // to select from them based on their priority.
    if (this.options_.sourceOrder) {
      // Source-first ordering
      foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder));
    } else {
      // Tech-first ordering
      foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder);
    }

    return foundSourceAndTech || false;
  }

  /**
   * Executes source setting and getting logic
   *
   * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
   *        A SourceObject, an array of SourceObjects, or a string referencing
   *        a URL to a media source. It is _highly recommended_ that an object
   *        or array of objects is used here, so that source selection
   *        algorithms can take the `type` into account.
   *
   *        If not provided, this method acts as a getter.
   * @param {boolean} isRetry
   *        Indicates whether this is being called internally as a result of a retry
   *
   * @return {string|undefined}
   *         If the `source` argument is missing, returns the current source
   *         URL. Otherwise, returns nothing/undefined.
   */
  handleSrc_(source, isRetry) {
    // getter usage
    if (typeof source === 'undefined') {
      return this.cache_.src || '';
    }

    // Reset retry behavior for new source
    if (this.resetRetryOnError_) {
      this.resetRetryOnError_();
    }

    // filter out invalid sources and turn our source into
    // an array of source objects
    const sources = filterSource(source);

    // if a source was passed in then it is invalid because
    // it was filtered to a zero length Array. So we have to
    // show an error
    if (!sources.length) {
      this.setTimeout(function() {
        this.error({ code: 4, message: this.options_.notSupportedMessage });
      }, 0);
      return;
    }

    // initial sources
    this.changingSrc_ = true;

    // Only update the cached source list if we are not retrying a new source after error,
    // since in that case we want to include the failed source(s) in the cache
    if (!isRetry) {
      this.cache_.sources = sources;
    }

    this.updateSourceCaches_(sources[0]);

    // middlewareSource is the source after it has been changed by middleware
    middleware.setSource(this, sources[0], (middlewareSource, mws) => {
      this.middleware_ = mws;

      // since sourceSet is async we have to update the cache again after we select a source since
      // the source that is selected could be out of order from the cache update above this callback.
      if (!isRetry) {
        this.cache_.sources = sources;
      }

      this.updateSourceCaches_(middlewareSource);

      const err = this.src_(middlewareSource);

      if (err) {
        if (sources.length > 1) {
          return this.handleSrc_(sources.slice(1));
        }

        this.changingSrc_ = false;

        // We need to wrap this in a timeout to give folks a chance to add error event handlers
        this.setTimeout(function() {
          this.error({ code: 4, message: this.options_.notSupportedMessage });
        }, 0);

        // we could not find an appropriate tech, but let's still notify the delegate that this is it
        // this needs a better comment about why this is needed
        this.triggerReady();

        return;
      }

      middleware.setTech(mws, this.tech_);
    });

    // Try another available source if this one fails before playback.
    if (sources.length > 1) {
      const retry = () => {
        // Remove the error modal
        this.error(null);
        this.handleSrc_(sources.slice(1), true);
      };

      const stopListeningForErrors = () => {
        this.off('error', retry);
      };

      this.one('error', retry);
      this.one('playing', stopListeningForErrors);

      this.resetRetryOnError_ = () => {
        this.off('error', retry);
        this.off('playing', stopListeningForErrors);
      };
    }
  }

  /**
   * Get or set the video source.
   *
   * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
   *        A SourceObject, an array of SourceObjects, or a string referencing
   *        a URL to a media source. It is _highly recommended_ that an object
   *        or array of objects is used here, so that source selection
   *        algorithms can take the `type` into account.
   *
   *        If not provided, this method acts as a getter.
   *
   * @return {string|undefined}
   *         If the `source` argument is missing, returns the current source
   *         URL. Otherwise, returns nothing/undefined.
   */
  src(source) {
    return this.handleSrc_(source, false);
  }

  /**
   * Set the source object on the tech, returns a boolean that indicates whether
   * there is a tech that can play the source or not
   *
   * @param {Tech~SourceObject} source
   *        The source object to set on the Tech
   *
   * @return {boolean}
   *         - True if there is no Tech to playback this source
   *         - False otherwise
   *
   * @private
   */
  src_(source) {
    const sourceTech = this.selectSource([source]);

    if (!sourceTech) {
      return true;
    }

    if (!titleCaseEquals(sourceTech.tech, this.techName_)) {
      this.changingSrc_ = true;
      // load this technology with the chosen source
      this.loadTech_(sourceTech.tech, sourceTech.source);
      this.tech_.ready(() => {
        this.changingSrc_ = false;
      });
      return false;
    }

    // wait until the tech is ready to set the source
    // and set it synchronously if possible (#2326)
    this.ready(function() {

      // The setSource tech method was added with source handlers
      // so older techs won't support it
      // We need to check the direct prototype for the case where subclasses
      // of the tech do not support source handlers
      if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) {
        this.techCall_('setSource', source);
      } else {
        this.techCall_('src', source.src);
      }

      this.changingSrc_ = false;
    }, true);

    return false;
  }

  /**
   * Begin loading the src data.
   */
  load() {
    this.techCall_('load');
  }

  /**
   * Reset the player. Loads the first tech in the techOrder,
   * removes all the text tracks in the existing `tech`,
   * and calls `reset` on the `tech`.
   */
  reset() {
    if (this.paused()) {
      this.doReset_();
    } else {
      const playPromise = this.play();

      silencePromise(playPromise.then(() => this.doReset_()));
    }
  }

  doReset_() {
    if (this.tech_) {
      this.tech_.clearTracks('text');
    }
    this.resetCache_();
    this.poster('');
    this.loadTech_(this.options_.techOrder[0], null);
    this.techCall_('reset');
    this.resetControlBarUI_();
    if (isEvented(this)) {
      this.trigger('playerreset');
    }
  }

  /**
   * Reset Control Bar's UI by calling sub-methods that reset
   * all of Control Bar's components
   */
  resetControlBarUI_() {
    this.resetProgressBar_();
    this.resetPlaybackRate_();
    this.resetVolumeBar_();
  }

  /**
   * Reset tech's progress so progress bar is reset in the UI
   */
  resetProgressBar_() {
    this.currentTime(0);

    const {
      currentTimeDisplay,
      durationDisplay,
      progressControl,
      remainingTimeDisplay
    } = this.controlBar || {};
    const { seekBar } = progressControl || {};

    if (currentTimeDisplay) {
      currentTimeDisplay.updateContent();
    }

    if (durationDisplay) {
      durationDisplay.updateContent();
    }

    if (remainingTimeDisplay) {
      remainingTimeDisplay.updateContent();
    }

    if (seekBar) {
      seekBar.update();

      if (seekBar.loadProgressBar) {
        seekBar.loadProgressBar.update();
      }
    }
  }

  /**
   * Reset Playback ratio
   */
  resetPlaybackRate_() {
    this.playbackRate(this.defaultPlaybackRate());
    this.handleTechRateChange_();
  }

  /**
   * Reset Volume bar
   */
  resetVolumeBar_() {
    this.volume(1.0);
    this.trigger('volumechange');
  }

  /**
   * Returns all of the current source objects.
   *
   * @return {Tech~SourceObject[]}
   *         The current source objects
   */
  currentSources() {
    const source = this.currentSource();
    const sources = [];

    // assume `{}` or `{ src }`
    if (Object.keys(source).length !== 0) {
      sources.push(source);
    }

    return this.cache_.sources || sources;
  }

  /**
   * Returns the current source object.
   *
   * @return {Tech~SourceObject}
   *         The current source object
   */
  currentSource() {
    return this.cache_.source || {};
  }

  /**
   * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4
   * Can be used in conjunction with `currentType` to assist in rebuilding the current source object.
   *
   * @return {string}
   *         The current source
   */
  currentSrc() {
    return this.currentSource() && this.currentSource().src || '';
  }

  /**
   * Get the current source type e.g. video/mp4
   * This can allow you rebuild the current source object so that you could load the same
   * source and tech later
   *
   * @return {string}
   *         The source MIME type
   */
  currentType() {
    return this.currentSource() && this.currentSource().type || '';
  }

  /**
   * Get or set the preload attribute
   *
   * @param {boolean} [value]
   *        - true means that we should preload
   *        - false means that we should not preload
   *
   * @return {string}
   *         The preload attribute value when getting
   */
  preload(value) {
    if (value !== undefined) {
      this.techCall_('setPreload', value);
      this.options_.preload = value;
      return;
    }
    return this.techGet_('preload');
  }

  /**
   * Get or set the autoplay option. When this is a boolean it will
   * modify the attribute on the tech. When this is a string the attribute on
   * the tech will be removed and `Player` will handle autoplay on loadstarts.
   *
   * @param {boolean|string} [value]
   *        - true: autoplay using the browser behavior
   *        - false: do not autoplay
   *        - 'play': call play() on every loadstart
   *        - 'muted': call muted() then play() on every loadstart
   *        - 'any': call play() on every loadstart. if that fails call muted() then play().
   *        - *: values other than those listed here will be set `autoplay` to true
   *
   * @return {boolean|string}
   *         The current value of autoplay when getting
   */
  autoplay(value) {
    // getter usage
    if (value === undefined) {
      return this.options_.autoplay || false;
    }

    let techAutoplay;

    // if the value is a valid string set it to that, or normalize `true` to 'play', if need be
    if (typeof value === 'string' && (/(any|play|muted)/).test(value) || value === true && this.options_.normalizeAutoplay) {
      this.options_.autoplay = value;
      this.manualAutoplay_(typeof value === 'string' ? value : 'play');
      techAutoplay = false;

    // any falsy value sets autoplay to false in the browser,
    // lets do the same
    } else if (!value) {
      this.options_.autoplay = false;

    // any other value (ie truthy) sets autoplay to true
    } else {
      this.options_.autoplay = true;
    }

    techAutoplay = typeof techAutoplay === 'undefined' ? this.options_.autoplay : techAutoplay;

    // if we don't have a tech then we do not queue up
    // a setAutoplay call on tech ready. We do this because the
    // autoplay option will be passed in the constructor and we
    // do not need to set it twice
    if (this.tech_) {
      this.techCall_('setAutoplay', techAutoplay);
    }
  }

  /**
   * Set or unset the playsinline attribute.
   * Playsinline tells the browser that non-fullscreen playback is preferred.
   *
   * @param {boolean} [value]
   *        - true means that we should try to play inline by default
   *        - false means that we should use the browser's default playback mode,
   *          which in most cases is inline. iOS Safari is a notable exception
   *          and plays fullscreen by default.
   *
   * @return {string|Player}
   *         - the current value of playsinline
   *         - the player when setting
   *
   * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
   */
  playsinline(value) {
    if (value !== undefined) {
      this.techCall_('setPlaysinline', value);
      this.options_.playsinline = value;
      return this;
    }
    return this.techGet_('playsinline');
  }

  /**
   * Get or set the loop attribute on the video element.
   *
   * @param {boolean} [value]
   *        - true means that we should loop the video
   *        - false means that we should not loop the video
   *
   * @return {boolean}
   *         The current value of loop when getting
   */
  loop(value) {
    if (value !== undefined) {
      this.techCall_('setLoop', value);
      this.options_.loop = value;
      return;
    }
    return this.techGet_('loop');
  }

  /**
   * Get or set the poster image source url
   *
   * @fires Player#posterchange
   *
   * @param {string} [src]
   *        Poster image source URL
   *
   * @return {string}
   *         The current value of poster when getting
   */
  poster(src) {
    if (src === undefined) {
      return this.poster_;
    }

    // The correct way to remove a poster is to set as an empty string
    // other falsey values will throw errors
    if (!src) {
      src = '';
    }

    if (src === this.poster_) {
      return;
    }

    // update the internal poster variable
    this.poster_ = src;

    // update the tech's poster
    this.techCall_('setPoster', src);

    this.isPosterFromTech_ = false;

    // alert components that the poster has been set
    /**
     * This event fires when the poster image is changed on the player.
     *
     * @event Player#posterchange
     * @type {Event}
     */
    this.trigger('posterchange');
  }

  /**
   * Some techs (e.g. YouTube) can provide a poster source in an
   * asynchronous way. We want the poster component to use this
   * poster source so that it covers up the tech's controls.
   * (YouTube's play button). However we only want to use this
   * source if the player user hasn't set a poster through
   * the normal APIs.
   *
   * @fires Player#posterchange
   * @listens Tech#posterchange
   * @private
   */
  handleTechPosterChange_() {
    if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) {
      const newPoster = this.tech_.poster() || '';

      if (newPoster !== this.poster_) {
        this.poster_ = newPoster;
        this.isPosterFromTech_ = true;

        // Let components know the poster has changed
        this.trigger('posterchange');
      }
    }
  }

  /**
   * Get or set whether or not the controls are showing.
   *
   * @fires Player#controlsenabled
   *
   * @param {boolean} [bool]
   *        - true to turn controls on
   *        - false to turn controls off
   *
   * @return {boolean}
   *         The current value of controls when getting
   */
  controls(bool) {
    if (bool === undefined) {
      return !!this.controls_;
    }

    bool = !!bool;

    // Don't trigger a change event unless it actually changed
    if (this.controls_ === bool) {
      return;
    }

    this.controls_ = bool;

    if (this.usingNativeControls()) {
      this.techCall_('setControls', bool);
    }

    if (this.controls_) {
      this.removeClass('vjs-controls-disabled');
      this.addClass('vjs-controls-enabled');
      /**
       * @event Player#controlsenabled
       * @type {Event}
       */
      this.trigger('controlsenabled');
      if (!this.usingNativeControls()) {
        this.addTechControlsListeners_();
      }
    } else {
      this.removeClass('vjs-controls-enabled');
      this.addClass('vjs-controls-disabled');
      /**
       * @event Player#controlsdisabled
       * @type {Event}
       */
      this.trigger('controlsdisabled');
      if (!this.usingNativeControls()) {
        this.removeTechControlsListeners_();
      }
    }
  }

  /**
   * Toggle native controls on/off. Native controls are the controls built into
   * devices (e.g. default iPhone controls) or other techs
   * (e.g. Vimeo Controls)
   * **This should only be set by the current tech, because only the tech knows
   * if it can support native controls**
   *
   * @fires Player#usingnativecontrols
   * @fires Player#usingcustomcontrols
   *
   * @param {boolean} [bool]
   *        - true to turn native controls on
   *        - false to turn native controls off
   *
   * @return {boolean}
   *         The current value of native controls when getting
   */
  usingNativeControls(bool) {
    if (bool === undefined) {
      return !!this.usingNativeControls_;
    }

    bool = !!bool;

    // Don't trigger a change event unless it actually changed
    if (this.usingNativeControls_ === bool) {
      return;
    }

    this.usingNativeControls_ = bool;

    if (this.usingNativeControls_) {
      this.addClass('vjs-using-native-controls');

      /**
       * player is using the native device controls
       *
       * @event Player#usingnativecontrols
       * @type {Event}
       */
      this.trigger('usingnativecontrols');
    } else {
      this.removeClass('vjs-using-native-controls');

      /**
       * player is using the custom HTML controls
       *
       * @event Player#usingcustomcontrols
       * @type {Event}
       */
      this.trigger('usingcustomcontrols');
    }
  }

  /**
   * Set or get the current MediaError
   *
   * @fires Player#error
   *
   * @param  {MediaError|string|number} [err]
   *         A MediaError or a string/number to be turned
   *         into a MediaError
   *
   * @return {MediaError|null}
   *         The current MediaError when getting (or null)
   */
  error(err) {
    if (err === undefined) {
      return this.error_ || null;
    }

    // allow hooks to modify error object
    hooks('beforeerror').forEach((hookFunction) => {
      const newErr = hookFunction(this, err);

      if (!(
        (isObject(newErr) && !Array.isArray(newErr)) ||
        typeof newErr === 'string' ||
        typeof newErr === 'number' ||
        newErr === null
      )) {
        this.log.error('please return a value that MediaError expects in beforeerror hooks');
        return;
      }

      err = newErr;
    });

    // Suppress the first error message for no compatible source until
    // user interaction
    if (this.options_.suppressNotSupportedError &&
        err && err.code === 4
    ) {
      const triggerSuppressedError = function() {
        this.error(err);
      };

      this.options_.suppressNotSupportedError = false;
      this.any(['click', 'touchstart'], triggerSuppressedError);
      this.one('loadstart', function() {
        this.off(['click', 'touchstart'], triggerSuppressedError);
      });
      return;
    }

    // restoring to default
    if (err === null) {
      this.error_ = err;
      this.removeClass('vjs-error');
      if (this.errorDisplay) {
        this.errorDisplay.close();
      }
      return;
    }

    this.error_ = new MediaError(err);

    // add the vjs-error classname to the player
    this.addClass('vjs-error');

    // log the name of the error type and any message
    // IE11 logs "[object object]" and required you to expand message to see error object
    log.error(`(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, this.error_.message, this.error_);

    /**
     * @event Player#error
     * @type {Event}
     */
    this.trigger('error');

    // notify hooks of the per player error
    hooks('error').forEach((hookFunction) => hookFunction(this, this.error_));

    return;
  }

  /**
   * Report user activity
   *
   * @param {Object} event
   *        Event object
   */
  reportUserActivity(event) {
    this.userActivity_ = true;
  }

  /**
   * Get/set if user is active
   *
   * @fires Player#useractive
   * @fires Player#userinactive
   *
   * @param {boolean} [bool]
   *        - true if the user is active
   *        - false if the user is inactive
   *
   * @return {boolean}
   *         The current value of userActive when getting
   */
  userActive(bool) {
    if (bool === undefined) {
      return this.userActive_;
    }

    bool = !!bool;

    if (bool === this.userActive_) {
      return;
    }

    this.userActive_ = bool;

    if (this.userActive_) {
      this.userActivity_ = true;
      this.removeClass('vjs-user-inactive');
      this.addClass('vjs-user-active');
      /**
       * @event Player#useractive
       * @type {Event}
       */
      this.trigger('useractive');
      return;
    }

    // Chrome/Safari/IE have bugs where when you change the cursor it can
    // trigger a mousemove event. This causes an issue when you're hiding
    // the cursor when the user is inactive, and a mousemove signals user
    // activity. Making it impossible to go into inactive mode. Specifically
    // this happens in fullscreen when we really need to hide the cursor.
    //
    // When this gets resolved in ALL browsers it can be removed
    // https://code.google.com/p/chromium/issues/detail?id=103041
    if (this.tech_) {
      this.tech_.one('mousemove', function(e) {
        e.stopPropagation();
        e.preventDefault();
      });
    }

    this.userActivity_ = false;
    this.removeClass('vjs-user-active');
    this.addClass('vjs-user-inactive');
    /**
     * @event Player#userinactive
     * @type {Event}
     */
    this.trigger('userinactive');
  }

  /**
   * Listen for user activity based on timeout value
   *
   * @private
   */
  listenForUserActivity_() {
    let mouseInProgress;
    let lastMoveX;
    let lastMoveY;
    const handleActivity = Fn.bind_(this, this.reportUserActivity);

    const handleMouseMove = function(e) {
      // #1068 - Prevent mousemove spamming
      // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970
      if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) {
        lastMoveX = e.screenX;
        lastMoveY = e.screenY;
        handleActivity();
      }
    };

    const handleMouseDown = function() {
      handleActivity();
      // For as long as the they are touching the device or have their mouse down,
      // we consider them active even if they're not moving their finger or mouse.
      // So we want to continue to update that they are active
      this.clearInterval(mouseInProgress);
      // Setting userActivity=true now and setting the interval to the same time
      // as the activityCheck interval (250) should ensure we never miss the
      // next activityCheck
      mouseInProgress = this.setInterval(handleActivity, 250);
    };

    const handleMouseUpAndMouseLeave = function(event) {
      handleActivity();
      // Stop the interval that maintains activity if the mouse/touch is down
      this.clearInterval(mouseInProgress);
    };

    // Any mouse movement will be considered user activity
    this.on('mousedown', handleMouseDown);
    this.on('mousemove', handleMouseMove);
    this.on('mouseup', handleMouseUpAndMouseLeave);
    this.on('mouseleave', handleMouseUpAndMouseLeave);

    const controlBar = this.getChild('controlBar');

    // Fixes bug on Android & iOS where when tapping progressBar (when control bar is displayed)
    // controlBar would no longer be hidden by default timeout.
    if (controlBar && !browser.IS_IOS && !browser.IS_ANDROID) {

      controlBar.on('mouseenter', function(event) {
        if (this.player().options_.inactivityTimeout !== 0) {
          this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout;
        }
        this.player().options_.inactivityTimeout = 0;
      });

      controlBar.on('mouseleave', function(event) {
        this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout;
      });

    }

    // Listen for keyboard navigation
    // Shouldn't need to use inProgress interval because of key repeat
    this.on('keydown', handleActivity);
    this.on('keyup', handleActivity);

    // Run an interval every 250 milliseconds instead of stuffing everything into
    // the mousemove/touchmove function itself, to prevent performance degradation.
    // `this.reportUserActivity` simply sets this.userActivity_ to true, which
    // then gets picked up by this loop
    // http://ejohn.org/blog/learning-from-twitter/
    let inactivityTimeout;

    this.setInterval(function() {
      // Check to see if mouse/touch activity has happened
      if (!this.userActivity_) {
        return;
      }

      // Reset the activity tracker
      this.userActivity_ = false;

      // If the user state was inactive, set the state to active
      this.userActive(true);

      // Clear any existing inactivity timeout to start the timer over
      this.clearTimeout(inactivityTimeout);

      const timeout = this.options_.inactivityTimeout;

      if (timeout <= 0) {
        return;
      }

      // In <timeout> milliseconds, if no more activity has occurred the
      // user will be considered inactive
      inactivityTimeout = this.setTimeout(function() {
        // Protect against the case where the inactivityTimeout can trigger just
        // before the next user activity is picked up by the activity check loop
        // causing a flicker
        if (!this.userActivity_) {
          this.userActive(false);
        }
      }, timeout);

    }, 250);
  }

  /**
   * Gets or sets the current playback rate. A playback rate of
   * 1.0 represents normal speed and 0.5 would indicate half-speed
   * playback, for instance.
   *
   * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate
   *
   * @param {number} [rate]
   *       New playback rate to set.
   *
   * @return {number}
   *         The current playback rate when getting or 1.0
   */
  playbackRate(rate) {
    if (rate !== undefined) {
      // NOTE: this.cache_.lastPlaybackRate is set from the tech handler
      // that is registered above
      this.techCall_('setPlaybackRate', rate);
      return;
    }

    if (this.tech_ && this.tech_.featuresPlaybackRate) {
      return this.cache_.lastPlaybackRate || this.techGet_('playbackRate');
    }
    return 1.0;
  }

  /**
   * Gets or sets the current default playback rate. A default playback rate of
   * 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance.
   * defaultPlaybackRate will only represent what the initial playbackRate of a video was, not
   * not the current playbackRate.
   *
   * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate
   *
   * @param {number} [rate]
   *       New default playback rate to set.
   *
   * @return {number|Player}
   *         - The default playback rate when getting or 1.0
   *         - the player when setting
   */
  defaultPlaybackRate(rate) {
    if (rate !== undefined) {
      return this.techCall_('setDefaultPlaybackRate', rate);
    }

    if (this.tech_ && this.tech_.featuresPlaybackRate) {
      return this.techGet_('defaultPlaybackRate');
    }
    return 1.0;
  }

  /**
   * Gets or sets the audio flag
   *
   * @param {boolean} bool
   *        - true signals that this is an audio player
   *        - false signals that this is not an audio player
   *
   * @return {boolean}
   *         The current value of isAudio when getting
   */
  isAudio(bool) {
    if (bool !== undefined) {
      this.isAudio_ = !!bool;
      return;
    }

    return !!this.isAudio_;
  }

  enableAudioOnlyUI_() {
    // Update styling immediately to show the control bar so we can get its height
    this.addClass('vjs-audio-only-mode');

    const playerChildren = this.children();
    const controlBar = this.getChild('ControlBar');
    const controlBarHeight = controlBar && controlBar.currentHeight();

    // Hide all player components except the control bar. Control bar components
    // needed only for video are hidden with CSS
    playerChildren.forEach(child => {
      if (child === controlBar) {
        return;
      }

      if (child.el_ && !child.hasClass('vjs-hidden')) {
        child.hide();

        this.audioOnlyCache_.hiddenChildren.push(child);
      }
    });

    this.audioOnlyCache_.playerHeight = this.currentHeight();

    // Set the player height the same as the control bar
    this.height(controlBarHeight);
    this.trigger('audioonlymodechange');
  }

  disableAudioOnlyUI_() {
    this.removeClass('vjs-audio-only-mode');

    // Show player components that were previously hidden
    this.audioOnlyCache_.hiddenChildren.forEach(child => child.show());

    // Reset player height
    this.height(this.audioOnlyCache_.playerHeight);
    this.trigger('audioonlymodechange');
  }

  /**
   * Get the current audioOnlyMode state or set audioOnlyMode to true or false.
   *
   * Setting this to `true` will hide all player components except the control bar,
   * as well as control bar components needed only for video.
   *
   * @param {boolean} [value]
   *         The value to set audioOnlyMode to.
   *
   * @return {Promise|boolean}
   *        A Promise is returned when setting the state, and a boolean when getting
   *        the present state
   */
  audioOnlyMode(value) {
    if (typeof value !== 'boolean' || value === this.audioOnlyMode_) {
      return this.audioOnlyMode_;
    }

    this.audioOnlyMode_ = value;

    // Enable Audio Only Mode
    if (value) {
      const exitPromises = [];

      // Fullscreen and PiP are not supported in audioOnlyMode, so exit if we need to.
      if (this.isInPictureInPicture()) {
        exitPromises.push(this.exitPictureInPicture());
      }

      if (this.isFullscreen()) {
        exitPromises.push(this.exitFullscreen());
      }

      if (this.audioPosterMode()) {
        exitPromises.push(this.audioPosterMode(false));
      }

      return Promise.all(exitPromises).then(() => this.enableAudioOnlyUI_());
    }

    // Disable Audio Only Mode
    return Promise.resolve().then(() => this.disableAudioOnlyUI_());
  }

  enablePosterModeUI_() {
    // Hide the video element and show the poster image to enable posterModeUI
    const tech = this.tech_ && this.tech_;

    tech.hide();
    this.addClass('vjs-audio-poster-mode');
    this.trigger('audiopostermodechange');
  }

  disablePosterModeUI_() {
    // Show the video element and hide the poster image to disable posterModeUI
    const tech = this.tech_ && this.tech_;

    tech.show();
    this.removeClass('vjs-audio-poster-mode');
    this.trigger('audiopostermodechange');
  }

  /**
   * Get the current audioPosterMode state or set audioPosterMode to true or false
   *
   * @param {boolean} [value]
   *         The value to set audioPosterMode to.
   *
   * @return {Promise|boolean}
   *         A Promise is returned when setting the state, and a boolean when getting
   *        the present state
   */
  audioPosterMode(value) {

    if (typeof value !== 'boolean' || value === this.audioPosterMode_) {
      return this.audioPosterMode_;
    }

    this.audioPosterMode_ = value;

    if (value) {

      if (this.audioOnlyMode()) {
        const audioOnlyModePromise = this.audioOnlyMode(false);

        return audioOnlyModePromise.then(() => {
          // enable audio poster mode after audio only mode is disabled
          this.enablePosterModeUI_();
        });
      }

      return Promise.resolve().then(() => {
        // enable audio poster mode
        this.enablePosterModeUI_();
      });
    }

    return Promise.resolve().then(() => {
      // disable audio poster mode
      this.disablePosterModeUI_();
    });
  }

  /**
   * A helper method for adding a {@link TextTrack} to our
   * {@link TextTrackList}.
   *
   * In addition to the W3C settings we allow adding additional info through options.
   *
   * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack
   *
   * @param {string} [kind]
   *        the kind of TextTrack you are adding
   *
   * @param {string} [label]
   *        the label to give the TextTrack label
   *
   * @param {string} [language]
   *        the language to set on the TextTrack
   *
   * @return {TextTrack|undefined}
   *         the TextTrack that was added or undefined
   *         if there is no tech
   */
  addTextTrack(kind, label, language) {
    if (this.tech_) {
      return this.tech_.addTextTrack(kind, label, language);
    }
  }

  /**
   * Create a remote {@link TextTrack} and an {@link HTMLTrackElement}.
   *
   * @param {Object} options
   *        Options to pass to {@link HTMLTrackElement} during creation. See
   *        {@link HTMLTrackElement} for object properties that you should use.
   *
   * @param {boolean} [manualCleanup=false] if set to true, the TextTrack will not be removed
   *                                        from the TextTrackList and HtmlTrackElementList
   *                                        after a source change
   *
   * @return { import('./tracks/html-track-element').default }
   *         the HTMLTrackElement that was created and added
   *         to the HtmlTrackElementList and the remote
   *         TextTrackList
   *
   */
  addRemoteTextTrack(options, manualCleanup) {
    if (this.tech_) {
      return this.tech_.addRemoteTextTrack(options, manualCleanup);
    }
  }

  /**
   * Remove a remote {@link TextTrack} from the respective
   * {@link TextTrackList} and {@link HtmlTrackElementList}.
   *
   * @param {Object} track
   *        Remote {@link TextTrack} to remove
   *
   * @return {undefined}
   *         does not return anything
   */
  removeRemoteTextTrack(obj = {}) {
    let {track} = obj;

    if (!track) {
      track = obj;
    }

    // destructure the input into an object with a track argument, defaulting to arguments[0]
    // default the whole argument to an empty object if nothing was passed in

    if (this.tech_) {
      return this.tech_.removeRemoteTextTrack(track);
    }
  }

  /**
   * Gets available media playback quality metrics as specified by the W3C's Media
   * Playback Quality API.
   *
   * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
   *
   * @return {Object|undefined}
   *         An object with supported media playback quality metrics or undefined if there
   *         is no tech or the tech does not support it.
   */
  getVideoPlaybackQuality() {
    return this.techGet_('getVideoPlaybackQuality');
  }

  /**
   * Get video width
   *
   * @return {number}
   *         current video width
   */
  videoWidth() {
    return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0;
  }

  /**
   * Get video height
   *
   * @return {number}
   *         current video height
   */
  videoHeight() {
    return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0;
  }

  /**
   * The player's language code.
   *
   * Changing the language will trigger
   * [languagechange]{@link Player#event:languagechange}
   * which Components can use to update control text.
   * ClickableComponent will update its control text by default on
   * [languagechange]{@link Player#event:languagechange}.
   *
   * @fires Player#languagechange
   *
   * @param {string} [code]
   *        the language code to set the player to
   *
   * @return {string}
   *         The current language code when getting
   */
  language(code) {
    if (code === undefined) {
      return this.language_;
    }

    if (this.language_ !== String(code).toLowerCase()) {
      this.language_ = String(code).toLowerCase();

      // during first init, it's possible some things won't be evented
      if (isEvented(this)) {
        /**
        * fires when the player language change
        *
        * @event Player#languagechange
        * @type {Event}
        */
        this.trigger('languagechange');
      }
    }
  }

  /**
   * Get the player's language dictionary
   * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time
   * Languages specified directly in the player options have precedence
   *
   * @return {Array}
   *         An array of of supported languages
   */
  languages() {
    return merge(Player.prototype.options_.languages, this.languages_);
  }

  /**
   * returns a JavaScript object representing the current track
   * information. **DOES not return it as JSON**
   *
   * @return {Object}
   *         Object representing the current of track info
   */
  toJSON() {
    const options = merge(this.options_);
    const tracks = options.tracks;

    options.tracks = [];

    for (let i = 0; i < tracks.length; i++) {
      let track = tracks[i];

      // deep merge tracks and null out player so no circular references
      track = merge(track);
      track.player = undefined;
      options.tracks[i] = track;
    }

    return options;
  }

  /**
   * Creates a simple modal dialog (an instance of the {@link ModalDialog}
   * component) that immediately overlays the player with arbitrary
   * content and removes itself when closed.
   *
   * @param {string|Function|Element|Array|null} content
   *        Same as {@link ModalDialog#content}'s param of the same name.
   *        The most straight-forward usage is to provide a string or DOM
   *        element.
   *
   * @param {Object} [options]
   *        Extra options which will be passed on to the {@link ModalDialog}.
   *
   * @return {ModalDialog}
   *         the {@link ModalDialog} that was created
   */
  createModal(content, options) {
    options = options || {};
    options.content = content || '';

    const modal = new ModalDialog(this, options);

    this.addChild(modal);
    modal.on('dispose', () => {
      this.removeChild(modal);
    });

    modal.open();
    return modal;
  }

  /**
   * Change breakpoint classes when the player resizes.
   *
   * @private
   */
  updateCurrentBreakpoint_() {
    if (!this.responsive()) {
      return;
    }

    const currentBreakpoint = this.currentBreakpoint();
    const currentWidth = this.currentWidth();

    for (let i = 0; i < BREAKPOINT_ORDER.length; i++) {
      const candidateBreakpoint = BREAKPOINT_ORDER[i];
      const maxWidth = this.breakpoints_[candidateBreakpoint];

      if (currentWidth <= maxWidth) {

        // The current breakpoint did not change, nothing to do.
        if (currentBreakpoint === candidateBreakpoint) {
          return;
        }

        // Only remove a class if there is a current breakpoint.
        if (currentBreakpoint) {
          this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]);
        }

        this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]);
        this.breakpoint_ = candidateBreakpoint;
        break;
      }
    }
  }

  /**
   * Removes the current breakpoint.
   *
   * @private
   */
  removeCurrentBreakpoint_() {
    const className = this.currentBreakpointClass();

    this.breakpoint_ = '';

    if (className) {
      this.removeClass(className);
    }
  }

  /**
   * Get or set breakpoints on the player.
   *
   * Calling this method with an object or `true` will remove any previous
   * custom breakpoints and start from the defaults again.
   *
   * @param  {Object|boolean} [breakpoints]
   *         If an object is given, it can be used to provide custom
   *         breakpoints. If `true` is given, will set default breakpoints.
   *         If this argument is not given, will simply return the current
   *         breakpoints.
   *
   * @param  {number} [breakpoints.tiny]
   *         The maximum width for the "vjs-layout-tiny" class.
   *
   * @param  {number} [breakpoints.xsmall]
   *         The maximum width for the "vjs-layout-x-small" class.
   *
   * @param  {number} [breakpoints.small]
   *         The maximum width for the "vjs-layout-small" class.
   *
   * @param  {number} [breakpoints.medium]
   *         The maximum width for the "vjs-layout-medium" class.
   *
   * @param  {number} [breakpoints.large]
   *         The maximum width for the "vjs-layout-large" class.
   *
   * @param  {number} [breakpoints.xlarge]
   *         The maximum width for the "vjs-layout-x-large" class.
   *
   * @param  {number} [breakpoints.huge]
   *         The maximum width for the "vjs-layout-huge" class.
   *
   * @return {Object}
   *         An object mapping breakpoint names to maximum width values.
   */
  breakpoints(breakpoints) {

    // Used as a getter.
    if (breakpoints === undefined) {
      return Object.assign(this.breakpoints_);
    }

    this.breakpoint_ = '';
    this.breakpoints_ = Object.assign({}, DEFAULT_BREAKPOINTS, breakpoints);

    // When breakpoint definitions change, we need to update the currently
    // selected breakpoint.
    this.updateCurrentBreakpoint_();

    // Clone the breakpoints before returning.
    return Object.assign(this.breakpoints_);
  }

  /**
   * Get or set a flag indicating whether or not this player should adjust
   * its UI based on its dimensions.
   *
   * @param  {boolean} value
   *         Should be `true` if the player should adjust its UI based on its
   *         dimensions; otherwise, should be `false`.
   *
   * @return {boolean}
   *         Will be `true` if this player should adjust its UI based on its
   *         dimensions; otherwise, will be `false`.
   */
  responsive(value) {

    // Used as a getter.
    if (value === undefined) {
      return this.responsive_;
    }

    value = Boolean(value);
    const current = this.responsive_;

    // Nothing changed.
    if (value === current) {
      return;
    }

    // The value actually changed, set it.
    this.responsive_ = value;

    // Start listening for breakpoints and set the initial breakpoint if the
    // player is now responsive.
    if (value) {
      this.on('playerresize', this.boundUpdateCurrentBreakpoint_);
      this.updateCurrentBreakpoint_();

    // Stop listening for breakpoints if the player is no longer responsive.
    } else {
      this.off('playerresize', this.boundUpdateCurrentBreakpoint_);
      this.removeCurrentBreakpoint_();
    }

    return value;
  }

  /**
   * Get current breakpoint name, if any.
   *
   * @return {string}
   *         If there is currently a breakpoint set, returns a the key from the
   *         breakpoints object matching it. Otherwise, returns an empty string.
   */
  currentBreakpoint() {
    return this.breakpoint_;
  }

  /**
   * Get the current breakpoint class name.
   *
   * @return {string}
   *         The matching class name (e.g. `"vjs-layout-tiny"` or
   *         `"vjs-layout-large"`) for the current breakpoint. Empty string if
   *         there is no current breakpoint.
   */
  currentBreakpointClass() {
    return BREAKPOINT_CLASSES[this.breakpoint_] || '';
  }

  /**
   * An object that describes a single piece of media.
   *
   * Properties that are not part of this type description will be retained; so,
   * this can be viewed as a generic metadata storage mechanism as well.
   *
   * @see      {@link https://wicg.github.io/mediasession/#the-mediametadata-interface}
   * @typedef  {Object} Player~MediaObject
   *
   * @property {string} [album]
   *           Unused, except if this object is passed to the `MediaSession`
   *           API.
   *
   * @property {string} [artist]
   *           Unused, except if this object is passed to the `MediaSession`
   *           API.
   *
   * @property {Object[]} [artwork]
   *           Unused, except if this object is passed to the `MediaSession`
   *           API. If not specified, will be populated via the `poster`, if
   *           available.
   *
   * @property {string} [poster]
   *           URL to an image that will display before playback.
   *
   * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src]
   *           A single source object, an array of source objects, or a string
   *           referencing a URL to a media source. It is _highly recommended_
   *           that an object or array of objects is used here, so that source
   *           selection algorithms can take the `type` into account.
   *
   * @property {string} [title]
   *           Unused, except if this object is passed to the `MediaSession`
   *           API.
   *
   * @property {Object[]} [textTracks]
   *           An array of objects to be used to create text tracks, following
   *           the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}.
   *           For ease of removal, these will be created as "remote" text
   *           tracks and set to automatically clean up on source changes.
   *
   *           These objects may have properties like `src`, `kind`, `label`,
   *           and `language`, see {@link Tech#createRemoteTextTrack}.
   */

  /**
   * Populate the player using a {@link Player~MediaObject|MediaObject}.
   *
   * @param  {Player~MediaObject} media
   *         A media object.
   *
   * @param  {Function} ready
   *         A callback to be called when the player is ready.
   */
  loadMedia(media, ready) {
    if (!media || typeof media !== 'object') {
      return;
    }

    this.reset();

    // Clone the media object so it cannot be mutated from outside.
    this.cache_.media = merge(media);

    const {artist, artwork, description, poster, src, textTracks, title} = this.cache_.media;

    // If `artwork` is not given, create it using `poster`.
    if (!artwork && poster) {
      this.cache_.media.artwork = [{
        src: poster,
        type: getMimetype(poster)
      }];
    }

    if (src) {
      this.src(src);
    }

    if (poster) {
      this.poster(poster);
    }

    if (Array.isArray(textTracks)) {
      textTracks.forEach(tt => this.addRemoteTextTrack(tt, false));
    }

    if (this.titleBar) {
      this.titleBar.update({
        title,
        description: description || artist || ''
      });
    }

    this.ready(ready);
  }

  /**
   * Get a clone of the current {@link Player~MediaObject} for this player.
   *
   * If the `loadMedia` method has not been used, will attempt to return a
   * {@link Player~MediaObject} based on the current state of the player.
   *
   * @return {Player~MediaObject}
   */
  getMedia() {
    if (!this.cache_.media) {
      const poster = this.poster();
      const src = this.currentSources();
      const textTracks = Array.prototype.map.call(this.remoteTextTracks(), (tt) => ({
        kind: tt.kind,
        label: tt.label,
        language: tt.language,
        src: tt.src
      }));

      const media = {src, textTracks};

      if (poster) {
        media.poster = poster;
        media.artwork = [{
          src: media.poster,
          type: getMimetype(media.poster)
        }];
      }

      return media;
    }

    return merge(this.cache_.media);
  }

  /**
   * Gets tag settings
   *
   * @param {Element} tag
   *        The player tag
   *
   * @return {Object}
   *         An object containing all of the settings
   *         for a player tag
   */
  static getTagSettings(tag) {
    const baseOptions = {
      sources: [],
      tracks: []
    };

    const tagOptions = Dom.getAttributes(tag);
    const dataSetup = tagOptions['data-setup'];

    if (Dom.hasClass(tag, 'vjs-fill')) {
      tagOptions.fill = true;
    }
    if (Dom.hasClass(tag, 'vjs-fluid')) {
      tagOptions.fluid = true;
    }

    // Check if data-setup attr exists.
    if (dataSetup !== null) {
      // Parse options JSON
      // If empty string, make it a parsable json object.
      const [err, data] = safeParseTuple(dataSetup || '{}');

      if (err) {
        log.error(err);
      }
      Object.assign(tagOptions, data);
    }

    Object.assign(baseOptions, tagOptions);

    // Get tag children settings
    if (tag.hasChildNodes()) {
      const children = tag.childNodes;

      for (let i = 0, j = children.length; i < j; i++) {
        const child = children[i];
        // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/
        const childName = child.nodeName.toLowerCase();

        if (childName === 'source') {
          baseOptions.sources.push(Dom.getAttributes(child));
        } else if (childName === 'track') {
          baseOptions.tracks.push(Dom.getAttributes(child));
        }
      }
    }

    return baseOptions;
  }

  /**
   * Set debug mode to enable/disable logs at info level.
   *
   * @param {boolean} enabled
   * @fires Player#debugon
   * @fires Player#debugoff
   */
  debug(enabled) {
    if (enabled === undefined) {
      return this.debugEnabled_;
    }
    if (enabled) {
      this.trigger('debugon');
      this.previousLogLevel_ = this.log.level;
      this.log.level('debug');
      this.debugEnabled_ = true;
    } else {
      this.trigger('debugoff');
      this.log.level(this.previousLogLevel_);
      this.previousLogLevel_ = undefined;
      this.debugEnabled_ = false;
    }

  }

  /**
   * Set or get current playback rates.
   * Takes an array and updates the playback rates menu with the new items.
   * Pass in an empty array to hide the menu.
   * Values other than arrays are ignored.
   *
   * @fires Player#playbackrateschange
   * @param {number[]} newRates
   *                   The new rates that the playback rates menu should update to.
   *                   An empty array will hide the menu
   * @return {number[]} When used as a getter will return the current playback rates
   */
  playbackRates(newRates) {
    if (newRates === undefined) {
      return this.cache_.playbackRates;
    }

    // ignore any value that isn't an array
    if (!Array.isArray(newRates)) {
      return;
    }

    // ignore any arrays that don't only contain numbers
    if (!newRates.every((rate) => typeof rate === 'number')) {
      return;
    }

    this.cache_.playbackRates = newRates;

    /**
    * fires when the playback rates in a player are changed
    *
    * @event Player#playbackrateschange
    * @type {Event}
    */
    this.trigger('playbackrateschange');
  }
}

/**
 * Get the {@link VideoTrackList}
 *
 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist
 *
 * @return {VideoTrackList}
 *         the current video track list
 *
 * @method Player.prototype.videoTracks
 */

/**
 * Get the {@link AudioTrackList}
 *
 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist
 *
 * @return {AudioTrackList}
 *         the current audio track list
 *
 * @method Player.prototype.audioTracks
 */

/**
 * Get the {@link TextTrackList}
 *
 * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks
 *
 * @return {TextTrackList}
 *         the current text track list
 *
 * @method Player.prototype.textTracks
 */

/**
 * Get the remote {@link TextTrackList}
 *
 * @return {TextTrackList}
 *         The current remote text track list
 *
 * @method Player.prototype.remoteTextTracks
 */

/**
 * Get the remote {@link HtmlTrackElementList} tracks.
 *
 * @return {HtmlTrackElementList}
 *         The current remote text track element list
 *
 * @method Player.prototype.remoteTextTrackEls
 */

TRACK_TYPES.names.forEach(function(name) {
  const props = TRACK_TYPES[name];

  Player.prototype[props.getterName] = function() {
    if (this.tech_) {
      return this.tech_[props.getterName]();
    }

    // if we have not yet loadTech_, we create {video,audio,text}Tracks_
    // these will be passed to the tech during loading
    this[props.privateName] = this[props.privateName] || new props.ListClass();
    return this[props.privateName];
  };
});

/**
 * Get or set the `Player`'s crossorigin option. For the HTML5 player, this
 * sets the `crossOrigin` property on the `<video>` tag to control the CORS
 * behavior.
 *
 * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
 *
 * @param {string} [value]
 *        The value to set the `Player`'s crossorigin to. If an argument is
 *        given, must be one of `anonymous` or `use-credentials`.
 *
 * @return {string|undefined}
 *         - The current crossorigin value of the `Player` when getting.
 *         - undefined when setting
 */
Player.prototype.crossorigin = Player.prototype.crossOrigin;

/**
 * Global enumeration of players.
 *
 * The keys are the player IDs and the values are either the {@link Player}
 * instance or `null` for disposed players.
 *
 * @type {Object}
 */
Player.players = {};

const navigator = window.navigator;

/*
 * Player instance options, surfaced using options
 * options = Player.prototype.options_
 * Make changes in options, not here.
 *
 * @type {Object}
 * @private
 */
Player.prototype.options_ = {
  // Default order of fallback technology
  techOrder: Tech.defaultTechOrder_,

  html5: {},

  // enable sourceset by default
  enableSourceset: true,

  // default inactivity timeout
  inactivityTimeout: 2000,

  // default playback rates
  playbackRates: [],
  // Add playback rate selection by adding rates
  // 'playbackRates': [0.5, 1, 1.5, 2],
  liveui: false,

  // Included control sets
  children: [
    'mediaLoader',
    'posterImage',
    'titleBar',
    'textTrackDisplay',
    'loadingSpinner',
    'bigPlayButton',
    'liveTracker',
    'controlBar',
    'errorDisplay',
    'textTrackSettings',
    'resizeManager'
  ],

  language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en',

  // locales and their language translations
  languages: {},

  // Default message to show when a video cannot be played.
  notSupportedMessage: 'No compatible source was found for this media.',

  normalizeAutoplay: false,

  fullscreen: {
    options: {
      navigationUI: 'hide'
    }
  },

  breakpoints: {},
  responsive: false,
  audioOnlyMode: false,
  audioPosterMode: false
};

[
  /**
   * Returns whether or not the player is in the "ended" state.
   *
   * @return {Boolean} True if the player is in the ended state, false if not.
   * @method Player#ended
   */
  'ended',
  /**
   * Returns whether or not the player is in the "seeking" state.
   *
   * @return {Boolean} True if the player is in the seeking state, false if not.
   * @method Player#seeking
   */
  'seeking',
  /**
   * Returns the TimeRanges of the media that are currently available
   * for seeking to.
   *
   * @return {TimeRanges} the seekable intervals of the media timeline
   * @method Player#seekable
   */
  'seekable',
  /**
   * Returns the current state of network activity for the element, from
   * the codes in the list below.
   * - NETWORK_EMPTY (numeric value 0)
   *   The element has not yet been initialised. All attributes are in
   *   their initial states.
   * - NETWORK_IDLE (numeric value 1)
   *   The element's resource selection algorithm is active and has
   *   selected a resource, but it is not actually using the network at
   *   this time.
   * - NETWORK_LOADING (numeric value 2)
   *   The user agent is actively trying to download data.
   * - NETWORK_NO_SOURCE (numeric value 3)
   *   The element's resource selection algorithm is active, but it has
   *   not yet found a resource to use.
   *
   * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states
   * @return {number} the current network activity state
   * @method Player#networkState
   */
  'networkState',
  /**
   * Returns a value that expresses the current state of the element
   * with respect to rendering the current playback position, from the
   * codes in the list below.
   * - HAVE_NOTHING (numeric value 0)
   *   No information regarding the media resource is available.
   * - HAVE_METADATA (numeric value 1)
   *   Enough of the resource has been obtained that the duration of the
   *   resource is available.
   * - HAVE_CURRENT_DATA (numeric value 2)
   *   Data for the immediate current playback position is available.
   * - HAVE_FUTURE_DATA (numeric value 3)
   *   Data for the immediate current playback position is available, as
   *   well as enough data for the user agent to advance the current
   *   playback position in the direction of playback.
   * - HAVE_ENOUGH_DATA (numeric value 4)
   *   The user agent estimates that enough data is available for
   *   playback to proceed uninterrupted.
   *
   * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate
   * @return {number} the current playback rendering state
   * @method Player#readyState
   */
  'readyState'
].forEach(function(fn) {
  Player.prototype[fn] = function() {
    return this.techGet_(fn);
  };
});

TECH_EVENTS_RETRIGGER.forEach(function(event) {
  Player.prototype[`handleTech${toTitleCase(event)}_`] = function() {
    return this.trigger(event);
  };
});

/**
 * Fired when the player has initial duration and dimension information
 *
 * @event Player#loadedmetadata
 * @type {Event}
 */

/**
 * Fired when the player has downloaded data at the current playback position
 *
 * @event Player#loadeddata
 * @type {Event}
 */

/**
 * Fired when the current playback position has changed *
 * During playback this is fired every 15-250 milliseconds, depending on the
 * playback technology in use.
 *
 * @event Player#timeupdate
 * @type {Event}
 */

/**
 * Fired when the volume changes
 *
 * @event Player#volumechange
 * @type {Event}
 */

/**
 * Reports whether or not a player has a plugin available.
 *
 * This does not report whether or not the plugin has ever been initialized
 * on this player. For that, [usingPlugin]{@link Player#usingPlugin}.
 *
 * @method Player#hasPlugin
 * @param  {string}  name
 *         The name of a plugin.
 *
 * @return {boolean}
 *         Whether or not this player has the requested plugin available.
 */

/**
 * Reports whether or not a player is using a plugin by name.
 *
 * For basic plugins, this only reports whether the plugin has _ever_ been
 * initialized on this player.
 *
 * @method Player#usingPlugin
 * @param  {string} name
 *         The name of a plugin.
 *
 * @return {boolean}
 *         Whether or not this player is using the requested plugin.
 */

Component.registerComponent('Player', Player);
export default Player;