/* eslint-env qunit */
import sinon from 'sinon';
import Plugin from '../../src/js/plugin';
import TestHelpers from './test-helpers';

QUnit.module('Plugin: advanced', {

  beforeEach() {
    this.player = TestHelpers.makePlayer();
    const spy = this.spy = sinon.spy();

    class MockPlugin extends Plugin {

      constructor(...args) {
        super(...args);
        spy.apply(this, args);
      }
    }

    MockPlugin.VERSION = '1.0.0';

    this.MockPlugin = MockPlugin;
    Plugin.registerPlugin('mock', MockPlugin);
  },

  afterEach() {
    this.player.dispose();

    Object.keys(Plugin.getPlugins()).forEach(key => {
      if (key !== Plugin.BASE_PLUGIN_NAME) {
        Plugin.deregisterPlugin(key);
      }
    });
  }
});

QUnit.test('pre-setup interface', function(assert) {
  assert.strictEqual(typeof this.player.plugin, 'undefined', 'the base Plugin does not add a method to the player');
  assert.strictEqual(typeof this.player.mock, 'function', 'plugins are a factory function on a player');
  assert.ok(this.player.hasPlugin('mock'), 'player has the plugin available');
  assert.strictEqual(this.player.mock.dispose, undefined, 'class-based plugins are not populated on a player until the factory method creates them');
  assert.notOk(this.player.usingPlugin('mock'), 'the player is not using the plugin');
});

QUnit.test('setup', function(assert) {
  const instance = this.player.mock({foo: 'bar'}, 123);

  assert.strictEqual(this.spy.callCount, 1, 'plugin was set up once');
  assert.strictEqual(this.spy.firstCall.thisValue, instance, 'plugin constructor `this` value was the instance');
  assert.deepEqual(this.spy.firstCall.args, [this.player, {foo: 'bar'}, 123], 'plugin had the correct arguments');
  assert.ok(this.player.usingPlugin('mock'), 'player now recognizes that the plugin was set up');
  assert.ok(this.player.hasPlugin('mock'), 'player has the plugin available');
  assert.ok(instance instanceof this.MockPlugin, 'plugin instance has the correct constructor');
  assert.strictEqual(instance, this.player.mock(), 'factory is replaced by method returning the instance');
  assert.strictEqual(instance.player, this.player, 'instance has a reference to the player');
  assert.strictEqual(instance.name, 'mock', 'instance knows its name');
  assert.strictEqual(typeof instance.state, 'object', 'instance is stateful');
  assert.strictEqual(typeof instance.setState, 'function', 'instance is stateful');
  assert.strictEqual(typeof instance.off, 'function', 'instance is evented');
  assert.strictEqual(typeof instance.on, 'function', 'instance is evented');
  assert.strictEqual(typeof instance.one, 'function', 'instance is evented');
  assert.strictEqual(typeof instance.trigger, 'function', 'instance is evented');
  assert.strictEqual(typeof instance.dispose, 'function', 'instance has dispose method');
  assert.strictEqual(typeof instance.version, 'function', 'instance has version method');
  assert.strictEqual(instance.version(), '1.0.0', 'version function returns VERSION value');

  assert.throws(
    () => new Plugin(this.player),
    new Error('Plugin must be sub-classed; not directly instantiated.'),
    'the Plugin class cannot be directly instantiated'
  );
});

QUnit.test('log is added by default', function(assert) {
  const instance = this.player.mock();

  assert.strictEqual(typeof instance.log, 'function', 'log is a function');
  assert.strictEqual(typeof instance.log.debug, 'function', 'log.debug is a function');
  assert.strictEqual(typeof instance.log.error, 'function', 'log.error is a function');
  assert.strictEqual(typeof instance.log.history, 'function', 'log.history is a function');
  assert.strictEqual(typeof instance.log.levels, 'object', 'log.levels is a object');
  assert.strictEqual(typeof instance.log.warn, 'function', 'log.warn is a function');
});

QUnit.test('log will not clobber pre-existing log property', function(assert) {
  class MockLogPlugin extends Plugin {
    log() {}
  }

  MockLogPlugin.VERSION = '1.0.0';
  Plugin.registerPlugin('mockLog', MockLogPlugin);

  const instance = this.player.mockLog();

  assert.strictEqual(typeof instance.log, 'function', 'log is a function');
  assert.strictEqual(instance.log, MockLogPlugin.prototype.log, 'log was not overridden');
});

QUnit.test('all "pluginsetup" events', function(assert) {
  const setupSpy = sinon.spy();
  const events = [
    'beforepluginsetup',
    'beforepluginsetup:mock',
    'pluginsetup',
    'pluginsetup:mock'
  ];

  this.player.on(events, setupSpy);

  const instance = this.player.mock();

  events.forEach((type, i) => {
    const event = setupSpy.getCall(i).args[0];
    const hash = setupSpy.getCall(i).args[1];

    assert.strictEqual(event.type, type, `the "${type}" event was triggered`);
    assert.strictEqual(event.target, this.player.el_, 'the event has the correct target');

    assert.deepEqual(hash, {
      name: 'mock',

      // The "before" events have a `null` instance and the others have the
      // return value of the plugin factory.
      instance: i < 2 ? null : instance,
      plugin: this.MockPlugin
    }, 'the event hash object is correct');
  });
});

QUnit.test('defaultState static property is used to populate state', function(assert) {
  class DefaultStateMock extends Plugin {}
  DefaultStateMock.defaultState = {foo: 1, bar: 2};
  Plugin.registerPlugin('dsm', DefaultStateMock);

  const instance = this.player.dsm();

  assert.deepEqual(instance.state, {foo: 1, bar: 2}, 'the plugin state has default properties');
});

QUnit.test('dispose', function(assert) {
  const instance = this.player.mock();

  instance.dispose();

  assert.notOk(this.player.usingPlugin('mock'), 'player recognizes that the plugin is NOT set up');
  assert.ok(this.player.hasPlugin('mock'), 'player still has the plugin available');
  assert.strictEqual(typeof this.player.mock, 'function', 'instance is replaced by factory');
  assert.notStrictEqual(instance, this.player.mock, 'instance is replaced by factory');
  assert.strictEqual(instance.player, null, 'instance no longer has a reference to the player');
  assert.strictEqual(instance.state, null, 'state is now null');
});

QUnit.test('"dispose" event', function(assert) {
  const disposeSpy = sinon.spy();
  const instance = this.player.mock();

  instance.on('dispose', disposeSpy);
  instance.dispose();

  assert.strictEqual(disposeSpy.callCount, 1, 'the "dispose" event was triggered');

  const event = disposeSpy.firstCall.args[0];
  const hash = disposeSpy.firstCall.args[1];

  assert.strictEqual(event.type, 'dispose', 'the event has the correct type');
  assert.strictEqual(event.target, instance.eventBusEl_, 'the event has the correct target');

  assert.deepEqual(hash, {
    name: 'mock',
    instance,
    plugin: this.MockPlugin
  }, 'the event hash object is correct');
});

QUnit.test('arbitrary events', function(assert) {
  const fooSpy = sinon.spy();
  const instance = this.player.mock();

  instance.on('foo', fooSpy);
  instance.trigger('foo');

  const event = fooSpy.firstCall.args[0];
  const hash = fooSpy.firstCall.args[1];

  assert.strictEqual(fooSpy.callCount, 1, 'the "foo" event was triggered');
  assert.strictEqual(event.type, 'foo', 'the event has the correct type');
  assert.strictEqual(event.target, instance.eventBusEl_, 'the event has the correct target');

  assert.deepEqual(hash, {
    name: 'mock',
    instance,
    plugin: this.MockPlugin
  }, 'the event hash object is correct');
});

QUnit.test('handleStateChanged() method is automatically bound to the "statechanged" event', function(assert) {
  const spy = sinon.spy();

  class TestHandler extends Plugin {
    handleStateChanged(...args) {
      spy.apply(this, args);
    }
  }
  Plugin.registerPlugin('testHandler', TestHandler);

  const instance = this.player.testHandler();

  instance.setState({foo: 1});
  assert.strictEqual(spy.callCount, 1, 'the handleStateChanged listener was called');
  assert.strictEqual(spy.firstCall.args[0].type, 'statechanged', 'the event was "statechanged"');
  assert.strictEqual(typeof spy.firstCall.args[0].changes, 'object', 'the event included a changes object');
});


assert.equal(currentTimeDisplay.textNode_.textContent, '0:00', 'current time display is 0:00');

assert.equal(durationDisplay.textNode_.textContent, '0:00', 'duration display is 0:00');

assert.equal(durationDisplay.textNode_.textContent, '-:-', 'duration display is -:-');

网络错误导致视频下载中途失败。

视频因格式不支持或者服务器或网络的问题无法加载。

由于视频文件损坏或是该视频使用了你的浏览器不支持的功能，播放终止。

无法找到此视频兼容的源。

可以按ESC按键或启用关闭按钮来关闭此弹窗。

文字

Context: 背景

打开对话窗口。Escape键将取消并关闭对话窗口

尝试直播，当前为延时播放

尝试直播，当前为实时播放


assert.equal(remainingTimeDisplay.textNode_.textContent, '-:-', 'remaining time display is -:-');

/* eslint-env qunit */
import TestHelpers from './test-helpers.js';
import {createTimeRanges} from '../../src/js/utils/time.js';
import sinon from 'sinon';
QUnit.module('player reset-ui');

