    /**
     * @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 tsml from 'tsml';
    import evented 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 log from './utils/log.js';
    import toTitleCase, { titleCaseEquals } from './utils/to-title-case.js';
    import { createTimeRange } from './utils/time-ranges.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 {assign} from './utils/obj';
    import mergeOptions from './utils/merge-options.js';
    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';

    // 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 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 {EventTarget~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 {EventTarget~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 {EventTarget~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 {EventTarget~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 {EventTarget~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 {EventTarget~Event}
       */
      /**
       * Retrigger the `stalled` 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 playing speed of the audio/video is changed
       *
       * @event Player#ratechange
       * @type {event}
       */
      /**
       * Retrigger the `ratechange` event that was triggered by the {@link Tech}.
       *
       * @private
       * @method Player#handleTechRatechange_
       * @fires Player#ratechange
       * @listens Tech#ratechange
       */
      'ratechange',

      /**
       * 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'
    ];

    /**
     * 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 two ways:
     * 1. By calling `videojs('example_video_1');`
     * 2. By using it directly via  `videojs.players.example_video_1;`
     *
     * @extends Component
     */
    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 {Component~ReadyCallback} [ready]
       *        Ready callback function.
       */
      constructor(tag, options, ready) {
        // Make sure tag ID exists
        tag.id = tag.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 = 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;

        // 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) {
          if (typeof tag.closest === 'function') {
            const closest = tag.closest('[lang]');

            if (closest) {
              options.language = closest.getAttribute('lang');
            }
          } else {
            let element = tag;

            while (element && element.nodeType === 1) {
              if (Dom.getAttributes(element).hasOwnProperty('lang')) {
                options.language = element.getAttribute('lang');
                break;
              }
              element = element.parentNode;
            }
          }
        }

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

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

        // 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;
        }

        // Cache for video property values.
        this.cache_ = {};

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

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

        // Set default values for lastVolume
        this.cache_.lastVolume = 1;

        // 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;

        /*
         * 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_'});

        // 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 = mergeOptions(this.options_);

        // Load plugins
        if (options.plugins) {
          const plugins = options.plugins;

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

        this.options_.playerOptions = playerOptionsCopy;

        this.middleware_ = [];

        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');
        }

        if (this.flexNotSupported_()) {
          this.addClass('vjs-no-flex');
        }

        // TODO: Make this smarter. Toggle user state between touching/mousing
        // using events, since devices can have both touch and mouse events.
        // 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.listenForUserActivity_();

        this.on('fullscreenchange', this.handleFullscreenChange_);
        this.on('stageclick', this.handleStageClick_);

        this.changingSrc_ = false;
      }

      /**
       * 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 {EventTarget~Event}
         */
        this.trigger('dispose');
        // prevent dispose from being called twice
        this.off('dispose');

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

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

        super.dispose();
      }

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

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

        // set tabindex to -1 so we could focus on the player element
        tag.setAttribute('tabindex', '-1');

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

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

        Object.getOwnPropertyNames(attrs).forEach(function(attr) {
          // workaround so we don't totally break IE7
          // http://stackoverflow.com/questions/3653444/css-styles-not-applied-on-dynamic-elements-in-internet-explorer-7
          if (attr === 'class') {
            el.className += ' ' + attrs[attr];
          } else {
            el.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 overrideable 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);
        }

        // Pass in the width/height/aspectRatio options which will update the style el
        this.width(this.options_.width);
        this.height(this.options_.height);
        this.fluid(this.options_.fluid);
        this.aspectRatio(this.options_.aspectRatio);

        // Hide any links within the video/audio tag, because IE doesn't hide them completely.
        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_ = el;

        return el;
      }

      /**
       * A getter/setter for the `Player`'s width.
       *
       * @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.
       *
       * @param {number} [value]
       *        The value to set the `Player's heigth 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 === '') {
          // If an empty string is given, reset the dimension to be automatic
          this[privDimension] = undefined;
        } else {
          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`.
       *
       * @param {boolean} [bool]
       *        - A value of true adds the class.
       *        - A value of false removes the class.
       *        - No value will toggle the fluid class.
       *
       * @return {boolean|undefined}
       *         - The value of fluid when getting.
       *         - `undefined` when setting.
       */
      fluid(bool) {
        if (bool === undefined) {
          return !!this.fluid_;
        }

        this.fluid_ = !!bool;

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

        this.updateStyleEl_();
      }

      /**
       * 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 calulate 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 {
            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;

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

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

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

        assign(techOptions, this.options_[titleTechName]);
        assign(techOptions, this.options_[camelTechName]);
        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, this[`handleTech${toTitleCase(event)}_`]);
        });
        this.on(this.tech_, 'loadstart', this.handleTechLoadStart_);
        this.on(this.tech_, 'waiting', this.handleTechWaiting_);
        this.on(this.tech_, 'canplay', this.handleTechCanPlay_);
        this.on(this.tech_, 'canplaythrough', this.handleTechCanPlayThrough_);
        this.on(this.tech_, 'playing', this.handleTechPlaying_);
        this.on(this.tech_, 'ended', this.handleTechEnded_);
        this.on(this.tech_, 'seeking', this.handleTechSeeking_);
        this.on(this.tech_, 'seeked', this.handleTechSeeked_);
        this.on(this.tech_, 'play', this.handleTechPlay_);
        this.on(this.tech_, 'firstplay', this.handleTechFirstPlay_);
        this.on(this.tech_, 'pause', this.handleTechPause_);
        this.on(this.tech_, 'durationchange', this.handleTechDurationChange_);
        this.on(this.tech_, 'fullscreenchange', this.handleTechFullscreenChange_);
        this.on(this.tech_, 'error', this.handleTechError_);
        this.on(this.tech_, 'loadedmetadata', this.updateStyleEl_);
        this.on(this.tech_, 'posterchange', this.handleTechPosterChange_);
        this.on(this.tech_, 'textdata', this.handleTechTextData_);

        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;
      }

      /**
       * 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(tsml`
            Using the tech directly can be dangerous. I hope you know what you're doing.
            See https://github.com/videojs/video.js/issues/2617 for more info.
          `);
        }

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

        // Some browsers (Chrome & IE) don't trigger a click on a flash swf, but do
        // trigger mousedown/up.
        // http://stackoverflow.com/questions/1444562/javascript-onclick-event-over-flash-object
        // Any touch events are set to block the mousedown event from happening
        this.on(this.tech_, 'mousedown', this.handleTechClick_);

        // 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.handleTechTouchStart_);
        this.on(this.tech_, 'touchmove', this.handleTechTouchMove_);
        this.on(this.tech_, 'touchend', this.handleTechTouchEnd_);

        // 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.handleTechTap_);
      }

      /**
       * 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.handleTechTap_);
        this.off(this.tech_, 'touchstart', this.handleTechTouchStart_);
        this.off(this.tech_, 'touchmove', this.handleTechTouchMove_);
        this.off(this.tech_, 'touchend', this.handleTechTouchEnd_);
        this.off(this.tech_, 'mousedown', this.handleTechClick_);
      }

      /**
       * 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_();

        // Chrome and Safari both have issues with autoplay.
        // In Safari (5.1.1), when we move the video element into the container div, autoplay doesn't work.
        // In Chrome (15), if you have autoplay + a poster + no controls, the video gets hidden (but audio plays)
        // This fixes both issues. Need to wait for API, so it updates displays correctly
        if ((this.src() || this.currentSrc()) && this.tag && this.options_.autoplay && this.paused()) {
          try {
            // Chrome Fix. Fixed in Chrome v16.
            delete this.tag.poster;
          } catch (e) {
            log('deleting tag.poster throws in some browsers', e);
          }
          this.play();
        }
      }

      /**
       * Retrigger the `loadstart` event that was triggered by the {@link Tech}. This
       * function will also trigger {@link Player#firstplay} if it is the first loadstart
       * for a video.
       *
       * @fires Player#loadstart
       * @fires Player#firstplay
       * @listens Tech#loadstart
       * @private
       */
      handleTechLoadStart_() {
        // TODO: Update to use `emptied` event instead. See #1277.

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

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

        // If it's already playing we want to trigger a firstplay event now.
        // The firstplay event relies on both the play and loadstart events
        // which can happen in any order for a new source
        if (!this.paused()) {
          /**
           * Fired when the user agent begins looking for media data
           *
           * @event Player#loadstart
           * @type {EventTarget~Event}
           */
          this.trigger('loadstart');
          this.trigger('firstplay');
        } else {
          // reset the hasStarted state
          this.hasStarted(false);
          this.trigger('loadstart');
        }
      }

      /**
       * Add/remove the vjs-has-started class
       *
       * @fires Player#firstplay
       *
       * @param {boolean} hasStarted
       *        - true: adds the class
       *        - false: remove the class
       *
       * @return {boolean}
       *         the boolean value of hasStarted
       */
      hasStarted(hasStarted) {
        if (hasStarted !== undefined) {
          // only update if this is a new value
          if (this.hasStarted_ !== hasStarted) {
            this.hasStarted_ = hasStarted;
            if (hasStarted) {
              this.addClass('vjs-has-started');
              // trigger the firstplay event if this newly has played
              this.trigger('firstplay');
            } else {
              this.removeClass('vjs-has-started');
            }
          }
          return;
        }
        return !!this.hasStarted_;
      }

      /**
       * 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');
        this.removeClass('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 {EventTarget~Event}
         */
        this.trigger('play');
      }

      /**
       * 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 {EventTarget~Event}
         */
        this.trigger('waiting');
        this.one('timeupdate', () => this.removeClass('vjs-waiting'));
      }

      /**
       * 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 {EventTarget~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 {EventTarget~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 {EventTarget~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 {EventTarget~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');
        /**
         * Fired when the player has finished jumping to a new time
         *
         * @event Player#seeked
         * @type {EventTarget~Event}
         */
        this.trigger('seeked');
      }

      /**
       * Retrigger the `firstplay` event that was triggered by the {@link Tech}.
       *
       * @fires Player#firstplay
       * @listens Tech#firstplay
       * @deprecated As of 6.0 firstplay event is deprecated.
       * @deprecated As of 6.0 passing the `starttime` option to the player and the firstplay event are deprecated.
       * @private
       */
      handleTechFirstPlay_() {
        // If the first starttime attribute is specified
        // then we will start at the given offset in seconds
        if (this.options_.starttime) {
          log.warn('Passing the `starttime` option to the player will be deprecated in 6.0');
          this.currentTime(this.options_.starttime);
        }

        this.addClass('vjs-has-started');
        /**
         * Fired the first time a video is played. Not part of the HLS spec, and this is
         * probably not the best implementation yet, so use sparingly. If you don't have a
         * reason to prevent playback, use `myPlayer.one('play');` instead.
         *
         * @event Player#firstplay
         * @deprecated As of 6.0 firstplay event is deprecated.
         * @type {EventTarget~Event}
         */
        this.trigger('firstplay');
      }

      /**
       * 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 {EventTarget~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');
        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 {EventTarget~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 {EventTarget~Event} event
       *        the event that caused this function to trigger
       *
       * @listens Tech#mousedown
       * @private
       */
      handleTechClick_(event) {
        // We're using mousedown to detect clicks thanks to Flash, but mousedown
        // will also be triggered with right-clicks, so we need to prevent that
        if (event.button !== 0) {
          return;
        }

        // When controls are disabled a click should not toggle playback because
        // the click is considered a control
        if (this.controls()) {
          if (this.paused()) {
            this.play();
          } else {
            this.pause();
          }
        }
      }

      /**
       * 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 {EventTarget~Event} event
       *        the touchend event that triggered
       *        this function
       *
       * @listens Tech#touchend
       * @private
       */
      handleTechTouchEnd_(event) {
        // Stop the mouse events from also happening
        event.preventDefault();
      }

      /**
       * Fired when the player switches in or out of fullscreen mode
       *
       * @private
       * @listens Player#fullscreenchange
       */
      handleFullscreenChange_() {
        if (this.isFullscreen()) {
          this.addClass('vjs-fullscreen');
        } else {
          this.removeClass('vjs-fullscreen');
        }
      }

      /**
       * native click events on the SWF aren't triggered on IE11, Win8.1RT
       * use stageclick events triggered from inside the SWF instead
       *
       * @private
       * @listens stageclick
       */
      handleStageClick_() {
        this.reportUserActivity();
      }

      /**
       * Handle Tech Fullscreen Change
       *
       * @param {EventTarget~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) {
          this.isFullscreen(data.isFullscreen);
        }
        /**
         * Fired when going in and out of fullscreen.
         *
         * @event Player#fullscreenchange
         * @type {EventTarget~Event}
         */
        this.trigger('fullscreenchange');
      }

      /**
       * 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 {EventTarget~Event}
         */
        this.trigger('textdata', data);
      }

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

      /**
       * 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);
          }

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

      /**
       * Get calls can't wait for the tech, and sometimes don't need to.
       *
       * @param {string} method
       *        Tech method
       *
       * @return {Function|undefined}
       *         the method or undefined
       *
       * @private
       */
      techGet_(method) {
        if (this.tech_ && this.tech_.isReady_) {

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

          // Flash likes to die and reload when you hide or reposition it.
          // In these cases the object methods go away and we get errors.
          // When that happens we'll catch the errors and inform tech that it's not ready any more.
          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);

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

        return;
      }

      /**
       * start media playback
       *
       * @return {Promise|undefined}
       *         Returns a `Promise` if the browser returns one, for most browsers this will
       *         return undefined.
       */
      play() {
        if (this.changingSrc_) {
          this.ready(function() {
            const retval = this.techGet_('play');

            // silence errors (unhandled promise from play)
            if (retval !== undefined && typeof retval.then === 'function') {
              retval.then(null, (e) => {});
            }
          });

        // Only calls the tech's play if we already have a src loaded
        } else if (this.isReady_ && (this.src() || this.currentSrc())) {
          return this.techGet_('play');
        } else {
          this.ready(function() {
            this.tech_.one('loadstart', function() {
              const retval = this.play();

              // silence errors (unhandled promise from play)
              if (retval !== undefined && typeof retval.then === 'function') {
                retval.then(null, (e) => {});
              }
            });
          });
        }
      }

      /**
       * 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 {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]
       *        wether 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;

        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') {
          this.techCall_('setCurrentTime', seconds);
          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 afterall.
        // 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;
      }

      /**
       * 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 in the case of Flash, 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 Inifity for signaling video is live
        if (seconds < 0) {
          seconds = Infinity;
        }

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

          if (seconds === Infinity) {
            this.addClass('vjs-live');
          } else {
            this.removeClass('vjs-live');
          }
          /**
           * @event Player#durationchange
           * @type {EventTarget~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();
      }

      //
      // 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 {TimeRange}
       *         A mock TimeRange 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 bufferred 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 intial 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, so not our flash swf)
       *
       * @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) {
          this.isFullscreen_ = !!isFS;
          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.
       *
       * @fires Player#fullscreenchange
       */
      requestFullscreen() {
        const fsApi = FullscreenApi;

        this.isFullscreen(true);

        if (fsApi.requestFullscreen) {
          // the browser supports going fullscreen at the element level so we can
          // take the controls fullscreen as well as the video

          // Trigger fullscreenchange event after change
          // We have to specifically add this each time, and remove
          // when canceling fullscreen. Otherwise if there's multiple
          // players on a page, they would all be reacting to the same fullscreen
          // events
          Events.on(document, fsApi.fullscreenchange, Fn.bind(this, function documentFullscreenChange(e) {
            this.isFullscreen(document[fsApi.fullscreenElement]);

            // If cancelling fullscreen, remove event listener.
            if (this.isFullscreen() === false) {
              Events.off(document, fsApi.fullscreenchange, documentFullscreenChange);
            }
            /**
             * @event Player#fullscreenchange
             * @type {EventTarget~Event}
             */
            this.trigger('fullscreenchange');
          }));

          this.el_[fsApi.requestFullscreen]();

        } else if (this.tech_.supportsFullScreen()) {
          // 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();
          /**
           * @event Player#fullscreenchange
           * @type {EventTarget~Event}
           */
          this.trigger('fullscreenchange');
        }
      }

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

        this.isFullscreen(false);

        // Check for browser element fullscreen support
        if (fsApi.requestFullscreen) {
          document[fsApi.exitFullscreen]();
        } else if (this.tech_.supportsFullScreen()) {
          this.techCall_('exitFullScreen');
        } else {
          this.exitFullWindow();
          /**
           * @event Player#fullscreenchange
           * @type {EventTarget~Event}
           */
          this.trigger('fullscreenchange');
        }
      }

      /**
       * 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.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', Fn.bind(this, this.fullWindowOnEscKey));

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

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

        /**
         * @event Player#enterFullWindow
         * @type {EventTarget~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 (event.keyCode === 27) {
          if (this.isFullscreen() === true) {
            this.exitFullscreen();
          } else {
            this.exitFullWindow();
          }
        }
      }

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

        // 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 {EventTarget~Event}
         */
        this.trigger('exitFullWindow');
      }

      /**
       * 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;
      }

      /**
       * 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) {
        // getter usage
        if (typeof source === 'undefined') {
          return this.cache_.src;
        }
        // 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.localize(this.options_.notSupportedMessage) });
          }, 0);
          return;
        }

        // intial sources
        this.cache_.sources = sources;
        this.changingSrc_ = true;

        // intial source
        this.cache_.source = sources[0];

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

          const err = this.src_(middlewareSource);

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

            // 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.localize(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;
          }

          this.changingSrc_ = false;
          // video element listed source
          this.cache_.src = middlewareSource.src;

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

      /**
       * Set the source object on the tech, returns a boolean that indicates wether
       * 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);
          return false;
        }

        // wait until the tech is ready to set the source
        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);
          }

          if (this.options_.preload === 'auto') {
            this.load();
          }

          if (this.options_.autoplay) {
            this.play();
          }

        // Set the source synchronously if possible (#2326)
        }, true);

        return false;
      }

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

      /**
       * Reset the player. Loads the first tech in the techOrder,
       * and calls `reset` on the tech`.
       */
      reset() {
        this.loadTech_(this.options_.techOrder[0], null);
        this.techCall_('reset');
      }

      /**
       * 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 conjuction 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 maens 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 attribute.
       *
       * @param {boolean} [value]
       *        - true means that we should autoplay
       *        - false means that we should not autoplay
       *
       * @return {string}
       *         The current value of autoplay when getting
       */
      autoplay(value) {
        if (value !== undefined) {
          this.techCall_('setAutoplay', value);
          this.options_.autoplay = value;
          return;
        }
        return this.techGet_('autoplay', value);
      }

      /**
       * 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 {string}
       *         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 = '';
        }

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

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

        // alert components that the poster has been set
        /**
         * This event fires when the poster image is changed on the player.
         *
         * @event Player#posterchange
         * @type {EventTarget~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.tech_ && this.tech_.poster) {
          this.poster_ = this.tech_.poster() || '';

          // 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) {
          bool = !!bool;

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

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

            if (bool) {
              this.removeClass('vjs-controls-disabled');
              this.addClass('vjs-controls-enabled');
              /**
               * @event Player#controlsenabled
               * @type {EventTarget~Event}
               */
              this.trigger('controlsenabled');

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

              if (!this.usingNativeControls()) {
                this.removeTechControlsListeners_();
              }
            }
          }
          return;
        }
        return !!this.controls_;
      }

      /**
       * Toggle native controls on/off. Native controls are the controls built into
       * devices (e.g. default iPhone controls), Flash, 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) {
          bool = !!bool;

          // Don't trigger a change event unless it actually changed
          if (this.usingNativeControls_ !== bool) {
            this.usingNativeControls_ = bool;
            if (bool) {
              this.addClass('vjs-using-native-controls');

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

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

      /**
       * 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;
        }

        // 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
        // ie8 just logs "[object object]" if you just log the error object
        log.error(`(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, this.error_.message, this.error_);

        /**
         * @event Player#error
         * @type {EventTarget~Event}
         */
        this.trigger('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) {
          bool = !!bool;
          if (bool !== this.userActive_) {
            this.userActive_ = bool;
            if (bool) {
              // If the user was inactive and is now active we want to reset the
              // inactivity timer
              this.userActivity_ = true;
              this.removeClass('vjs-user-inactive');
              this.addClass('vjs-user-active');
              /**
               * @event Player#useractive
               * @type {EventTarget~Event}
               */
              this.trigger('useractive');
            } else {
              // We're switching the state to inactive manually, so erase any other
              // activity
              this.userActivity_ = false;

              // 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.removeClass('vjs-user-active');
              this.addClass('vjs-user-inactive');
              /**
               * @event Player#userinactive
               * @type {EventTarget~Event}
               */
              this.trigger('userinactive');
            }
          }
          return;
        }
        return this.userActive_;
      }

      /**
       * 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 handleMouseUp = 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', handleMouseUp);

        // 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_) {
            // 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) {
              // 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) {
          this.techCall_('setPlaybackRate', rate);
          return;
        }

        if (this.tech_ && this.tech_.featuresPlaybackRate) {
          return 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 intial 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_;
      }

      /**
       * 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}. It will
       * automatically removed from the video element whenever the source changes, unless
       * manualCleanup is set to false.
       *
       * @param {Object} options
       *        Options to pass to {@link HTMLTrackElement} during creation. See
       *        {@link HTMLTrackElement} for object properties that you should use.
       *
       * @param {boolean} [manualCleanup=true] if set to false, the TextTrack will be
       *
       * @return {HtmlTrackElement}
       *         the HTMLTrackElement that was created and added
       *         to the HtmlTrackElementList and the remote
       *         TextTrackList
       *
       * @deprecated The default value of the "manualCleanup" parameter will default
       *             to "false" in upcoming versions of Video.js
       */
      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({track = arguments[0]} = {}) {
        // 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
       * NOTE: The language should be set in the player options if you want the
       * the controls to be built with a specific language. Changing the lanugage
       * later will not update controls text.
       *
       * @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_;
        }

        this.language_ = String(code).toLowerCase();
      }

      /**
       * 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 mergeOptions(Player.prototype.options_.languages, this.languages_);
      }

      /**
       * returns a JavaScript object reperesenting the current track
       * information. **DOES not return it as JSON**
       *
       * @return {Object}
       *         Object representing the current of track info
       */
      toJSON() {
        const options = mergeOptions(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 = mergeOptions(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;
      }

      /**
       * 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-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);
          }
          assign(tagOptions, data);
        }

        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;
      }

      /**
       * Determine wether or not flexbox is supported
       *
       * @return {boolean}
       *         - true if flexbox is supported
       *         - false if flexbox is not supported
       */
      flexNotSupported_() {
        const elem = document.createElement('i');

        // Note: We don't actually use flexBasis (or flexOrder), but it's one of the more
        // common flex features that we can rely on when checking for flex support.
        return !('flexBasis' in elem.style ||
                'webkitFlexBasis' in elem.style ||
                'mozFlexBasis' in elem.style ||
                'msFlexBasis' in elem.style ||
                // IE10-specific (2012 flex spec)
                'msFlexOrder' in elem.style);
      }
    }

    /**
     * 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];
      };
    });

    /**
     * Global player list
     *
     * @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: {},
      flash: {},

      // default inactivity timeout
      inactivityTimeout: 2000,

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

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

      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.'
    };

    [
      /**
       * 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 {EventTarget~Event}
     */

    /**
     * Fired when the player has downloaded data at the current playback position
     *
     * @event Player#loadeddata
     * @type {EventTarget~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 {EventTarget~Event}
     */

    /**
     * Fired when the volume changes
     *
     * @event Player#volumechange
     * @type {EventTarget~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;