QUnit.test('Calling resetProgressBar should reset the components displaying time related values', function(assert) {
  const clock = sinon.useFakeTimers();
  const player = TestHelpers.makePlayer({controls: true});

  clock.tick(1000);
  player.trigger('ready');
  player.buffered = function() {
    return createTimeRanges(0, 5);
  };
  player.duration(40);
  player.currentTime(20);

  const {
    currentTimeDisplay,
    durationDisplay,
    remainingTimeDisplay,
    progressControl: {
      seekBar
    }
  } = player.controlBar;

  clock.tick(30);

  player.trigger('timeupdate');
  player.trigger('progress');

  clock.tick(30);

  // Current time display
  assert.equal(currentTimeDisplay.textNode_.textContent, '0:20', 'current time display is 0:20');
  // Duration display
  assert.equal(durationDisplay.textNode_.textContent, '0:40', 'duration display is 0:40');
  // Remaining time display
  assert.equal(remainingTimeDisplay.textNode_.textContent, '0:20', 'remaining time display is 0:20');
  // Seek bar
  assert.equal(seekBar.getProgress(), '0.5', 'seek bar progress is 0.5');
  assert.equal(seekBar.getAttribute('aria-valuetext'), '0:20 of 0:40', 'seek bar progress holder aria value text is 0:20 of 0:40');
  assert.equal(seekBar.getAttribute('aria-valuenow'), '50.00', 'seek bar progress holder aria value now is 50.00');
  // Load progress
  assert.equal(seekBar.loadProgressBar.el().textContent, 'Loaded: 12.50%', 'load progress bar textContent is Loaded: 12.50%');
  assert.equal(seekBar.loadProgressBar.el().style.width, '12.5%', 'load progress bar width is 12.5%');
  // Play progress
  assert.equal(seekBar.playProgressBar.el().textContent, '0:20', 'player progress bar textContent is 0:20');
  assert.equal(seekBar.playProgressBar.el().style.width, '50%', 'player progress bar width is 50%');
  assert.equal(seekBar.playProgressBar.timeTooltip.el().textContent, '0:20', 'player progress bar time tooltip is 0:20');

  // Do reset
  player.resetProgressBar_();
  player.duration(0);
  clock.tick(30);

  assert.equal(player.currentTime(), 0, 'player current time is 0');

  // Current time display
  assert.equal(currentTimeDisplay.textNode_.textContent, '0:00', 'current time display is 0:00');
  // Duration display
  assert.equal(durationDisplay.textNode_.textContent, '0:00', 'duration display is 0:00');
  // Remaining time display
  assert.equal(remainingTimeDisplay.textNode_.textContent, '0:00', 'remaining time display is 0:00');
  // Seek bar
  assert.equal(seekBar.getProgress(), '0', 'seek bar progress is 0');
  assert.equal(seekBar.getAttribute('aria-valuetext'), '0:00 of 0:00', 'seek bar progress holder aria value text is 0:00 of 0:00');
  assert.equal(seekBar.getAttribute('aria-valuenow'), '0.00', 'seek bar progress holder aria value now is 0.00');
  // Load progress
  assert.equal(seekBar.loadProgressBar.el().textContent, 'Loaded: 0.00%', 'load progress bar textContent is Loaded: 0.00%');
  assert.equal(seekBar.loadProgressBar.el().style.width, '0%', 'load progress bar width is 0%');
  // Play progress
  assert.equal(seekBar.playProgressBar.el().textContent, '0:00', 'player progress bar textContent is 0:00');
  assert.equal(seekBar.playProgressBar.el().style.width, '0%', 'player progress bar width is 0%');
  assert.equal(seekBar.playProgressBar.timeTooltip.el().textContent, '0:00', 'player progress bar time tooltip is 0:00');

  clock.restore();
  player.dispose();
});

QUnit.test('Calling resetProgressBar_ player method when no durationDisplay or remainingTimeDisplay controls exist', function(assert) {
  const player = TestHelpers.makePlayer({
    children: {
      controlBar: {}
    },
    controlBar: {
      children: {
        playToggle: {}
      }
    }
  });

  player.currentTime(20);
  player.resetProgressBar_();

  assert.equal(
    player.currentTime(), 0,
    'player current time is 0'
  );
  player.dispose();
});

QUnit.test('Calling resetPlaybackRate player method should place play rate at 1x', function(assert) {
  const player = TestHelpers.makePlayer({techOrder: ['html5']});

  player.playbackRate(2);
  player.handleTechRateChange_();
  player.resetPlaybackRate_();
  const defaultRate = player.defaultPlaybackRate();

  assert.equal(
    player.controlBar.playbackRateMenuButton.labelEl_.textContent, defaultRate + 'x',
    'Playback rate is the default one on the UI'
  );
  assert.equal(
    player.playbackRate(), defaultRate,
    'Playback rate is the default one on the player object'
  );
  player.dispose();
});

QUnit.test('Calling resetVolumeBar player method should reset volume bar', function(assert) {
  const player = TestHelpers.makePlayer({ techOrder: ['html5'] });

  player.volume(0.5);

  player.trigger('volumechange');

  assert.equal(player.controlBar.volumePanel.volumeControl.volumeBar.el_.getAttribute('aria-valuenow'), 50, 'UI value of VolumeBar is 50');

  player.resetVolumeBar_();

  assert.equal(player.controlBar.volumePanel.volumeControl.volumeBar.el_.getAttribute('aria-valuenow'), 100, 'UI value of VolumeBar is 100');

  player.dispose();
});


<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title></title> <script src="//use.edgefonts.net/source-code-pro.js"></script> <link href="https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700" rel="stylesheet" type="text/css"> <!-- there are many other style for highlighted code here: https://cdnjs.com/libraries/highlight.js --> <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/styles/atelier-forest.light.min.css"> <link rel="stylesheet" type="text/css" href="css/api-docs.css"> <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/modernizr/2.8.3/modernizr.min.js"></script> </head> <body> <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/highlight.min.js"></script> <script src="js/doc-data.js"></script> <script src="js/api-docs.js"></script> </body> </html>


<!DOCTYPE html>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
<link rel="stylesheet" type="text/css" href="../css/guides.css">
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700,700italic,600,600italic' rel='stylesheet' type='text/css'><link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.8.0/styles/solarized_light.min.css" />

<link rel="canonical" href="https://docs.videojs.com/tutorial-setup.html">

This documentation is for an outdated version of Video.js. See documentation for the current release.

Setup

Video.js is pretty easy to set up. It can take a matter of seconds to get the player up and working on your web page.

Step 1: Include the Video.js Javascript and CSS files in the head of your page.

You can download the Video.js source and host it on your own servers, or use the free CDN hosted version. As of Video.js 5.0, the source is transpiled from ES2015 (formerly known as ES6) to ES5, but IE8 only supports ES3. In order to continue to support IE8, we've bundled an ES5 shim and sham together and hosted it on the CDN.

<script src="//vjs.zencdn.net/ie8/1.1.1/videojs-ie8.min.js"></script>

CDN Version

<link href="//vjs.zencdn.net/5.4.6/video-js.min.css" rel="stylesheet">
<script src="//vjs.zencdn.net/5.4.6/video.min.js"></script>

Alternatively you can always go here to get the latest URL for videojs CDN.

We include a stripped down Google Analytics pixel that tracks a random percentage (currently 1%) of players loaded from the CDN. This allows us to see (roughly) what browsers are in use in the wild, along with other useful metrics such as OS and device. If you'd like to disable analytics, you can simply include the following global before including Video.js:

window.HELP_IMPROVE_VIDEOJS = false;

Install via package manager

NPM

$ npm install --save video.js

Bower

$ bower install --save video.js

Self Hosted.

To entirely self-host, you'll need to pull in the font files and let Video.js know where the swf is located. If you simply copy the dist folder or zip file contents into your project everything
should Just Work™, but the paths can easily be changed by editing the LESS file and re-building, or by modifying the generated CSS file. Additionally include the videojs-vtt.js source, which adds the WebVTT object to the global scope.

<link href="//example.com/path/to/video-js.min.css" rel="stylesheet">
<script src="//example.com/path/to/videojs-vtt.js"></script>
<script src="//example.com/path/to/video.min.js"></script>
<script>
  videojs.options.flash.swf = "http://example.com/path/to/video-js.swf"
</script>

Step 2: Add an HTML5 video tag to your page.

With Video.js you just use an HTML5 video tag to embed a video. Video.js will then read the tag and make it work in all browsers, not just ones that support HTML5 video. Beyond the basic markup, Video.js needs a few extra pieces.

Note: The data-setup attribute described here should not be used if you use the alternative setup described in the next section.

The 'data-setup' Attribute tells Video.js to automatically set up the video when the page is ready, and read any options (in JSON format) from the attribute (see options). There are other methods for initializing the player, but this is the easiest.

The 'id' Attribute: Should be used and unique for every video on the same page.

The 'class' attribute contains two classes:

video-js applies styles that are required for Video.js functionality, like fullscreen and subtitles.

vjs-default-skin applies the default skin to the HTML controls, and can be removed or overridden to create your own controls design.

Otherwise include/exclude attributes, settings, sources, and tracks exactly as you would for HTML5 video.*

<video id="example_video_1" class="video-js vjs-default-skin"
  controls preload="auto" width="640" height="264"
  poster="http://video-js.zencoder.com/oceans-clip.png"
  data-setup='{"example_option":true}'>
 <source src="http://video-js.zencoder.com/oceans-clip.mp4" type="video/mp4" />
 <source src="http://video-js.zencoder.com/oceans-clip.webm" type="video/webm" />
 <source src="http://video-js.zencoder.com/oceans-clip.ogv" type="video/ogg" />
 <p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a></p>
</video>

By default, the big play button is located in the upper left hand corner so it doesn't cover up the interesting parts of the poster. If you'd prefer to center the big play button, you can add an additional vjs-big-play-centered class to your video element. For example:

<video id="example_video_1" class="video-js vjs-default-skin vjs-big-play-centered"
  controls preload="auto" width="640" height="264"
  poster="http://video-js.zencoder.com/oceans-clip.png"
  data-setup='{"example_option":true}'>
  ...
</video>

Alternative Setup for Dynamically Loaded HTML

If your web page or application loads the video tag dynamically (ajax, appendChild, etc.), so that it may not exist when the page loads, you'll want to manually set up the player instead of relying on the data-setup attribute. To do this, first remove the data-setup attribute from the tag so there's no confusion around when the player is initialized. Next, run the following javascript some time after the Video.js javascript library has loaded, and after the video tag has been loaded into the DOM.

videojs("example_video_1", {}, function(){
  // Player (this) is initialized and ready.
});

The first argument in the videojs function is the ID of your video tag. Replace it with your own.

The second argument is an options object. It allows you to set additional options like you can with the data-setup attribute.

The third argument is a 'ready' callback. Once Video.js has initialized it will call this function.

Instead of using an element ID, you can also pass a reference to the element itself.

videojs(document.getElementById('example_video_1'), {}, function() {
  // This is functionally the same as the previous example.
});

videojs(document.getElementsByClassName('awesome_video_class')[0], {}, function() {
  // You can grab an element by class if you'd like, just make sure
  // if it's an array that you pick one (here we chose the first).
});

* If you have trouble playing back content you know is in the correct format, your HTTP server might not be delivering the content with the correct MIME type. Please double check your content's headers before opening an issue.


/**
 * @file loader.js
 */
import Component from '../component.js';
import Tech from './tech.js';
import {toTitleCase} from '../utils/str.js';
import {merge} from '../utils/obj.js';

/**
 * The `MediaLoader` is the `Component` that decides which playback technology to load
 * when a player is initialized.
 *
 * @extends Component
 */
class MediaLoader extends Component {

  /**
   * Create an instance of this class.
   *
   * @param { import('../player').default } player
   *        The `Player` that this class should attach to.
   *
   * @param {Object} [options]
   *        The key/value store of player options.
   *
   * @param {Function} [ready]
   *        The function that is run when this component is ready.
   */
  constructor(player, options, ready) {
    // MediaLoader has no element
    const options_ = merge({createEl: false}, options);

    super(player, options_, ready);

    // If there are no sources when the player is initialized,
    // load the first supported playback technology.

    if (!options.playerOptions.sources || options.playerOptions.sources.length === 0) {
      for (let i = 0, j = options.playerOptions.techOrder; i < j.length; i++) {
        const techName = toTitleCase(j[i]);
        let tech = Tech.getTech(techName);

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

        // Check if the browser supports this technology
        if (tech && tech.isSupported()) {
          player.loadTech_(techName);
          break;
        }
      }
    } else {
      // Loop through playback technologies (e.g. HTML5) and check for support.
      // Then load the best source.
      // A few assumptions here:
      //   All playback technologies respect preload false.
      player.src(options.playerOptions.sources);
    }
  }
}

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


assert.equal(seekBar.getAttribute('aria-valuetext'), '0:00 of 0:00', 'seek bar progress holder aria value text is 0:00 of 0:00');

assert.equal(seekBar.getAttribute('aria-valuenow'), '0.00', 'seek bar progress holder aria value now is 0.00');

assert.equal(seekBar.getAttribute('aria-valuetext'), '0:00 of -:-', 'seek bar progress holder aria value text is 0:00 of -:-');


<!DOCTYPE html>

  <meta charset="utf-8" />
  <title>Video.js Sandbox</title>
  <link href="../dist/video-js.css" rel="stylesheet" type="text/css">

    <video id='start-test-video' height=264 width=640 src="https://vjs.zencdn.net/v/oceans.mp4" controls></video>

Press play on the video to start tests. (as we need user permission to autoplay)


/* eslint-env qunit */
import document from 'global/document';
import sinon from 'sinon';
import * as Dom from '../../../src/js/utils/dom.js';
import TestHelpers from '../test-helpers.js';

QUnit.module('utils/dom');

QUnit.test('should create an element', function(assert) {
  const div = Dom.createEl();
  const span = Dom.createEl('span', {
    innerHTML: 'fdsa'
  }, {
    'data-test': 'asdf'
  });

  assert.strictEqual(div.nodeName, 'DIV');
  assert.strictEqual(span.nodeName, 'SPAN');
  assert.strictEqual(span.getAttribute('data-test'), 'asdf');
  assert.strictEqual(span.innerHTML, 'fdsa');
});

QUnit.test('should create an element, supporting textContent', function(assert) {
  const span = Dom.createEl('span', {textContent: 'howdy'});

  if (span.textContent) {
    assert.strictEqual(span.textContent, 'howdy', 'works in browsers that support textContent');
  } else {
    assert.strictEqual(span.innerText, 'howdy', 'works in browsers that DO NOT support textContent');
  }
});

QUnit.test('should create an element with tabIndex prop', function(assert) {
  const span = Dom.createEl('span', {tabIndex: '5'});

  assert.strictEqual(span.tabIndex, 5);
});

QUnit.test('should create an element with content', function(assert) {
  const span = Dom.createEl('span');
  const div = Dom.createEl('div', undefined, undefined, span);

  assert.strictEqual(div.firstChild, span);
});

QUnit.test('should be able to set standard props as attributes, and vice versa, on a created element', function(assert) {
  const span = Dom.createEl('span', {className: 'bar'}, {id: 'foo'});

  assert.strictEqual(span.getAttribute('class'), 'bar');
  assert.strictEqual(span.id, 'foo');
});

QUnit.test('should insert an element first in another', function(assert) {
  const el1 = document.createElement('div');
  const el2 = document.createElement('div');
  const parent = document.createElement('div');

  Dom.prependTo(el1, parent);
  assert.strictEqual(parent.firstChild, el1, 'inserts first into empty parent');

  Dom.prependTo(el2, parent);
  assert.strictEqual(parent.firstChild, el2, 'inserts first into parent with child');
});

QUnit.test('addClass()', function(assert) {
  const el = document.createElement('div');

  assert.expect(6);

  Dom.addClass(el, 'test-class');
  assert.strictEqual(el.className, 'test-class', 'adds a single class');

  Dom.addClass(el, 'test-class');
  assert.strictEqual(el.className, 'test-class', 'does not duplicate classes');

  Dom.addClass(el, 'test2_className');
  assert.strictEqual(el.className, 'test-class test2_className', 'adds second class');

  Dom.addClass(el, 'FOO');
  assert.strictEqual(el.className, 'test-class test2_className FOO', 'adds third class');

  Dom.addClass(el, 'left-class', 'right-class');
  assert.strictEqual(el.className, 'test-class test2_className FOO left-class right-class', 'adds two classes');

  Dom.addClass(el, 'l-class r-class');
  assert.strictEqual(
    el.className,
    'test-class test2_className FOO left-class right-class l-class r-class',
    'adds two classes via one string'
  );
});

QUnit.test('removeClass()', function(assert) {
  const el = document.createElement('div');

  el.className = 'test-class test2_className FOO bar';

  assert.expect(5);

  Dom.removeClass(el, 'test-class');
  assert.strictEqual(el.className, 'test2_className FOO bar', 'removes one class');

  Dom.removeClass(el, 'test2_className');
  assert.strictEqual(el.className, 'FOO bar', 'removes another class');

  Dom.removeClass(el, 'FOO');
  assert.strictEqual(el.className, 'bar', 'removes another class');

  el.className = 'bar left-class right-class';

  Dom.removeClass(el, 'left-class', 'right-class');
  assert.strictEqual(el.className, 'bar', 'removes two classes');

  el.className = 'bar l-class r-class';

  Dom.removeClass(el, 'l-class r-class');
  assert.strictEqual(el.className, 'bar', 'removes two classes via one string');
});

QUnit.test('hasClass()', function(assert) {
  const el = document.createElement('div');

  el.className = 'test-class foo foo test2_className FOO bar';

  assert.strictEqual(Dom.hasClass(el, 'test-class'), true, 'class detected');
  assert.strictEqual(Dom.hasClass(el, 'foo'), true, 'class detected');
  assert.strictEqual(Dom.hasClass(el, 'test2_className'), true, 'class detected');
  assert.strictEqual(Dom.hasClass(el, 'FOO'), true, 'class detected');
  assert.strictEqual(Dom.hasClass(el, 'bar'), true, 'class detected');
  assert.strictEqual(
    Dom.hasClass(el, 'test2'),
    false,
    'valid substring - but not a class - correctly not detected'
  );
  assert.strictEqual(
    Dom.hasClass(el, 'className'),
    false,
    'valid substring - but not a class - correctly not detected'
  );

  assert.throws(function() {
    Dom.hasClass(el, 'FOO bar');
  }, 'throws when attempting to detect a class with whitespace');
});

QUnit.test('toggleClass()', function(assert) {
  const el = Dom.createEl('div', {className: 'foo bar'});

  const predicateToggles = [
    {
      toggle: 'foo',
      predicate: true,
      className: 'foo bar',
      message: 'if predicate `true` matches state of the element, do nothing'
    },
    {
      toggle: 'baz',
      predicate: false,
      className: 'foo bar',
      message: 'if predicate `false` matches state of the element, do nothing'
    },
    {
      toggle: 'baz',
      predicate: true,
      className: 'foo bar baz',
      message: 'if predicate `true` differs from state of the element, add the class'
    },
    {
      toggle: 'foo',
      predicate: false,
      className: 'bar baz',
      message: 'if predicate `false` differs from state of the element, remove the class'
    },
    {
      toggle: 'bar',
      predicate: () => true,
      className: 'bar baz',
      message: 'if a predicate function returns `true`, ' +
               'matching the state of the element, do nothing'
    },
    {
      toggle: 'foo',
      predicate: () => false,
      className: 'bar baz',
      message: 'if a predicate function returns `false`, matching ' +
               'the state of the element, do nothing'
    },
    {
      toggle: 'foo',
      predicate: () => true,
      className: 'bar baz foo',
      message: 'if a predicate function returns `true`, ' +
               'differing from state of the element, add the class'
    },
    {
      toggle: 'foo',
      predicate: () => false,
      className: 'bar baz',
      message: 'if a predicate function returns `false`, differing ' +
               'from state of the element, remove the class'
    },
    {
      toggle: 'foo',
      predicate: Function.prototype,
      className: 'bar baz foo',
      message: 'if a predicate function returns `undefined` and ' +
               'the element does not have the class, add the class'
    },
    {
      toggle: 'bar',
      predicate: Function.prototype,
      className: 'baz foo',
      message: 'if a predicate function returns `undefined` and the ' +
               'element has the class, remove the class'
    },
    {
      toggle: 'bar',
      predicate: () => [],
      className: 'baz foo bar',
      message: 'if a predicate function returns a defined non-boolean ' +
               'value and the element does not have the class, add the class'
    },
    {
      toggle: 'baz',
      predicate: () => 'this is incorrect',
      className: 'foo bar',
      message: 'if a predicate function returns a defined non-boolean value ' +
               'and the element has the class, remove the class'
    }
  ];

  assert.expect(4 + predicateToggles.length);

  Dom.toggleClass(el, 'bar');
  assert.strictEqual(el.className, 'foo', 'toggles a class off, if present');

  Dom.toggleClass(el, 'bar');
  assert.strictEqual(el.className, 'foo bar', 'toggles a class on, if absent');

  Dom.toggleClass(el, 'bla ok');
  assert.strictEqual(el.className, 'foo bar bla ok', 'toggles a classes on, if absent');

  Dom.toggleClass(el, 'bla ok');
  assert.strictEqual(el.className, 'foo bar', 'toggles a classes off, if present');

  predicateToggles.forEach(x => {
    Dom.toggleClass(el, x.toggle, x.predicate);
    assert.strictEqual(el.className, x.className, x.message);
  });
});

QUnit.test('should set element attributes from object', function(assert) {
  const el = document.createElement('div');

  el.id = 'el1';
  Dom.setAttributes(el, {'controls': true, 'data-test': 'asdf'});

  assert.equal(el.getAttribute('id'), 'el1');
  assert.equal(el.getAttribute('controls'), '');
  assert.equal(el.getAttribute('data-test'), 'asdf');
});

QUnit.test('should read tag attributes from elements, including HTML5 in all browsers', function(assert) {
  // Creating the source/track tags outside of the video tag prevents log errors
  const tags = `
  <video id="vid1" controls autoplay loop muted preload="none" src="http://google.com" poster="http://www2.videojs.com/img/video-js-html5-video-player.png" data-test="asdf" data-empty-string="">
    <source id="source" src="http://google.com" type="video/mp4" media="fdsa" title="test" >
  </video>
  <track id="track" default src="http://google.com" kind="captions" srclang="en" label="testlabel" title="test" >
  `;

  const fixture = document.getElementById('qunit-fixture');

  // Have to use innerHTML to append for IE8. AppendChild doesn't work.
  // Also it must be added to the page body, not just in memory.
  fixture.innerHTML += tags;

  const vid1Vals = Dom.getAttributes(fixture.getElementsByTagName('video')[0]);
  const sourceVals = Dom.getAttributes(fixture.getElementsByTagName('source')[0]);
  const trackVals = Dom.getAttributes(fixture.getElementsByTagName('track')[0]);

  // vid1
  // was using deepEqual, but ie8 would send all properties as attributes
  assert.equal(vid1Vals.autoplay, true);
  assert.equal(vid1Vals.controls, true);
  assert.equal(vid1Vals['data-test'], 'asdf');
  assert.equal(vid1Vals['data-empty-string'], '');
  assert.equal(vid1Vals.id, 'vid1');
  assert.equal(vid1Vals.loop, true);
  assert.equal(vid1Vals.muted, true);
  assert.equal(vid1Vals.poster, 'http://www2.videojs.com/img/video-js-html5-video-player.png');
  assert.equal(vid1Vals.preload, 'none');
  assert.equal(vid1Vals.src, 'http://google.com');

  // sourceVals
  assert.equal(sourceVals.title, 'test');
  assert.equal(sourceVals.media, 'fdsa');
  assert.equal(sourceVals.type, 'video/mp4');
  assert.equal(sourceVals.src, 'http://google.com');
  assert.equal(sourceVals.id, 'source');

  // trackVals
  assert.equal(trackVals.default, true);
  assert.equal(trackVals.id, 'track');
  assert.equal(trackVals.kind, 'captions');
  assert.equal(trackVals.label, 'testlabel');
  assert.equal(trackVals.src, 'http://google.com');
  assert.equal(trackVals.srclang, 'en');
  assert.equal(trackVals.title, 'test');
});

QUnit.test('Dom.findPosition should find top and left position', function(assert) {
  const d = document.createElement('div');
  let position = Dom.findPosition(d);

  d.style.width = '100px';
  d.style.height = '50px';
  d.style.top = '10px';
  d.style.left = '20px';
  d.style.position = 'absolute';

  assert.deepEqual(
    position,
    {left: 0, top: 0, width: 0, height: 0},
    'If element isn\'t in the DOM, we should get zeros'
  );

  document.body.appendChild(d);
  position = Dom.findPosition(d);
  assert.deepEqual(position.left, 20, 'The position left was not correct');
  assert.deepEqual(position.top, 10, 'The position top was not correct');
  assert.deepEqual(position.width, 100, 'The dimension width was not correct');
  assert.deepEqual(position.height, 50, 'The dimension height was not correct');

  d.style.display = 'none';
  position = Dom.findPosition(d);
  assert.deepEqual(
    position,
    {left: 0, top: 0, width: 0, height: 0},
    'If there is no offsetParent, we should get zeros'
  );
});

QUnit.test('Dom.isEl', function(assert) {
  assert.expect(7);
  assert.notOk(Dom.isEl(), 'undefined is not an element');
  assert.notOk(Dom.isEl(true), 'booleans are not elements');
  assert.notOk(Dom.isEl({}), 'objects are not elements');
  assert.notOk(Dom.isEl([]), 'arrays are not elements');
  assert.notOk(Dom.isEl('

'), 'strings are not elements');
  assert.ok(Dom.isEl(document.createElement('div')), 'elements are elements');
  assert.ok(Dom.isEl({nodeType: 1}), 'duck typing is imperfect');
});

QUnit.test('Dom.isTextNode', function(assert) {
  assert.expect(7);
  assert.notOk(Dom.isTextNode(), 'undefined is not a text node');
  assert.notOk(Dom.isTextNode(true), 'booleans are not text nodes');
  assert.notOk(Dom.isTextNode({}), 'objects are not text nodes');
  assert.notOk(Dom.isTextNode([]), 'arrays are not text nodes');
  assert.notOk(Dom.isTextNode('hola mundo'), 'strings are not text nodes');
  assert.ok(
    Dom.isTextNode(document.createTextNode('hello, world!')),
    'text nodes are text nodes'
  );
  assert.ok(Dom.isTextNode({nodeType: 3}), 'duck typing is imperfect');
});

QUnit.test('Dom.emptyEl', function(assert) {
  const el = Dom.createEl();

  el.appendChild(Dom.createEl('span'));
  el.appendChild(Dom.createEl('span'));
  el.appendChild(document.createTextNode('hola mundo'));
  el.appendChild(Dom.createEl('span'));

  Dom.emptyEl(el);

  assert.expect(1);
  assert.notOk(el.firstChild, 'the element was emptied');
});

QUnit.test('Dom.normalizeContent: strings and elements/nodes', function(assert) {
  assert.expect(8);

  const str = Dom.normalizeContent('hello');

  assert.strictEqual(str[0].data, 'hello', 'single string becomes a text node');

  const elem = Dom.normalizeContent(Dom.createEl());

  assert.ok(Dom.isEl(elem[0]), 'an element is passed through');

  const node = Dom.normalizeContent(document.createTextNode('goodbye'));

  assert.strictEqual(node[0].data, 'goodbye', 'a text node is passed through');

  assert.strictEqual(Dom.normalizeContent(null).length, 0, 'falsy values are ignored');
  assert.strictEqual(Dom.normalizeContent(false).length, 0, 'falsy values are ignored');
  assert.strictEqual(Dom.normalizeContent().length, 0, 'falsy values are ignored');
  assert.strictEqual(Dom.normalizeContent(123).length, 0, 'numbers are ignored');
  assert.strictEqual(Dom.normalizeContent({}).length, 0, 'objects are ignored');
});

QUnit.test('Dom.normalizeContent: functions returning strings and elements/nodes', function(assert) {
  assert.expect(9);

  const str = Dom.normalizeContent(() => 'hello');

  assert.strictEqual(
    str[0].data,
    'hello',
    'a function can return a string, which becomes a text node'
  );

  const elem = Dom.normalizeContent(() => Dom.createEl());

  assert.ok(Dom.isEl(elem[0]), 'a function can return an element');

  const node = Dom.normalizeContent(() => document.createTextNode('goodbye'));

  assert.strictEqual(node[0].data, 'goodbye', 'a function can return a text node');

  assert.strictEqual(
    Dom.normalizeContent(() => null).length,
    0,
    'a function CANNOT return falsy values'
  );
  assert.strictEqual(
    Dom.normalizeContent(() => false).length,
    0,
    'a function CANNOT return falsy values'
  );
  assert.strictEqual(
    Dom.normalizeContent(() => undefined).length,
    0,
    'a function CANNOT return falsy values'
  );
  assert.strictEqual(
    Dom.normalizeContent(() => 123).length,
    0,
    'a function CANNOT return numbers'
  );
  assert.strictEqual(
    Dom.normalizeContent(() => {}).length,
    0,
    'a function CANNOT return objects'
  );
  assert.strictEqual(
    Dom.normalizeContent(() => (() => null)).length,
    0,
    'a function CANNOT return a function'
  );
});

QUnit.test('Dom.normalizeContent: arrays of strings and objects', function(assert) {
  assert.expect(7);

  const source = [
    'hello',
    {},
    Dom.createEl(),
    ['oops'],
    null,
    document.createTextNode('goodbye'),
    () => 'it works'
  ];
  const result = Dom.normalizeContent(source);

  assert.strictEqual(
    result[0].data,
    'hello',
    'an array can include a string normalized to a text node'
  );
  assert.ok(Dom.isEl(result[1]), 'an array can include an element');
  assert.strictEqual(result[2].data, 'goodbye', 'an array can include a text node');
  assert.strictEqual(
    result[3].data,
    'it works',
    'an array can include a function, which is invoked'
  );
  assert.strictEqual(result.indexOf(source[1]), -1, 'an array CANNOT include an object');
  assert.strictEqual(result.indexOf(source[3]), -1, 'an array CANNOT include an array');
  assert.strictEqual(
    result.indexOf(source[4]),
    -1,
    'an array CANNOT include falsy values'
  );
});

QUnit.test('Dom.normalizeContent: functions returning arrays', function(assert) {
  assert.expect(3);

  const arr = [];
  const result = Dom.normalizeContent(() => ['hello', Function.prototype, arr]);

  assert.strictEqual(result[0].data, 'hello', 'a function can return an array');
  assert.strictEqual(
    result.indexOf(Function.prototype),
    -1,
    'a function can return an array, but it CANNOT include a function'
  );
  assert.strictEqual(
    result.indexOf(arr),
    -1,
    'a function can return an array, but it CANNOT include an array'
  );
});

QUnit.test('Dom.insertContent', function(assert) {
  const p = Dom.createEl('p');
  const text = document.createTextNode('hello');
  const el = Dom.insertContent(Dom.createEl(), [p, text]);

  assert.expect(2);
  assert.strictEqual(el.firstChild, p, 'the paragraph was inserted first');
  assert.strictEqual(el.firstChild.nextSibling, text, 'the text node was inserted last');
});

QUnit.test('Dom.appendContent', function(assert) {
  const p1 = Dom.createEl('p');
  const p2 = Dom.createEl('p');
  const el = Dom.insertContent(Dom.createEl(), [p1]);

  Dom.appendContent(el, p2);

  assert.expect(2);
  assert.strictEqual(el.firstChild, p1, 'the first paragraph is the first child');
  assert.strictEqual(el.firstChild.nextSibling, p2, 'the second paragraph was appended');
});

QUnit.test('$() and $$()', function(assert) {
  const fixture = document.getElementById('qunit-fixture');
  const container = document.createElement('div');
  const children = [
    document.createElement('div'),
    document.createElement('div'),
    document.createElement('div')
  ];

  children.forEach(child => container.appendChild(child));
  fixture.appendChild(container);

  const totalDivCount = document.getElementsByTagName('div').length;

  assert.expect(12);

  assert.strictEqual(
    Dom.$('#qunit-fixture'),
    fixture,
    'can find an element in the document context'
  );
  assert.strictEqual(
    Dom.$$('div').length,
    totalDivCount,
    'finds elements in the document context'
  );

  assert.strictEqual(
    Dom.$('div', container),
    children[0],
    'can find an element in a DOM element context'
  );
  assert.strictEqual(
    Dom.$$('div', container).length,
    children.length,
    'finds elements in a DOM element context'
  );

  assert.strictEqual(
    Dom.$('#qunit-fixture', document.querySelector('unknown')),
    fixture,
    'falls back to document given a bad context element'
  );
  assert.strictEqual(
    Dom.$$('div', document.querySelector('unknown')).length,
    totalDivCount,
    'falls back to document given a bad context element'
  );

  assert.strictEqual(
    Dom.$('#qunit-fixture', 'body'),
    fixture,
    'can find an element in a selector context'
  );
  assert.strictEqual(
    Dom.$$('div', '#qunit-fixture').length,
    1 + children.length,
    'finds elements in a selector context'
  );

  assert.strictEqual(
    Dom.$('#qunit-fixture', 'unknown'),
    fixture,
    'falls back to document given a bad context selector'
  );
  assert.strictEqual(
    Dom.$$('div', 'unknown').length,
    totalDivCount,
    'falls back to document given a bad context selector'
  );

  assert.strictEqual(Dom.$('div', children[0]), null, 'returns null for missing elements');
  assert.strictEqual(
    Dom.$$('div', children[0]).length,
    0,
    'returns 0 for missing elements'
  );
});

QUnit.test('getBoundingClientRect() returns an object for elements that support it', function(assert) {
  const mockEl = {
    getBoundingClientRect: sinon.spy(() => {
      return {
        bottom: 3,
        height: 10,
        left: 4,
        right: 2,
        top: 1,
        width: 20
      };
    }),
    parentNode: true
  };

  const actual = Dom.getBoundingClientRect(mockEl);

  // The expected result is what is returned by the mock element.
  const expected = mockEl.getBoundingClientRect.firstCall.returnValue;

  assert.notStrictEqual(actual, expected, 'the object returned by the mock element was cloned and not returned directly');

  Object.keys(expected).forEach(k => {
    assert.strictEqual(actual[k], expected[k], `the "${k}" returned by the Dom util matches what was returned by the mock element`);
  });
});

QUnit.test('isSingleLeftClick() returns false for mousemove event', function(assert) {
  const mouseEvent = TestHelpers.createEvent('mousemove');

  mouseEvent.button = 0;
  mouseEvent.buttons = 0;

  assert.notOk(Dom.isSingleLeftClick(mouseEvent), 'a mousemove event is not a single left click');
});

QUnit.test('isSingleLeftClick() returns true for mouseup event', function(assert) {
  const mouseEvent = TestHelpers.createEvent('mouseup');

  mouseEvent.button = 0;
  mouseEvent.buttons = 0;

  assert.ok(Dom.isSingleLeftClick(mouseEvent), 'a mouseup event is a single left click');
});

QUnit.test('isSingleLeftClick() checks return values for mousedown event', function(assert) {
  const mouseEvent = TestHelpers.createEvent('mousedown');

  // Left mouse click
  mouseEvent.button = 0;
  mouseEvent.buttons = 0;

  assert.notOk(Dom.isSingleLeftClick(mouseEvent), 'a left mouse click on an older browser (Safari) is a single left click');

  // Left mouse click
  mouseEvent.button = 0;
  mouseEvent.buttons = 1;

  assert.ok(Dom.isSingleLeftClick(mouseEvent), 'a left mouse click on browsers that supporting buttons property is a single left click');

  // Right mouse click
  mouseEvent.button = 2;
  mouseEvent.buttons = 2;

  assert.notOk(Dom.isSingleLeftClick(mouseEvent), 'a right mouse click is not a single left click');

  // Touch event on some mobiles
  mouseEvent.button = 0;
  mouseEvent.buttons = undefined;

  assert.ok(Dom.isSingleLeftClick(mouseEvent), 'a touch event on mobiles is a single left click');

  // Chrome simulates mobile devices
  mouseEvent.button = undefined;
  mouseEvent.buttons = undefined;

  assert.ok(Dom.isSingleLeftClick(mouseEvent), 'a touch event on simulated mobiles is a single left click');
});


/**
 * @file setup.js - Functions for setting up a player without
 * user interaction based on the data-setup `attribute` of the video tag.
 *
 * @module setup
 */
import * as Dom from './utils/dom';
import document from 'global/document';
import window from 'global/window';

let _windowLoaded = false;
let videojs;

/**
 * Set up any tags that have a data-setup `attribute` when the player is started.
 */
const autoSetup = function() {

  if (videojs.options.autoSetup === false) {
    return;
  }

  const vids = Array.prototype.slice.call(document.getElementsByTagName('video'));
  const audios = Array.prototype.slice.call(document.getElementsByTagName('audio'));
  const divs = Array.prototype.slice.call(document.getElementsByTagName('video-js'));
  const mediaEls = vids.concat(audios, divs);

  // Check if any media elements exist
  if (mediaEls && mediaEls.length > 0) {

    for (let i = 0, e = mediaEls.length; i < e; i++) {
      const mediaEl = mediaEls[i];

      // Check if element exists, has getAttribute func.
      if (mediaEl && mediaEl.getAttribute) {

        // Make sure this player hasn't already been set up.
        if (mediaEl.player === undefined) {
          const options = mediaEl.getAttribute('data-setup');

          // Check if data-setup attr exists.
          // We only auto-setup if they've added the data-setup attr.
          if (options !== null) {
            // Create new video.js instance.
            videojs(mediaEl);
          }
        }

      // If getAttribute isn't defined, we need to wait for the DOM.
      } else {
        autoSetupTimeout(1);
        break;
      }
    }

  // No videos were found, so keep looping unless page is finished loading.
  } else if (!_windowLoaded) {
    autoSetupTimeout(1);
  }
};

/**
 * Wait until the page is loaded before running autoSetup. This will be called in
 * autoSetup if `hasLoaded` returns false.
 *
 * @param {number} wait
 *        How long to wait in ms
 *
 * @param {module:videojs} [vjs]
 *        The videojs library function
 */
function autoSetupTimeout(wait, vjs) {
  // Protect against breakage in non-browser environments
  if (!Dom.isReal()) {
    return;
  }

  if (vjs) {
    videojs = vjs;
  }

  window.setTimeout(autoSetup, wait);
}

/**
 * Used to set the internal tracking of window loaded state to true.
 *
 * @private
 */
function setWindowLoaded() {
  _windowLoaded = true;
  window.removeEventListener('load', setWindowLoaded);
}

if (Dom.isReal()) {
  if (document.readyState === 'complete') {
    setWindowLoaded();
  } else {
    /**
     * Listen for the load event on window, and set _windowLoaded to true.
     *
     * We use a standard event listener here to avoid incrementing the GUID
     * before any players are created.
     *
     * @listens load
     */
    window.addEventListener('load', setWindowLoaded);
  }
}

/**
 * check if the window has been loaded
 */
const hasLoaded = function() {
  return _windowLoaded;
};

export {autoSetup, autoSetupTimeout, hasLoaded};


assert.equal(seekBar.loadProgressBar.el().textContent, 'Loaded: 0.00%', 'load progress bar textContent is Loaded: 0.00%');

assert.equal(seekBar.loadProgressBar.el().style.width, '0%', 'load progress bar width is 0%');

assert.ok(!calculateDistance.called, 'calculateDistance was not called');

/* eslint-env qunit */
import EventTarget from '../../src/js/event-target.js';
import VolumeControl from '../../src/js/control-bar/volume-control/volume-control.js';
import MuteToggle from '../../src/js/control-bar/mute-toggle.js';
import VolumeBar from '../../src/js/control-bar/volume-control/volume-bar.js';
import PlayToggle from '../../src/js/control-bar/play-toggle.js';
import PlaybackRateMenuButton from '../../src/js/control-bar/playback-rate-menu/playback-rate-menu-button.js';
import Slider from '../../src/js/slider/slider.js';
import PictureInPictureToggle from '../../src/js/control-bar/picture-in-picture-toggle.js';
import FullscreenToggle from '../../src/js/control-bar/fullscreen-toggle.js';
import ControlBar from '../../src/js/control-bar/control-bar.js';
import SeekBar from '../../src/js/control-bar/progress-control/seek-bar.js';
import RemainingTimeDisplay from '../../src/js/control-bar/time-controls/remaining-time-display.js';
import TestHelpers from './test-helpers.js';
import document from 'global/document';
import window from 'global/window';
import sinon from 'sinon';

QUnit.module('Controls', {
  beforeEach(assert) {
    this.clock = sinon.useFakeTimers();
  },
  afterEach(assert) {
    this.clock.restore();
  }
});

QUnit.test('should hide volume and mute toggle control if it\'s not supported', function(assert) {
  assert.expect(2);

  const player = TestHelpers.makePlayer();

  player.tech_.featuresVolumeControl = false;
  player.tech_.featuresMuteControl = false;

  const volumeControl = new VolumeControl(player);
  const muteToggle = new MuteToggle(player);

  assert.ok(volumeControl.hasClass('vjs-hidden'), 'volumeControl is not hidden');
  assert.ok(muteToggle.hasClass('vjs-hidden'), 'muteToggle is not hidden');

  player.dispose();
  volumeControl.dispose();
  muteToggle.dispose();
});

QUnit.test('should show replay icon when video playback ended', function(assert) {
  assert.expect(1);

  const player = TestHelpers.makePlayer();

  const playToggle = new PlayToggle(player);

  player.trigger('ended');

  assert.ok(playToggle.hasClass('vjs-ended'), 'playToogle is in the ended state');

  player.dispose();
  playToggle.dispose();
});

QUnit.test('should show replay icon when video playback ended and replay option is set to true', function(assert) {
  assert.expect(1);

  const player = TestHelpers.makePlayer();

  const playToggle = new PlayToggle(player, {replay: true});

  player.trigger('ended');

  assert.ok(playToggle.hasClass('vjs-ended'), 'playToogle is in the ended state');

  player.dispose();
  playToggle.dispose();
});

QUnit.test('should not show the replay icon when video playback ended', function(assert) {
  assert.expect(1);

  const player = TestHelpers.makePlayer();

  const playToggle = new PlayToggle(player, {replay: false});

  player.trigger('ended');

  assert.equal(playToggle.hasClass('vjs-ended'), false, 'playToogle is not in the ended state');

  player.dispose();
  playToggle.dispose();
});

QUnit.test('should test and toggle volume control on `loadstart`', function(assert) {
  const player = TestHelpers.makePlayer();

  player.tech_.featuresVolumeControl = true;
  player.tech_.featuresMuteControl = true;

  const volumeControl = new VolumeControl(player);
  const muteToggle = new MuteToggle(player);

  assert.equal(volumeControl.hasClass('vjs-hidden'), false, 'volumeControl is hidden initially');
  assert.equal(muteToggle.hasClass('vjs-hidden'), false, 'muteToggle is hidden initially');

  player.tech_.featuresVolumeControl = false;
  player.tech_.featuresMuteControl = false;
  player.trigger('loadstart');

  assert.equal(volumeControl.hasClass('vjs-hidden'), true, 'volumeControl does not hide itself');
  assert.equal(muteToggle.hasClass('vjs-hidden'), true, 'muteToggle does not hide itself');

  player.tech_.featuresVolumeControl = true;
  player.tech_.featuresMuteControl = true;
  player.trigger('loadstart');

  assert.equal(volumeControl.hasClass('vjs-hidden'), false, 'volumeControl does not show itself');
  assert.equal(muteToggle.hasClass('vjs-hidden'), false, 'muteToggle does not show itself');

  player.dispose();
  volumeControl.dispose();
  muteToggle.dispose();
});

QUnit.test('calculateDistance should use changedTouches, if available', function(assert) {
  const player = TestHelpers.makePlayer();

  player.tech_.featuresVolumeControl = true;

  const slider = new Slider(player);

  document.body.appendChild(slider.el_);
  slider.el_.style.position = 'absolute';
  slider.el_.style.width = '200px';
  slider.el_.style.left = '0px';

  const event = {
    pageX: 10,
    changedTouches: [{
      pageX: 100
    }]
  };

  assert.equal(slider.calculateDistance(event), 0.5, 'we should have touched exactly in the center, so, the ratio should be half');

  player.dispose();
  slider.dispose();
});

QUnit.test("SeekBar doesn't set scrubbing on mouse down, only on mouse move", function(assert) {
  const player = TestHelpers.makePlayer();
  const scrubbingSpy = sinon.spy(player, 'scrubbing');
  const seekBar = new SeekBar(player);
  const doc = new EventTarget();

  // mousemove is listened to on the document.
  // Specifically, we check the ownerDocument of the seekBar's bar.
  // Therefore, we want to mock it out to be able to trigger mousemove
  seekBar.bar.dispose();
  seekBar.bar.el_ = new EventTarget();
  seekBar.bar.el_.ownerDocument = doc;

  seekBar.trigger('mousedown');
  assert.ok(scrubbingSpy.calledWith(), 'called scrubbing as a getter');
  assert.notOk(scrubbingSpy.calledWith(true), 'did not set scrubbing true');

  player.scrubbing(false);

  scrubbingSpy.resetHistory();

  doc.trigger('mousemove');
  assert.ok(scrubbingSpy.calledWith(), 'called scrubbing as a getter');
  assert.ok(scrubbingSpy.calledWith(true), 'did set scrubbing true');

  seekBar.dispose();
  player.dispose();
});

QUnit.test('playback rate button is hidden by default', function(assert) {
  assert.expect(1);

  const player = TestHelpers.makePlayer();
  const playbackRate = new PlaybackRateMenuButton(player);

  assert.ok(playbackRate.el().className.indexOf('vjs-hidden') >= 0, 'playbackRate is hidden');

  player.dispose();
  playbackRate.dispose();
});

QUnit.test('playback rate button is not hidden if playback rates are set', function(assert) {
  assert.expect(1);

  const player = TestHelpers.makePlayer({
    playbackRates: [1, 2, 3]
  });
  const playbackRate = new PlaybackRateMenuButton(player);

  assert.ok(playbackRate.el().className.indexOf('vjs-hidden') === -1, 'playbackRate is not hidden');

  player.dispose();
  playbackRate.dispose();
});

QUnit.test('should show or hide playback rate menu button on playback rates change', function(assert) {
  const rates = [1, 2, 3];
  const norates = [];
  let playbackRatesReturnValue = rates;
  const player = TestHelpers.makePlayer();

  player.playbackRates = () => playbackRatesReturnValue;

  const playbackRate = new PlaybackRateMenuButton(player);

  assert.ok(playbackRate.el().className.indexOf('vjs-hidden') === -1, 'playbackRate is not hidden');

  playbackRatesReturnValue = norates;

  player.trigger('playbackrateschange');

  assert.ok(playbackRate.el().className.indexOf('vjs-hidden') >= 0, 'playbackRate is hidden');

  player.dispose();
  playbackRate.dispose();
});

QUnit.test('Picture-in-Picture control text should be correct when enterpictureinpicture and leavepictureinpicture are triggered', function(assert) {
  const player = TestHelpers.makePlayer();
  const pictureInPictureToggle = new PictureInPictureToggle(player);

  player.isInPictureInPicture(true);
  player.trigger('enterpictureinpicture');
  assert.equal(pictureInPictureToggle.controlText(), 'Exit Picture-in-Picture', 'Control Text is correct while switching to Picture-in-Picture mode');

  player.isInPictureInPicture(false);
  player.trigger('leavepictureinpicture');
  assert.equal(pictureInPictureToggle.controlText(), 'Picture-in-Picture', 'Control Text is correct while switching back to normal mode');

  player.dispose();
  pictureInPictureToggle.dispose();
});

QUnit.test('Picture-in-Picture control enabled property value should be correct when enterpictureinpicture and leavepictureinpicture are triggered', function(assert) {
  const player = TestHelpers.makePlayer();
  const pictureInPictureToggle = new PictureInPictureToggle(player);

  assert.equal(pictureInPictureToggle.enabled_, false, 'pictureInPictureToggle button should be disabled after creation');

  if ('pictureInPictureEnabled' in document && player.disablePictureInPicture() === false) {
    player.isInPictureInPicture(true);
    player.trigger('enterpictureinpicture');
    assert.equal(pictureInPictureToggle.enabled_, true, 'pictureInPictureToggle button should be enabled after triggering an enterpictureinpicture event');

    player.isInPictureInPicture(false);
    player.trigger('leavepictureinpicture');
    assert.equal(pictureInPictureToggle.enabled_, true, 'pictureInPictureToggle button should be enabled after triggering an leavepictureinpicture event');
  } else {
    player.isInPictureInPicture(true);
    player.trigger('enterpictureinpicture');
    assert.equal(pictureInPictureToggle.enabled_, false, 'pictureInPictureToggle button should be disabled after triggering an enterpictureinpicture event');

    player.isInPictureInPicture(false);
    player.trigger('leavepictureinpicture');
    assert.equal(pictureInPictureToggle.enabled_, false, 'pictureInPictureToggle button should be disabled after triggering an leavepictureinpicture event');
  }

  player.dispose();
  pictureInPictureToggle.dispose();
});

QUnit.test('Picture-in-Picture control enabled property value should be correct when loadedmetadata is triggered', function(assert) {
  const player = TestHelpers.makePlayer();
  const pictureInPictureToggle = new PictureInPictureToggle(player);

  assert.equal(pictureInPictureToggle.enabled_, false, 'pictureInPictureToggle button should be disabled after creation');

  if ('pictureInPictureEnabled' in document && player.disablePictureInPicture() === false) {
    player.trigger('loadedmetadata');
    assert.equal(pictureInPictureToggle.enabled_, true, 'pictureInPictureToggle button should be enabled after triggering an loadedmetadata event');
  } else {
    player.trigger('loadedmetadata');
    assert.equal(pictureInPictureToggle.enabled_, false, 'pictureInPictureToggle button should be disabled after triggering an loadedmetadata event');
  }

  player.dispose();
  pictureInPictureToggle.dispose();
});

QUnit.test('Picture-in-Picture control is hidden when the source is audio', function(assert) {
  const player = TestHelpers.makePlayer({});
  const pictureInPictureToggle = new PictureInPictureToggle(player);

  player.src({src: 'example.mp4', type: 'video/mp4'});
  player.trigger('loadedmetadata');

  assert.notOk(pictureInPictureToggle.hasClass('vjs-hidden'), 'pictureInPictureToggle button is not hidden initially');

  player.src({src: 'example1.mp3', type: 'audio/mp3'});
  player.trigger('loadedmetadata');
  assert.ok(pictureInPictureToggle.hasClass('vjs-hidden'), 'pictureInPictureToggle button is hidden whenh the source is audio');

  player.dispose();
  pictureInPictureToggle.dispose();
});

QUnit.test('Picture-in-Picture control is displayed if docPiP is enabled', function(assert) {
  const player = TestHelpers.makePlayer({
    disablePictureInPicture: true,
    enableDocumentPictureInPicture: true
  });
  const pictureInPictureToggle = new PictureInPictureToggle(player);
  const testPiPObj = {};

  if (!window.documentPictureInPicture) {
    window.documentPictureInPicture = testPiPObj;
  }

  player.src({src: 'example.mp4', type: 'video/mp4'});
  player.trigger('loadedmetadata');

  assert.notOk(pictureInPictureToggle.hasClass('vjs-hidden'), 'pictureInPictureToggle button is not hidden');

  player.dispose();
  pictureInPictureToggle.dispose();
  if (window.documentPictureInPicture === testPiPObj) {
    delete window.documentPictureInPicture;
  }
});

QUnit.test('Fullscreen control text should be correct when fullscreenchange is triggered', function(assert) {
  const player = TestHelpers.makePlayer({controlBar: false});
  const fullscreentoggle = new FullscreenToggle(player);

  // make the fullscreenchange handler doesn't trigger
  player.off(player.fsApi_.fullscreenchange, player.boundDocumentFullscreenChange_);

  player.isFullscreen(true);
  player.trigger('fullscreenchange');
  assert.equal(fullscreentoggle.controlText(), 'Exit Fullscreen', 'Control Text is correct while switching to fullscreen mode');

  player.isFullscreen(false);
  player.trigger('fullscreenchange');
  assert.equal(fullscreentoggle.controlText(), 'Fullscreen', 'Control Text is correct while switching back to normal mode');

  player.dispose();
  fullscreentoggle.dispose();
});

QUnit.test('Clicking MuteToggle when volume is above 0 should toggle muted property and not change volume', function(assert) {
  const player = TestHelpers.makePlayer({ techOrder: ['html5'] });
  const muteToggle = new MuteToggle(player);

  assert.equal(player.volume(), 1, 'volume is above 0');
  assert.equal(player.muted(), false, 'player is not muted');

  muteToggle.handleClick();

  assert.equal(player.volume(), 1, 'volume is same');
  assert.equal(player.muted(), true, 'player is muted');

  player.dispose();
  muteToggle.dispose();
});

QUnit.test('Clicking MuteToggle when volume is 0 and muted is false should set volume to lastVolume and keep muted false', function(assert) {
  const player = TestHelpers.makePlayer({ techOrder: ['html5'] });
  const muteToggle = new MuteToggle(player);

  player.volume(0);
  assert.equal(player.lastVolume_(), 1, 'lastVolume is set');
  assert.equal(player.muted(), false, 'player is muted');

  muteToggle.handleClick();

  assert.equal(player.volume(), 1, 'volume is set to lastVolume');
  assert.equal(player.muted(), false, 'muted remains false');

  player.dispose();
  muteToggle.dispose();
});

QUnit.test('Clicking MuteToggle when volume is 0 and muted is true should set volume to lastVolume and sets muted to false', function(assert) {
  const player = TestHelpers.makePlayer({ techOrder: ['html5'] });
  const muteToggle = new MuteToggle(player);

  player.volume(0);
  player.muted(true);
  player.lastVolume_(0.5);

  muteToggle.handleClick();

  assert.equal(player.volume(), 0.5, 'volume is set to lastVolume');
  assert.equal(player.muted(), false, 'muted is set to false');

  player.dispose();
  muteToggle.dispose();
});

QUnit.test('Clicking MuteToggle when volume is 0, lastVolume is less than 0.1, and muted is true sets volume to 0.1 and muted to false', function(assert) {
  const player = TestHelpers.makePlayer({ techOrder: ['html5'] });
  const muteToggle = new MuteToggle(player);

  player.volume(0);
  player.muted(true);
  player.lastVolume_(0.05);

  muteToggle.handleClick();

  // `Number.prototype.toFixed()` is used here to circumvent rounding issues
  assert.equal(player.volume().toFixed(1), (0.1).toFixed(1), 'since lastVolume is less than 0.1, volume is set to 0.1');
  assert.equal(player.muted(), false, 'muted is set to false');

  player.dispose();
  muteToggle.dispose();
});

QUnit.test('ARIA value of VolumeBar should start at 100', function(assert) {
  const player = TestHelpers.makePlayer({ techOrder: ['html5'] });
  const volumeBar = new VolumeBar(player);

  this.clock.tick(1);

  assert.equal(volumeBar.el_.getAttribute('aria-valuenow'), 100, 'ARIA value of VolumeBar is 100');

  player.dispose();
  volumeBar.dispose();
});

QUnit.test('Muting with MuteToggle should set ARIA value of VolumeBar to 0', function(assert) {
  const player = TestHelpers.makePlayer({ techOrder: ['html5'] });
  const volumeBar = new VolumeBar(player);
  const muteToggle = new MuteToggle(player);

  this.clock.tick(1);

  assert.equal(player.volume(), 1, 'Volume is 1');
  assert.equal(player.muted(), false, 'Muted is false');
  assert.equal(volumeBar.el_.getAttribute('aria-valuenow'), 100, 'ARIA value of VolumeBar is 100');

  muteToggle.handleClick();

  // Because `volumechange` is triggered asynchronously, it doesn't end up
  // getting fired on `player` in the test environment, so we run it
  // manually.
  player.trigger('volumechange');

  assert.equal(player.volume(), 1, 'Volume remains 1');
  assert.equal(player.muted(), true, 'Muted is true');
  assert.equal(volumeBar.el_.getAttribute('aria-valuenow'), 0, 'ARIA value of VolumeBar is 0');

  player.dispose();
  muteToggle.dispose();
  volumeBar.dispose();
});

QUnit.test('controlbar children to false individually, does not cause an assertion', function(assert) {
  const defaultChildren = ControlBar.prototype.options_.children;

  defaultChildren.forEach((childName) => {
    const options = {controlBar: {}};

    options.controlBar[childName] = false;

    const player = TestHelpers.makePlayer(options);

    this.clock.tick(1000);
    player.triggerReady();
    player.dispose();
    assert.ok(true, `${childName}: false. did not cause an assertion`);
  });
});

QUnit.test('all controlbar children to false, does not cause an assertion', function(assert) {
  const defaultChildren = ControlBar.prototype.options_.children;
  const options = {controlBar: {}};

  defaultChildren.forEach((childName) => {
    options.controlBar[childName] = false;
  });

  const player = TestHelpers.makePlayer(options);

  this.clock.tick(1000);
  player.triggerReady();
  player.dispose();
  assert.ok(true, 'did not cause an assertion');
});

QUnit.test('Remaing time negative sign can be optional', function(assert) {
  const player = TestHelpers.makePlayer({ techOrder: ['html5'] });

  const rtd1 = new RemainingTimeDisplay(player);
  const rtd2 = new RemainingTimeDisplay(player, {displayNegative: false});

  this.clock.tick(1);

  assert.ok(rtd1.el().textContent.indexOf('-') > 0, 'Value is negative by default');
  assert.equal(rtd2.el().textContent.indexOf('-'), -1, 'Value is positive with option');

  rtd1.dispose();
  rtd2.dispose();
  player.dispose();
});


video.js

An HTML5 video player that supports HLS and DASH with a common API and skin.

Copyright Brightcove, Inc. <https://www.brightcove.com/>

shx rm -rf ./dist ./test/dist ./docs/api ./lang/zh-Han*.json

conventional-changelog -p videojs -i CHANGELOG.md -s

npm-run-all clean lint-errors build:js build:css build:lang build:test build:types

shx cp sandbox/descriptions.html.example sandbox/descriptions.test-a11y.html

browserify test/require/browserify.js -o test/dist/browserify.js

webpack --hide-modules test/require/webpack.js test/dist/webpack.js

shx cp -R node_modules/videojs-font/fonts dist/font

sass --load-path='./' --load-path='./node_modules/' --no-source-map src/css/vjs-cdn.scss dist/alt/video-js-cdn.css

postcss --verbose --config postcss.config.js -d dist/alt dist/alt/video-js-cdn.css

sass --load-path='./' --load-path='./node_modules/' --no-source-map src/css/vjs.scss dist/video-js.css

postcss --verbose --config postcss.config.js -d dist/ dist/video-js.css

shx cp lang/zh-CN.json lang/zh-Hans.json

shx cp lang/zh-TW.json lang/zh-Hant.json

cleancss dist/alt/video-js-cdn.css -o dist/alt/video-js-cdn.min.css

cleancss dist/video-js.css -o dist/video-js.min.css

chokidar --initial 'lang/**/!(zh-Hans|zh-Hant)*.json' -c 'npm run build:lang'

npm-run-all -p build:css:default build:css:cdn watch:css:*

karma start test/karma.conf.js --singleRun=false --auto-watch

[ "$CI_TEST_TYPE" != 'coverage' ] || shx cat test/dist/coverage/text.txt

node ./build/doc-version.js && jsdoc -c .jsdoc.json

remark --output -- './{,!(node_modules)/**/}!(CHANGELOG)*.md'

is-prerelease || npm run changelog && git add CHANGELOG.md

cd dist && cross-env bestzip "./video-js-${npm_package_version}.zip" * && cd ..

dist docs sandbox test/dist test/api core.js core.es.js


/* eslint-env qunit */
import * as Num from '../../../src/js/utils/num';

QUnit.module('utils/num', function() {

  QUnit.module('clamp');

  QUnit.test('keep a number between min/max values', function(assert) {
    assert.expect(5);
    assert.strictEqual(Num.clamp(5, 1, 10), 5);
    assert.strictEqual(Num.clamp(5, 1, 5), 5);
    assert.strictEqual(Num.clamp(5, 1, 2), 2);
    assert.strictEqual(Num.clamp(-1, 1, 10), 1);
    assert.strictEqual(Num.clamp(NaN, 1, 10), 1);
  });
});


/* eslint-disable no-console */
try {
  require('../../');
} catch (e) {
  console.error(e);
  process.exit(1);
}

process.exit(0);


assert.equal(seekBar.playProgressBar.timeTooltip.el().textContent, '0:00', 'player progress bar time tooltip is 0:00');

/**
 * @file seek-bar.js
 */
import Slider from '../../slider/slider.js';
import Component from '../../component.js';
import {IS_IOS, IS_ANDROID} from '../../utils/browser.js';
import * as Dom from '../../utils/dom.js';
import * as Fn from '../../utils/fn.js';
import {formatTime} from '../../utils/time.js';
import {silencePromise} from '../../utils/promise';
import keycode from 'keycode';
import document from 'global/document';

import './load-progress-bar.js';
import './play-progress-bar.js';
import './mouse-time-display.js';

// The number of seconds the `step*` functions move the timeline.
const STEP_SECONDS = 5;

// The multiplier of STEP_SECONDS that PgUp/PgDown move the timeline.
const PAGE_KEY_MULTIPLIER = 12;

/**
 * Seek bar and container for the progress bars. Uses {@link PlayProgressBar}
 * as its `bar`.
 *
 * @extends Slider
 */
class SeekBar extends Slider {

  /**
   * Creates an instance of this class.
   *
   * @param { import('../../player').default } player
   *        The `Player` that this class should be attached to.
   *
   * @param {Object} [options]
   *        The key/value store of player options.
   */
  constructor(player, options) {
    super(player, options);
    this.setEventHandlers_();
  }

  /**
   * Sets the event handlers
   *
   * @private
   */
  setEventHandlers_() {
    this.update_ = Fn.bind_(this, this.update);
    this.update = Fn.throttle(this.update_, Fn.UPDATE_REFRESH_INTERVAL);

    this.on(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
    if (this.player_.liveTracker) {
      this.on(this.player_.liveTracker, 'liveedgechange', this.update);
    }

    // when playing, let's ensure we smoothly update the play progress bar
    // via an interval
    this.updateInterval = null;

    this.enableIntervalHandler_ = (e) => this.enableInterval_(e);
    this.disableIntervalHandler_ = (e) => this.disableInterval_(e);

    this.on(this.player_, ['playing'], this.enableIntervalHandler_);

    this.on(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);

    // we don't need to update the play progress if the document is hidden,
    // also, this causes the CPU to spike and eventually crash the page on IE11.
    if ('hidden' in document && 'visibilityState' in document) {
      this.on(document, 'visibilitychange', this.toggleVisibility_);
    }
  }

  toggleVisibility_(e) {
    if (document.visibilityState === 'hidden') {
      this.cancelNamedAnimationFrame('SeekBar#update');
      this.cancelNamedAnimationFrame('Slider#update');
      this.disableInterval_(e);
    } else {
      if (!this.player_.ended() && !this.player_.paused()) {
        this.enableInterval_();
      }

      // we just switched back to the page and someone may be looking, so, update ASAP
      this.update();
    }
  }

  enableInterval_() {
    if (this.updateInterval) {
      return;

    }
    this.updateInterval = this.setInterval(this.update, Fn.UPDATE_REFRESH_INTERVAL);
  }

  disableInterval_(e) {
    if (this.player_.liveTracker && this.player_.liveTracker.isLive() && e && e.type !== 'ended') {
      return;
    }

    if (!this.updateInterval) {
      return;
    }

    this.clearInterval(this.updateInterval);
    this.updateInterval = null;
  }

  /**
   * Create the `Component`'s DOM element
   *
   * @return {Element}
   *         The element that was created.
   */
  createEl() {
    return super.createEl('div', {
      className: 'vjs-progress-holder'
    }, {
      'aria-label': this.localize('Progress Bar')
    });
  }

  /**
   * This function updates the play progress bar and accessibility
   * attributes to whatever is passed in.
   *
   * @param {Event} [event]
   *        The `timeupdate` or `ended` event that caused this to run.
   *
   * @listens Player#timeupdate
   *
   * @return {number}
   *          The current percent at a number from 0-1
   */
  update(event) {
    // ignore updates while the tab is hidden
    if (document.visibilityState === 'hidden') {
      return;
    }

    const percent = super.update();

    this.requestNamedAnimationFrame('SeekBar#update', () => {
      const currentTime = this.player_.ended() ?
        this.player_.duration() : this.getCurrentTime_();
      const liveTracker = this.player_.liveTracker;
      let duration = this.player_.duration();

      if (liveTracker && liveTracker.isLive()) {
        duration = this.player_.liveTracker.liveCurrentTime();
      }

      if (this.percent_ !== percent) {
        // machine readable value of progress bar (percentage complete)
        this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2));
        this.percent_ = percent;
      }

      if (this.currentTime_ !== currentTime || this.duration_ !== duration) {
        // human readable value of progress bar (time complete)
        this.el_.setAttribute(
          'aria-valuetext',
          this.localize(
            'progress bar timing: currentTime={1} duration={2}',
            [formatTime(currentTime, duration),
              formatTime(duration, duration)],
            '{1} of {2}'
          )
        );

        this.currentTime_ = currentTime;
        this.duration_ = duration;
      }

      // update the progress bar time tooltip with the current time
      if (this.bar) {
        this.bar.update(Dom.getBoundingClientRect(this.el()), this.getProgress());
      }
    });

    return percent;
  }

  /**
   * Prevent liveThreshold from causing seeks to seem like they
   * are not happening from a user perspective.
   *
   * @param {number} ct
   *        current time to seek to
   */
  userSeek_(ct) {
    if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
      this.player_.liveTracker.nextSeekedFromUser();
    }

    this.player_.currentTime(ct);
  }

  /**
   * Get the value of current time but allows for smooth scrubbing,
   * when player can't keep up.
   *
   * @return {number}
   *         The current time value to display
   *
   * @private
   */
  getCurrentTime_() {
    return (this.player_.scrubbing()) ?
      this.player_.getCache().currentTime :
      this.player_.currentTime();
  }

  /**
   * Get the percentage of media played so far.
   *
   * @return {number}
   *         The percentage of media played so far (0 to 1).
   */
  getPercent() {
    const currentTime = this.getCurrentTime_();
    let percent;
    const liveTracker = this.player_.liveTracker;

    if (liveTracker && liveTracker.isLive()) {
      percent = (currentTime - liveTracker.seekableStart()) / liveTracker.liveWindow();

      // prevent the percent from changing at the live edge
      if (liveTracker.atLiveEdge()) {
        percent = 1;
      }
    } else {
      percent = currentTime / this.player_.duration();
    }

    return percent;
  }

  /**
   * Handle mouse down on seek bar
   *
   * @param {MouseEvent} event
   *        The `mousedown` event that caused this to run.
   *
   * @listens mousedown
   */
  handleMouseDown(event) {
    if (!Dom.isSingleLeftClick(event)) {
      return;
    }

    // Stop event propagation to prevent double fire in progress-control.js
    event.stopPropagation();

    this.videoWasPlaying = !this.player_.paused();
    this.player_.pause();

    super.handleMouseDown(event);
  }

  /**
   * Handle mouse move on seek bar
   *
   * @param {MouseEvent} event
   *        The `mousemove` event that caused this to run.
   * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false
   *
   * @listens mousemove
   */
  handleMouseMove(event, mouseDown = false) {
    if (!Dom.isSingleLeftClick(event)) {
      return;
    }

    if (!mouseDown && !this.player_.scrubbing()) {
      this.player_.scrubbing(true);
    }

    let newTime;
    const distance = this.calculateDistance(event);
    const liveTracker = this.player_.liveTracker;

    if (!liveTracker || !liveTracker.isLive()) {
      newTime = distance * this.player_.duration();

      // Don't let video end while scrubbing.
      if (newTime === this.player_.duration()) {
        newTime = newTime - 0.1;
      }
    } else {

      if (distance >= 0.99) {
        liveTracker.seekToLiveEdge();
        return;
      }
      const seekableStart = liveTracker.seekableStart();
      const seekableEnd = liveTracker.liveCurrentTime();

      newTime = seekableStart + (distance * liveTracker.liveWindow());

      // Don't let video end while scrubbing.
      if (newTime >= seekableEnd) {
        newTime = seekableEnd;
      }

      // Compensate for precision differences so that currentTime is not less
      // than seekable start
      if (newTime <= seekableStart) {
        newTime = seekableStart + 0.1;
      }

      // On android seekableEnd can be Infinity sometimes,
      // this will cause newTime to be Infinity, which is
      // not a valid currentTime.
      if (newTime === Infinity) {
        return;
      }
    }

    // Set new time (tell player to seek to new time)
    this.userSeek_(newTime);
  }

  enable() {
    super.enable();
    const mouseTimeDisplay = this.getChild('mouseTimeDisplay');

    if (!mouseTimeDisplay) {
      return;
    }

    mouseTimeDisplay.show();
  }

  disable() {
    super.disable();
    const mouseTimeDisplay = this.getChild('mouseTimeDisplay');

    if (!mouseTimeDisplay) {
      return;
    }

    mouseTimeDisplay.hide();
  }

  /**
   * Handle mouse up on seek bar
   *
   * @param {MouseEvent} event
   *        The `mouseup` event that caused this to run.
   *
   * @listens mouseup
   */
  handleMouseUp(event) {
    super.handleMouseUp(event);

    // Stop event propagation to prevent double fire in progress-control.js
    if (event) {
      event.stopPropagation();
    }
    this.player_.scrubbing(false);

    /**
     * Trigger timeupdate because we're done seeking and the time has changed.
     * This is particularly useful for if the player is paused to time the time displays.
     *
     * @event Tech#timeupdate
     * @type {Event}
     */
    this.player_.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true });
    if (this.videoWasPlaying) {
      silencePromise(this.player_.play());
    } else {
      // We're done seeking and the time has changed.
      // If the player is paused, make sure we display the correct time on the seek bar.
      this.update_();
    }
  }

  /**
   * Move more quickly fast forward for keyboard-only users
   */
  stepForward() {
    this.userSeek_(this.player_.currentTime() + STEP_SECONDS);
  }

  /**
   * Move more quickly rewind for keyboard-only users
   */
  stepBack() {
    this.userSeek_(this.player_.currentTime() - STEP_SECONDS);
  }

  /**
   * Toggles the playback state of the player
   * This gets called when enter or space is used on the seekbar
   *
   * @param {KeyboardEvent} event
   *        The `keydown` event that caused this function to be called
   *
   */
  handleAction(event) {
    if (this.player_.paused()) {
      this.player_.play();
    } else {
      this.player_.pause();
    }
  }

  /**
   * Called when this SeekBar has focus and a key gets pressed down.
   * Supports the following keys:
   *
   *   Space or Enter key fire a click event
   *   Home key moves to start of the timeline
   *   End key moves to end of the timeline
   *   Digit "0" through "9" keys move to 0%, 10% ... 80%, 90% of the timeline
   *   PageDown key moves back a larger step than ArrowDown
   *   PageUp key moves forward a large step
   *
   * @param {KeyboardEvent} event
   *        The `keydown` event that caused this function to be called.
   *
   * @listens keydown
   */
  handleKeyDown(event) {
    const liveTracker = this.player_.liveTracker;

    if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
      event.preventDefault();
      event.stopPropagation();
      this.handleAction(event);
    } else if (keycode.isEventKey(event, 'Home')) {
      event.preventDefault();
      event.stopPropagation();
      this.userSeek_(0);
    } else if (keycode.isEventKey(event, 'End')) {
      event.preventDefault();
      event.stopPropagation();
      if (liveTracker && liveTracker.isLive()) {
        this.userSeek_(liveTracker.liveCurrentTime());
      } else {
        this.userSeek_(this.player_.duration());
      }
    } else if (/^[0-9]$/.test(keycode(event))) {
      event.preventDefault();
      event.stopPropagation();
      const gotoFraction = (keycode.codes[keycode(event)] - keycode.codes['0']) * 10.0 / 100.0;

      if (liveTracker && liveTracker.isLive()) {
        this.userSeek_(liveTracker.seekableStart() + (liveTracker.liveWindow() * gotoFraction));
      } else {
        this.userSeek_(this.player_.duration() * gotoFraction);
      }
    } else if (keycode.isEventKey(event, 'PgDn')) {
      event.preventDefault();
      event.stopPropagation();
      this.userSeek_(this.player_.currentTime() - (STEP_SECONDS * PAGE_KEY_MULTIPLIER));
    } else if (keycode.isEventKey(event, 'PgUp')) {
      event.preventDefault();
      event.stopPropagation();
      this.userSeek_(this.player_.currentTime() + (STEP_SECONDS * PAGE_KEY_MULTIPLIER));
    } else {
      // Pass keydown handling up for unsupported keys
      super.handleKeyDown(event);
    }
  }

  dispose() {
    this.disableInterval_();

    this.off(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
    if (this.player_.liveTracker) {
      this.off(this.player_.liveTracker, 'liveedgechange', this.update);
    }

    this.off(this.player_, ['playing'], this.enableIntervalHandler_);
    this.off(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);

    // we don't need to update the play progress if the document is hidden,
    // also, this causes the CPU to spike and eventually crash the page on IE11.
    if ('hidden' in document && 'visibilityState' in document) {
      this.off(document, 'visibilitychange', this.toggleVisibility_);
    }

    super.dispose();
  }
}

/**
 * Default options for the `SeekBar`
 *
 * @type {Object}
 * @private
 */
SeekBar.prototype.options_ = {
  children: [
    'loadProgressBar',
    'playProgressBar'
  ],
  barName: 'playProgressBar'
};

// MouseTimeDisplay tooltips should not be added to a player on mobile devices
if (!IS_IOS && !IS_ANDROID) {
  SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay');
}

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