/* eslint-env qunit */
import sinon from 'sinon';
import evented from '../../../src/js/mixins/evented';
import stateful from '../../../src/js/mixins/stateful';
import * as Obj from '../../../src/js/utils/obj';

QUnit.module('mixins: stateful');

QUnit.test('stateful() mutates an object as expected', function(assert) {
  const target = {};

  assert.strictEqual(Object.prototype.toString.call(stateful), '[object Function]', 'the mixin is a function');
  assert.strictEqual(stateful(target), target, 'returns the target object');

  assert.ok(Obj.isObject(target), 'the target is still an object');
  assert.ok(Obj.isPlain(target.state), 'the target has a state');
  assert.strictEqual(Object.keys(target.state).length, 0, 'the target state is empty by default');
  assert.strictEqual(Object.prototype.toString.call(target.setState), '[object Function]', 'the target has a setState method');
});

QUnit.test('stateful() with default state passed in', function(assert) {
  const target = stateful({}, {foo: 'bar'});

  assert.strictEqual(target.state.foo, 'bar', 'the default properties are added to the state');
});

QUnit.test('stateful() without default state passed in', function(assert) {
  const target = stateful({});

  assert.strictEqual(Object.keys(target.state).length, 0, 'no default properties are added to the state');
});

QUnit.test('setState() works as expected', function(assert) {
  const target = stateful(evented({}), {foo: 'bar', abc: 'xyz'});
  const spy = sinon.spy();

  target.on('statechanged', spy);

  const next = {foo: null, boo: 123};
  const changes = target.setState(next);

  assert.deepEqual(changes, {
    foo: {from: 'bar', to: null},
    boo: {from: undefined, to: 123}
  }, 'setState returns changes, a plain object');

  assert.deepEqual(target.state, {
    abc: 'xyz',
    foo: null,
    boo: 123
  }, 'the state was updated as expected');

  assert.ok(spy.called, 'the "statechanged" event occurred');

  const event = spy.firstCall.args[0];

  assert.strictEqual(event.type, 'statechanged', 'the event had the expected type');
  assert.strictEqual(event.changes, changes, 'the changes object is sent along with the event');
});

QUnit.test('setState() without changes does not trigger the "statechanged" event', function(assert) {
  const target = stateful(evented({}), {foo: 'bar'});
  const spy = sinon.spy();

  target.on('statechanged', spy);

  const changes = target.setState({foo: 'bar'});

  assert.strictEqual(changes, undefined, 'no changes were returned');
  assert.strictEqual(spy.callCount, 0, 'no event was triggered');
});

QUnit.test('handleStateChanged() is automatically bound to "statechanged" event', function(assert) {
  const target = evented({});

  target.handleStateChanged = sinon.spy();
  stateful(target, {foo: 'bar'});

  const changes = target.setState({foo: true});

  assert.ok(target.handleStateChanged.called, 'the "statechanged" event occurred');

  const event = target.handleStateChanged.firstCall.args[0];

  assert.strictEqual(event.type, 'statechanged', 'the event had the expected type');
  assert.strictEqual(event.changes, changes, 'the handleStateChanged() method was called');
});


* @private
*/
PlayProgressBar.prototype.options_ = {
children: [
'timeTooltip'
]
};

keep keep keep replace replace replace keep keep

/* eslint-env qunit */
import extendFn from '../../src/js/extend.js';

QUnit.module('extend.js');

QUnit.test('should add implicit parent constructor call', function(assert) {
  let superCalled = false;
  const Parent = function() {
    superCalled = true;
  };
  const Child = extendFn(Parent, {
    foo: 'bar'
  });
  const child = new Child();

  assert.ok(superCalled, 'super constructor called');
  assert.ok(child.foo, 'child properties set');
});


/**
 * @file mouse-time-display.js
 */
import Component from '../../component.js';
// import * as Dom from '../../utils/dom.js';
import * as Fn from '../../utils/fn.js';
import formatTime from '../../utils/format-time.js';
// import computedStyle from '../../utils/computed-style.js';

import './time-tooltip';

/**
 * The {@link MouseTimeDisplay} component tracks mouse movement over the
 * {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip}
 * indicating the time which is represented by a given point in the
 * {@link ProgressControl}.
 *
 * @extends Component
 */
class MouseTimeDisplay extends Component {

  /**
   * Creates an instance of this class.
   *
   * @param {Player} player
   *        The {@link 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.update = Fn.throttle(Fn.bind(this, this.update), 25);
  }

  /**
   * Create the the DOM element for this class.
   *
   * @return {Element}
   *         The element that was created.
   */
  createEl() {
    return super.createEl('div', {
      className: 'vjs-mouse-display'
    });
  }

  /**
   * Enqueues updates to its own DOM as well as the DOM of its
   * {@link TimeTooltip} child.
   *
   * @param {Object} seekBarRect
   *        The `ClientRect` for the {@link SeekBar} element.
   *
   * @param {number} seekBarPoint
   *        A number from 0 to 1, representing a horizontal reference point
   *        from the left edge of the {@link SeekBar}
   */
  update(seekBarRect, seekBarPoint) {

    // If there is an existing rAF ID, cancel it so we don't over-queue.
    if (this.rafId_) {
      this.cancelAnimationFrame(this.rafId_);
    }

    this.rafId_ = this.requestAnimationFrame(() => {
      const duration = this.player_.duration();
      const content = formatTime(seekBarPoint * duration, duration);

      this.el_.style.left = `${seekBarRect.width * seekBarPoint}px`;
      this.getChild('timeTooltip').update(seekBarRect, seekBarPoint, content);
    });
  }
}

/**
 * Default options for `MouseTimeDisplay`
 *
 * @type {Object}
 * @private
 */
MouseTimeDisplay.prototype.options_ = {
  children: [
    'timeTooltip'
  ]
};

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


import sh from 'shelljs';
import path from 'path';

export default function(commit, commitRange) {
  const SINGLE_COMMIT = `git diff-tree --no-commit-id --name-only -r ${commit}`;
  const COMMIT_RANGE = `git diff --name-only ${commitRange}`;

  let command = SINGLE_COMMIT;

  if (commitRange) {
    command = COMMIT_RANGE
  }

  const output = sh.exec(command, {async: false, silent: true}).stdout;

  const files = output.split('\n').filter(Boolean);
  return files.every((file) => file.startsWith('docs') || path.extname(file) === '.md');
};


/**
 * @file log.js
 * @module log
 */
import window from 'global/window';
import {IE_VERSION} from './browser';
import {isObject} from './obj';

let log;

// This is the private tracking variable for logging level.
let level = 'all';

// This is the private tracking variable for the logging history.
let history = [];

/**
 * Log messages to the console and history based on the type of message
 *
 * @private
 * @param  {string} type
 *         The name of the console method to use.
 *
 * @param  {Array} args
 *         The arguments to be passed to the matching console method.
 *
 * @param  {boolean} [stringify]
 *         By default, only old IEs should get console argument stringification,
 *         but this is exposed as a parameter to facilitate testing.
 */
export const logByType = (type, args, stringify = !!IE_VERSION && IE_VERSION < 11) => {
  const lvl = log.levels[level];
  const lvlRegExp = new RegExp(`^(${lvl})$`);

  if (type !== 'log') {

    // Add the type to the front of the message when it's not "log".
    args.unshift(type.toUpperCase() + ':');
  }

  // Add a clone of the args at this point to history.
  if (history) {
    history.push([].concat(args));
  }

  // Add console prefix after adding to history.
  args.unshift('VIDEOJS:');

  // If there's no console then don't try to output messages, but they will
  // still be stored in history.
  //
  // Was setting these once outside of this function, but containing them
  // in the function makes it easier to test cases where console doesn't exist
  // when the module is executed.
  const fn = window.console && window.console[type];

  // Bail out if there's no console or if this type is not allowed by the
  // current logging level.
  if (!fn || !lvl || !lvlRegExp.test(type)) {
    return;
  }

  // IEs previous to 11 log objects uselessly as "[object Object]"; so, JSONify
  // objects and arrays for those less-capable browsers.
  if (stringify) {
    args = args.map(a => {
      if (isObject(a) || Array.isArray(a)) {
        try {
          return JSON.stringify(a);
        } catch (x) {
          return String(a);
        }
      }

      // Cast to string before joining, so we get null and undefined explicitly
      // included in output (as we would in a modern console).
      return String(a);
    }).join(' ');
  }

  // Old IE versions do not allow .apply() for console methods (they are
  // reported as objects rather than functions).
  if (!fn.apply) {
    fn(args);
  } else {
    fn[Array.isArray(args) ? 'apply' : 'call'](window.console, args);
  }
};

/**
 * Logs plain debug messages. Similar to `console.log`.
 *
 * @class
 * @param    {Mixed[]} args
 *           One or more messages or objects that should be logged.
 */
log = function(...args) {
  logByType('log', args);
};

/**
 * Enumeration of available logging levels, where the keys are the level names
 * and the values are `|`-separated strings containing logging methods allowed
 * in that logging level. These strings are used to create a regular expression
 * matching the function name being called.
 *
 * Levels provided by video.js are:
 *
 * - `off`: Matches no calls. Any value that can be cast to `false` will have
 *   this effect. The most restrictive.
 * - `all` (default): Matches only Video.js-provided functions (`log`,
 *   `log.warn`, and `log.error`).
 * - `warn`: Matches `log.warn` and `log.error` calls.
 * - `error`: Matches only `log.error` calls.
 *
 * @type {Object}
 */
log.levels = {
  all: 'log|warn|error',
  error: 'error',
  off: '',
  warn: 'warn|error',
  DEFAULT: level
};

/**
 * Get or set the current logging level. If a string matching a key from
 * {@link log.levels} is provided, acts as a setter. Regardless of argument,
 * returns the current logging level.
 *
 * @param  {string} [lvl]
 *         Pass to set a new logging level.
 *
 * @return {string}
 *         The current logging level.
 */
log.level = (lvl) => {
  if (typeof lvl === 'string') {
    if (!log.levels.hasOwnProperty(lvl)) {
      throw new Error(`"${lvl}" in not a valid log level`);
    }
    level = lvl;
  }
  return level;
};

/**
 * Returns an array containing everything that has been logged to the history.
 *
 * This array is a shallow clone of the internal history record. However, its
 * contents are _not_ cloned; so, mutating objects inside this array will
 * mutate them in history.
 *
 * @return {Array}
 */
log.history = () => history ? [].concat(history) : [];

/**
 * Clears the internal history tracking, but does not prevent further history
 * tracking.
 */
log.history.clear = () => {
  if (history) {
    history.length = 0;
  }
};

/**
 * Disable history tracking if it is currently enabled.
 */
log.history.disable = () => {
  if (history !== null) {
    history.length = 0;
    history = null;
  }
};

/**
 * Enable history tracking if it is currently disabled.
 */
log.history.enable = () => {
  if (history === null) {
    history = [];
  }
};

/**
 * Logs error messages. Similar to `console.error`.
 *
 * @param {Mixed[]} args
 *        One or more messages or objects that should be logged as an error
 */
log.error = (...args) => logByType('error', args);

/**
 * Logs warning messages. Similar to `console.warn`.
 *
 * @param {Mixed[]} args
 *        One or more messages or objects that should be logged as a warning.
 */
log.warn = (...args) => logByType('warn', args);

export default log;


<!DOCTYPE html>

  <meta charset="utf-8" />
  <title>Video.js Text Descriptions, Chapters & Captions Example</title>

  <link href="../build/temp/video-js.css" rel="stylesheet" type="text/css">

  <video id="example_video_1" class="video-js vjs-default-skin" controls preload="none" width="640" height="360"
      data-setup='{ "html5" : { "nativeTextTracks" : false } }'>
    <source src="https://archive.org/download/ElephantsDream/ed_hd.mp4" type="video/mp4">
    <source src="https://archive.org/download/ElephantsDream/ed_hd.ogv" type="video/ogv">

    <track kind="captions" src="../docs/examples/elephantsdream/captions.en.vtt" srclang="en" label="English" default></track>
    <track kind="captions" src="../docs/examples/elephantsdream/captions.sv.vtt" srclang="sv" label="Swedish"></track>
    <track kind="captions" src="../docs/examples/elephantsdream/captions.ru.vtt" srclang="ru" label="Russian"></track>
    <track kind="captions" src="../docs/examples/elephantsdream/captions.ja.vtt" srclang="ja" label="Japanese"></track>
    <track kind="captions" src="../docs/examples/elephantsdream/captions.ar.vtt" srclang="ar" label="Arabic"></track>

    <track kind="descriptions" src="../docs/examples/elephantsdream/descriptions.en.vtt" srclang="en" label="English"></track>

    <track kind="chapters" src="../docs/examples/elephantsdream/chapters.en.vtt" srclang="en" label="English"></track>

To view this video please enable JavaScript, and consider upgrading to a web browser that supports HTML5 video

  </video>


import Component from '../../component.js';

import * as Fn from '../../utils/fn.js';

import * as Dom from '../../utils/dom.js';


#resizer,
footer {
  background-color: #ECEEF1;
  color: #868688;
  padding: 3px 10px;
}

.footer-text {
  padding: 3px;
  display: block;
}

footer .copyright {
  float: left;
}

footer .other-links {
  margin: 0;
  padding: 0;
  float: right;
}

footer .other-links li {
  display: inline;
}

footer .logo {
  display: none;
}


/* eslint-env qunit */
import TextTrackMenuItem from '../../../src/js/control-bar/text-track-controls/text-track-menu-item.js';
import TestHelpers from '../test-helpers.js';
import * as browser from '../../../src/js/utils/browser.js';
import sinon from 'sinon';

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

const track = {
  kind: 'captions',
  label: 'test'
};

QUnit.test('should be displayed when text tracks list is not empty', function(assert) {
  const player = TestHelpers.makePlayer({
    tracks: [track]
  });

  this.clock.tick(1000);

  assert.ok(!player.controlBar.captionsButton.hasClass('vjs-hidden'),
           'control is displayed');
  assert.equal(player.textTracks().length, 1, 'textTracks contains one item');

  player.dispose();
});

QUnit.test('should be displayed when a text track is added to an empty track list', function(assert) {
  const player = TestHelpers.makePlayer();

  player.addRemoteTextTrack(track, true);

  assert.ok(!player.controlBar.captionsButton.hasClass('vjs-hidden'),
           'control is displayed');
  assert.equal(player.textTracks().length, 1, 'textTracks contains one item');

  player.dispose();
});

QUnit.test('should not be displayed when text tracks list is empty', function(assert) {
  const player = TestHelpers.makePlayer();

  assert.ok(player.controlBar.captionsButton.hasClass('vjs-hidden'),
           'control is not displayed');
  assert.equal(player.textTracks().length, 0, 'textTracks is empty');

  player.dispose();
});

QUnit.test('should not be displayed when last text track is removed', function(assert) {
  const player = TestHelpers.makePlayer({
    tracks: [track]
  });

  player.removeRemoteTextTrack(player.textTracks()[0]);

  assert.ok(player.controlBar.captionsButton.hasClass('vjs-hidden'),
           'control is not displayed');
  assert.equal(player.textTracks().length, 0, 'textTracks is empty');

  player.dispose();
});

QUnit.test('menu should contain "Settings", "Off" and one track', function(assert) {
  const player = TestHelpers.makePlayer({
    tracks: [track]
  });

  this.clock.tick(1000);

  const menuItems = player.controlBar.captionsButton.items;

  assert.equal(menuItems.length, 3, 'menu contains three items');
  assert.equal(menuItems[0].track.label,
              'captions settings',
              'menu contains "captions settings"');
  assert.equal(menuItems[1].track.label, 'captions off', 'menu contains "captions off"');
  assert.equal(menuItems[2].track.label, 'test', 'menu contains "test" track');

  player.dispose();
});

QUnit.test('menu should update with addRemoteTextTrack', function(assert) {
  const player = TestHelpers.makePlayer({
    tracks: [track]
  });

  this.clock.tick(1000);

  player.addRemoteTextTrack(track, true);

  assert.equal(player.controlBar.captionsButton.items.length,
              4,
              'menu does contain added track');
  assert.equal(player.textTracks().length, 2, 'textTracks contains two items');

  player.dispose();
});

QUnit.test('menu should update with removeRemoteTextTrack', function(assert) {
  const player = TestHelpers.makePlayer({
    tracks: [track, track]
  });

  this.clock.tick(1000);

  player.removeRemoteTextTrack(player.textTracks()[0]);

  assert.equal(player.controlBar.captionsButton.items.length,
              3,
              'menu does not contain removed track');
  assert.equal(player.textTracks().length, 1, 'textTracks contains one item');

  player.dispose();
});

const descriptionstrack = {
  kind: 'descriptions',
  label: 'desc'
};

QUnit.test('descriptions should be displayed when text tracks list is not empty', function(assert) {
  const player = TestHelpers.makePlayer({
    tracks: [descriptionstrack]
  });

  this.clock.tick(1000);

  assert.ok(!player.controlBar.descriptionsButton.hasClass('vjs-hidden'),
           'descriptions control is displayed');
  assert.equal(player.textTracks().length, 1, 'textTracks contains one item');

  player.dispose();
});

QUnit.test('descriptions should be displayed when a text track is added to an empty track list', function(assert) {
  const player = TestHelpers.makePlayer();

  player.addRemoteTextTrack(descriptionstrack, true);

  assert.ok(!player.controlBar.descriptionsButton.hasClass('vjs-hidden'),
           'control is displayed');
  assert.equal(player.textTracks().length, 1, 'textTracks contains one item');

  player.dispose();
});

QUnit.test('descriptions should not be displayed when text tracks list is empty', function(assert) {
  const player = TestHelpers.makePlayer();

  assert.ok(player.controlBar.descriptionsButton.hasClass('vjs-hidden'),
           'control is not displayed');
  assert.equal(player.textTracks().length, 0, 'textTracks is empty');

  player.dispose();
});

QUnit.test('descriptions should not be displayed when last text track is removed', function(assert) {
  const player = TestHelpers.makePlayer({
    tracks: [descriptionstrack]
  });

  player.removeRemoteTextTrack(player.textTracks()[0]);

  assert.ok(player.controlBar.descriptionsButton.hasClass('vjs-hidden'),
           'control is not displayed');
  assert.equal(player.textTracks().length, 0, 'textTracks is empty');

  player.dispose();
});

QUnit.test('descriptions menu should contain "Off" and one track', function(assert) {
  const player = TestHelpers.makePlayer({
    tracks: [descriptionstrack]
  });

  this.clock.tick(1000);

  const menuItems = player.controlBar.descriptionsButton.items;

  assert.equal(menuItems.length, 2, 'descriptions menu contains two items');
  assert.equal(menuItems[0].track.label,
              'descriptions off',
              'menu contains "descriptions off"');
  assert.equal(menuItems[1].track.label, 'desc', 'menu contains "desc" track');

  player.dispose();
});

QUnit.test('enabling a captions track should disable the descriptions menu button', function(assert) {
  assert.expect(14);

  const player = TestHelpers.makePlayer({
    tracks: [track, descriptionstrack]
  });

  this.clock.tick(1000);

  assert.ok(!player.controlBar.captionsButton.hasClass('vjs-hidden'),
           'captions control is displayed');
  assert.ok(!player.controlBar.descriptionsButton.hasClass('vjs-hidden'),
           'descriptions control is displayed');
  assert.equal(player.textTracks().length, 2, 'textTracks contains two items');

  assert.ok(!player.controlBar.captionsButton.hasClass('vjs-disabled'),
           'captions control is NOT disabled');
  assert.ok(!player.controlBar.descriptionsButton.hasClass('vjs-disabled'),
           'descriptions control is NOT disabled');

  for (let i = 0; i < player.textTracks().length; i++) {
    if (player.textTracks()[i].kind === 'descriptions') {
      player.textTracks()[i].mode = 'showing';
      assert.ok(player.textTracks()[i].kind === 'descriptions' &&
               player.textTracks()[i].mode === 'showing',
               'descriptions mode set to showing');
    }
  }

  this.clock.tick(1000);

  assert.ok(!player.controlBar.captionsButton.hasClass('vjs-disabled'),
           'captions control is NOT disabled');
  assert.ok(!player.controlBar.descriptionsButton.hasClass('vjs-disabled'),
           'descriptions control is NOT disabled');

  for (let i = 0; i < player.textTracks().length; i++) {
    if (player.textTracks()[i].kind === 'captions') {
      player.textTracks()[i].mode = 'showing';
      assert.ok(player.textTracks()[i].kind === 'captions' &&
               player.textTracks()[i].mode === 'showing',
               'captions mode set to showing');
    }
  }

  this.clock.tick(1000);

  assert.ok(!player.controlBar.captionsButton.hasClass('vjs-disabled'),
           'captions control is NOT disabled');
  assert.ok(player.controlBar.descriptionsButton.hasClass('vjs-disabled'),
           'descriptions control IS disabled');

  for (let i = 0; i < player.textTracks().length; i++) {
    if (player.textTracks()[i].kind === 'captions') {
      player.textTracks()[i].mode = 'disabled';
      assert.ok(player.textTracks()[i].kind === 'captions' &&
               player.textTracks()[i].mode === 'disabled',
               'captions mode set to disabled');
    }
  }

  this.clock.tick(1000);

  assert.ok(!player.controlBar.captionsButton.hasClass('vjs-disabled'),
           'captions control is NOT disabled');
  assert.ok(!player.controlBar.descriptionsButton.hasClass('vjs-disabled'),
           'descriptions control is NOT disabled');

  player.dispose();
});

if (!browser.IS_IE8) {
  // This test doesn't work on IE8.
  // However, this test tests a specific with iOS7 where
  // the TextTrackList doesn't report track mode changes.
  // TODO: figure out why this test doens't work on IE8. https://github.com/videojs/video.js/issues/1861
  QUnit.test('menu items should polyfill mode change events', function(assert) {
    const player = TestHelpers.makePlayer({});
    let changes;

    // emulate a TextTrackList that doesn't report track mode changes,
    // like iOS7
    player.textTracks().onchange = undefined;
    const trackMenuItem = new TextTrackMenuItem(player, {
      track
    });

    player.textTracks().on('change', function() {
      changes++;
    });
    changes = 0;
    trackMenuItem.trigger('tap');
    assert.equal(changes, 1, 'taps trigger change events');

    trackMenuItem.trigger('click');
    assert.equal(changes, 2, 'clicks trigger change events');

    player.dispose();
  });
}

const chaptersTrack = {
  kind: 'chapters',
  label: 'Test Chapters'
};

test('chapters should not be displayed when text tracks list is empty', function() {
  const player = TestHelpers.makePlayer();

  ok(player.controlBar.chaptersButton.hasClass('vjs-hidden'), 'control is not displayed');
  equal(player.textTracks().length, 0, 'textTracks is empty');

  player.dispose();
});

test('chapters should not be displayed when there is chapters track but no cues', function() {
  const player = TestHelpers.makePlayer({
    tracks: [chaptersTrack]
  });

  this.clock.tick(1000);

  ok(player.controlBar.chaptersButton.hasClass('vjs-hidden'), 'chapters menu is not displayed');
  equal(player.textTracks().length, 1, 'textTracks contains one item');

  player.dispose();
});

test('chapters should be displayed when cues added to initial track and button updated', function() {
  const player = TestHelpers.makePlayer({
    tracks: [chaptersTrack]
  });

  this.clock.tick(1000);

  const chapters = player.textTracks()[0];

  chapters.addCue({
    startTime: 0,
    endTime: 2,
    text: 'Chapter 1'
  });
  chapters.addCue({
    startTime: 2,
    endTime: 4,
    text: 'Chapter 2'
  });
  equal(chapters.cues.length, 2);

  player.controlBar.chaptersButton.update();

  ok(!player.controlBar.chaptersButton.hasClass('vjs-hidden'), 'chapters menu is displayed');

  const menuItems = player.controlBar.chaptersButton.items;

  equal(menuItems.length, 2, 'menu contains two item');

  player.dispose();
});

test('chapters should be displayed when a track and its cures added and button updated', function() {
  const player = TestHelpers.makePlayer();

  this.clock.tick(1000);

  const chapters = player.addTextTrack('chapters', 'Test Chapters', 'en');

  chapters.addCue({
    startTime: 0,
    endTime: 2,
    text: 'Chapter 1'
  });
  chapters.addCue({
    startTime: 2,
    endTime: 4,
    text: 'Chapter 2'
  });
  equal(chapters.cues.length, 2);

  player.controlBar.chaptersButton.update();

  ok(!player.controlBar.chaptersButton.hasClass('vjs-hidden'), 'chapters menu is displayed');

  const menuItems = player.controlBar.chaptersButton.items;

  equal(menuItems.length, 2, 'menu contains two item');

  player.dispose();
});

test('chapters menu should use track label as menu title', function() {
  const player = TestHelpers.makePlayer({
    tracks: [chaptersTrack]
  });

  this.clock.tick(1000);

  const chapters = player.textTracks()[0];

  chapters.addCue({
    startTime: 0,
    endTime: 2,
    text: 'Chapter 1'
  });
  chapters.addCue({
    startTime: 2,
    endTime: 4,
    text: 'Chapter 2'
  });
  equal(chapters.cues.length, 2);

  player.controlBar.chaptersButton.update();

  const menu = player.controlBar.chaptersButton.menu;
  const titleEl = menu.contentEl().firstChild;
  const menuTitle = titleEl.textContent || titleEl.innerText;

  equal(menuTitle, 'Test Chapters', 'menu gets track label as title');

  player.dispose();
});

test('chapters should be displayed when remote track added and load event fired', function() {
  const player = TestHelpers.makePlayer();

  this.clock.tick(1000);

  const chaptersEl = player.addRemoteTextTrack(chaptersTrack, true);

  chaptersEl.track.addCue({
    startTime: 0,
    endTime: 2,
    text: 'Chapter 1'
  });
  chaptersEl.track.addCue({
    startTime: 2,
    endTime: 4,
    text: 'Chapter 2'
  });

  equal(chaptersEl.track.cues.length, 2);

  // Anywhere where we support using native text tracks, we can trigger a custom DOM event.
  // On IE8 and other places where we have emulated tracks, either we cannot trigger custom
  // DOM events (like IE8 with the custom DOM element) or we aren't using a DOM element at all.
  // In those cases just trigger `load` directly on the chaptersEl object.
  if (player.tech_.featuresNativeTextTracks) {
    TestHelpers.triggerDomEvent(chaptersEl, 'load');
  } else {
    chaptersEl.trigger('load');
  }

  ok(!player.controlBar.chaptersButton.hasClass('vjs-hidden'), 'chapters menu is displayed');

  const menuItems = player.controlBar.chaptersButton.items;

  equal(menuItems.length, 2, 'menu contains two item');

  player.dispose();
});


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

QUnit.module('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 content', function(assert) {
  const span = Dom.createEl('span');
  const div = Dom.createEl('div', undefined, undefined, span);

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

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

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

  assert.throws(function() {
    Dom.addClass(el, 'foo foo-bar');
  }, 'throws when attempting to add a class with whitespace');

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

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

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

  assert.expect(4);

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

  assert.throws(function() {
    Dom.removeClass(el, 'test2_className bar');
  }, 'throws when attempting to remove a class with whitespace');

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

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(3 + 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');

  assert.throws(function() {
    Dom.toggleClass(el, 'foo bar');
  }, 'throws when attempting to toggle a class with whitespace');

  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.top = '10px';
  d.style.left = '20px';
  d.style.position = 'absolute';

  assert.deepEqual(position,
                  {left: 0, top: 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, top: 10}, 'The position was not correct');

  d.getBoundingClientRect = null;
  position = Dom.findPosition(d);
  assert.deepEqual(position,
                  {left: 0, top: 0},
                  'If there is no gBCR, 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('getBoundingClientRect() shims only width and height for elements that do not return them', function(assert) {
  const oldGCS = window.getComputedStyle;

  // This is done so that we fall back to looking for the `currentStyle`
  // property on the mock element.
  window.getComputedStyle = null;

  const mockEl = {
    currentStyle: {
      height: '123',
      width: '456'
    },
    getBoundingClientRect: sinon.spy(() => {
      return {};
    }),
    parentNode: true
  };

  const actual = Dom.getBoundingClientRect(mockEl);

  assert.deepEqual(Object.keys(actual).sort(), ['height', 'width'], 'only "height" and "width" were shimmed');
  assert.strictEqual(actual.height, 123, '"height" was shimmed because it was missing');
  assert.strictEqual(actual.width, 456, '"width" was shimmed because it was missing');

  window.getComputedStyle = oldGCS;
});


import {throttle, bind} from '../../utils/fn.js';

/**
 * @file progress-control.js
 */
import Component from '../../component.js';
import * as Fn from '../../utils/fn.js';
import * as Dom from '../../utils/dom.js';
import { throttle, bind } from '../../utils/fn.js';

import './seek-bar.js';

/**
 * The Progress Control component contains the seek bar, load progress,
 * and play progress.
 *
 * @extends Component
 */
class ProgressControl extends Component {

  /**
   * Creates an instance of this class.
   *
   * @param {Player} 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.handleMouseMove = Fn.throttle(Fn.bind(this, this.handleMouseMove), 25);
    this.on(this.el_, 'mousemove', this.handleMouseMove);

    this.throttledHandleMouseSeek = throttle(bind(this, this.handleMouseSeek), 25);
    this.on(['mousedown', 'touchstart'], this.handleMouseDown);
  }

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

  /**
   * When the mouse moves over the `ProgressControl`, the pointer position
   * gets passed down to the `MouseTimeDisplay` component.
   *
   * @param {EventTarget~Event} event
   *        The `mousemove` event that caused this function to run.
   *
   * @listen mousemove
   */
  handleMouseMove(event) {
    const seekBar = this.getChild('seekBar');
    const seekBarEl = seekBar.el();
    const seekBarRect = Dom.getBoundingClientRect(seekBarEl);
    let seekBarPoint = Dom.getPointerPosition(seekBarEl, event).x;

    // The default skin has a gap on either side of the `SeekBar`. This means
    // that it's possible to trigger this behavior outside the boundaries of
    // the `SeekBar`. This ensures we stay within it at all times.
    if (seekBarPoint > 1) {
      seekBarPoint = 1;
    } else if (seekBarPoint < 0) {
      seekBarPoint = 0;
    }

    seekBar.getChild('mouseTimeDisplay').update(seekBarRect, seekBarPoint);
  }

  /**
   * A throttled version of the {@link ProgressControl#handleMouseSeek} listener.
   *
   * @method ProgressControl#throttledHandleMouseSeek
   * @param {EventTarget~Event} event
   *        The `mousemove` event that caused this function to run.
   *
   * @listen mousemove
   * @listen touchmove
   */

  /**
   * Handle `mousemove` or `touchmove` events on the `ProgressControl`.
   *
   * @param {EventTarget~Event} event
   *        `mousedown` or `touchstart` event that triggered this function
   *
   * @listens mousemove
   * @listens touchmove
   */
  handleMouseSeek(event) {
    const seekBar = this.getChild('seekBar');

    seekBar.handleMouseMove(event);
  }

  /**
   * Handle `mousedown` or `touchstart` events on the `ProgressControl`.
   *
   * @param {EventTarget~Event} event
   *        `mousedown` or `touchstart` event that triggered this function
   *
   * @listens mousedown
   * @listens touchstart
   */
  handleMouseDown(event) {
    const doc = this.el_.ownerDocument;

    this.on(doc, 'mousemove', this.throttledHandleMouseSeek);
    this.on(doc, 'touchmove', this.throttledHandleMouseSeek);
    this.on(doc, 'mouseup', this.handleMouseUp);
    this.on(doc, 'touchend', this.handleMouseUp);
  }

  /**
   * Handle `mouseup` or `touchend` events on the `ProgressControl`.
   *
   * @param {EventTarget~Event} event
   *        `mouseup` or `touchend` event that triggered this function.
   *
   * @listens touchend
   * @listens mouseup
   */
  handleMouseUp(event) {
    const doc = this.el_.ownerDocument;

    this.off(doc, 'mousemove', this.throttledHandleMouseSeek);
    this.off(doc, 'touchmove', this.throttledHandleMouseSeek);
    this.off(doc, 'mouseup', this.handleMouseUp);
    this.off(doc, 'touchend', this.handleMouseUp);
  }
}

/**
 * Default options for `ProgressControl`
 *
 * @type {Object}
 * @private
 */
ProgressControl.prototype.options_ = {
  children: [
    'seekBar'
  ]
};

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


Prompt: confirm

You will now be taken to Github where you will choose an account to fork the project under. Remember which account you choose.

Prompt: text

git clone https://github.com/{{ owner }}/{{ meta.name }}.git

Set up version control and install dependencies

git checkout -b stable origin/stable
Create the stable branch for patches

git remote add upstream https://github.com/{{meta.org}}/{{meta.name}}.git

Add the upstream project as a remote for pulling changes

git fetch upstream
Get all upstream branches and changes

git checkout master
Switch to the development branch

Get any changes to master in the main project

git checkout stable
Switch to the release branch

Get any changes to stable in the main project

git checkout patch
Switch to the patch branch

Get any changes to patch in the main project

Push any changes to your copy of the main project

Create a new feature or general enhancement

Submit a pull request for a feature when it's finished

Modify a submitted feature that you are reviewing

Create an urgent fix for the latest stable version

Make your changes and then run `contrib patch submit`

Submit a pull request for a patch when it's finished

Modify a submitted patch that you are reviewing

git checkout master
Checkout the developmet branch

git merge stable
Merge the patch changes

git push upstream master
Push the development changes

Prompt: text

Create a title that is descriptive of the problem

Prompt: text

Prompt: text

Prompt: text

Prompt: text

Prompt: text

Prompt: text

What browsers/platforms did you experience this in (e.g. Win 7, IE10; Android 4, Chrome;)?

Prompt: text

Is there a URL to a live example, or a jsbin (e.g. http://jsbin.com/axedog/9999/edit)?

Prompt: text

Are there any other details you'd like to provide?

{{meta.urls.repo_ui}}/issues/new?title={{title}}&body=**Steps to reproduce:**
> {{reproduce}}

**What was expected:**
> {{expected}}

**What Happened:**
> {{actual}}

**Video.js Version:**
> {{version}}

**Plugins:**
> {{plugins}}

**Browsers experienced on:**
> {{browsers}}

**Example:**
> {{example}}

**Other details:**
> {{details}}

Thanks for submitting a bug report! One of our contributors will address it as soon as possible.

Prompt: text

Create a title that is descriptive of the enhancement

Prompt: text

Describe the feature/enhancement (be as detailed as possible so it's clear who, why, and how it would be used)

Prompt: text

Is there any existing documentation or related specifications?

Prompt: text

Prompt: confirm

You will be redirected to Github where you can submit this issue, OK?

{{meta.urls.repo_ui}}/issues/new?title={{ title }}&body=**Describe the feature/enhancement:**
> {{ describe }}

**Existing docs/specs:**
> {{ docs }}

**Existing examples:**
> {{ examples }}

Thanks for submitting a feature request! One of our contributors will address it as soon as possible.

Create a patch release from the release branch (stable)

Create a minor release from the development branch (master)

Copy the latest development changes to the release branch

Merge package changes into the dev brach

git push upstream master
Push the dev branch changes to the repo

Create a patch release and tag it @next on npm

Create a minor release and tag it @next on npm

Create a major release and tag it @next on npm

git checkout -b temp-release-branch master

grunt version:{{release_type}}
Bump package versions

Return the current VJS Version from the package.json file

Update the changelog with the new release

git commit -am 'v{{version}}'
Add and commit the package changes

git checkout temp-release-branch
Checkout the temp branch again

Add the (otherwise ignored) release files

git commit -m 'v{{version}} dist'
Commit the dist changes

git tag -a v{{version}} -m 'v{{version}}'

git push upstream --tags
Push the new tag to the repo

grunt github-release:prerelease
Create a new pre-release on Github

npm publish --tag next
Publish to npm as 'next'

git branch -D temp-release-branch
Delete the temp release branch

Ensure dependency updates have been installed

Push the release branch changes to the repo

git checkout -b temp-release-branch stable

grunt github-release:release
Create a new release on Github

git checkout stable
Checkout the developmet branch

Prompt: text

Push the branch to your remote copy of the project

Commit and push changes made to files in the project

Prompt: text

Push the changes to your remote copy of the project

Make sure all changes have been saved (added and committed) or stashed

Prompt: confirm

Are you sure *{{branch_name}}* is the correct branch?

Prompt: text

Which github user or org are you submitting from?

Submit a pull request for when the change is finished

Prompt: text

git fetch {{pr.head.repo.ssh_url}} {{pr.head.ref}}

Get the pull request changes but don't merge them

git merge-base master FETCH_HEAD
Get the common ancestor commit
base

git checkout -b review-{{pr.user.login}}-{{pr.head.ref}} {{base}}

Create a new local branch for the pull request that has a base of the common commit

git merge FETCH_HEAD
Merge in the pull request changes

npm install
Install any new dependencies

Submit a modification to a pull request that you are currently reviewing

Prompt: text

Prompt: text

Get the PR commits to access author info

git checkout -b {{pr.user.login}}/{{pr.head.ref}} {{pr.base.ref}}

Create a new branch for merging the changes

git merge --no-commit --squash FETCH_HEAD

Merge the changes in without committing so they can be squashed

grunt test
Run tests to make sure they still pass

Prompt: text

Add the changlelog change to be committed

git commit -a --author='{{prCommits.[0].commit.author.name}} <{{prCommits.[0].commit.author.email}}>' -m '{{line}}. closes #{{prNum}}'

Prompt: confirm

git checkout {{pr.base.ref}}
Check out the base branch

git push upstream {{pr.base.ref}}
Push the changes to the main project

git branch -D {{pr.user.login}}/{{pr.head.ref}}

Delete the local branch used for merging


/**
 * Check if volume control is supported and if it isn't hide the
 * `Component` that was passed  using the `vjs-hidden` class.
 *
 * @param {Component} self
 *        The component that should be hidden if volume is unsupported
 *
 * @param {Player} player
 *        A reference to the player
 *
 * @private
 */
const checkVolumeSupport = function(self, player) {
  // hide volume controls when they're not supported by the current tech
  if (player.tech_ && !player.tech_.featuresVolumeControl) {
    self.addClass('vjs-hidden');
  }

  self.on(player, 'loadstart', function() {
    if (!player.tech_.featuresVolumeControl) {
      self.addClass('vjs-hidden');
    } else {
      self.removeClass('vjs-hidden');
    }
  });
};

export default checkVolumeSupport;


/* eslint-env qunit */
import CloseButton from '../../src/js/close-button';
import sinon from 'sinon';
import TestHelpers from './test-helpers';

QUnit.module('CloseButton', {

  beforeEach() {
    this.player = TestHelpers.makePlayer();
    this.btn = new CloseButton(this.player);
  },

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

QUnit.test('should create the expected element', function(assert) {
  const elAssertions = TestHelpers.assertEl(assert, this.btn.el(), {
    tagName: 'button',
    classes: [
      'vjs-button',
      'vjs-close-button',
      'vjs-control'
    ]
  });

  assert.expect(elAssertions.count + 1);
  elAssertions();
  assert.strictEqual(this.btn.el().querySelector('.vjs-control-text').innerHTML, 'Close');
});

QUnit.test('should allow setting the controlText_ property as an option', function(assert) {
  const text = 'OK!';
  const btn = new CloseButton(this.player, {controlText: text});

  assert.expect(1);
  assert.strictEqual(btn.controlText_, text, 'set the controlText_ property');
});

QUnit.test('should trigger an event on activation', function(assert) {
  const spy = sinon.spy();

  this.btn.on('close', spy);
  this.btn.trigger('click');
  assert.expect(1);
  assert.strictEqual(spy.callCount, 1, 'the "close" event was triggered');
});


*        The key/value store of player options.

this.handleMouseMove = Fn.throttle(Fn.bind(this, this.handleMouseMove), 25);

this.throttledHandleMouseSeek = throttle(bind(this, this.handleMouseSeek), 25);

keep keep keep keep keep replace keep keep keep keep keep

this.handleMouseMove = throttle(bind(this, this.handleMouseMove), 25);


.video-js .vjs-custom-control-spacer {
  display: none;
}


import AudioTrackList from './audio-track-list';
import VideoTrackList from './video-track-list';
import TextTrackList from './text-track-list';
import HTMLTrackElementList from './html-track-element-list';

import TextTrack from './text-track';
import AudioTrack from './audio-track';
import VideoTrack from './video-track';
import HTMLTrackElement from './html-track-element';

import mergeOptions from '../utils/merge-options';

/*
 * This file contains all track properties that are used in
 * player.js, tech.js, html5.js and possibly other techs in the future.
 */

const NORMAL = {
  audio: {
    ListClass: AudioTrackList,
    TrackClass: AudioTrack,
    capitalName: 'Audio'
  },
  video: {
    ListClass: VideoTrackList,
    TrackClass: VideoTrack,
    capitalName: 'Video'
  },
  text: {
    ListClass: TextTrackList,
    TrackClass: TextTrack,
    capitalName: 'Text'
  }
};

Object.keys(NORMAL).forEach(function(type) {
  NORMAL[type].getterName = `${type}Tracks`;
  NORMAL[type].privateName = `${type}Tracks_`;
});

const REMOTE = {
  remoteText: {
    ListClass: TextTrackList,
    TrackClass: TextTrack,
    capitalName: 'RemoteText',
    getterName: 'remoteTextTracks',
    privateName: 'remoteTextTracks_'
  },
  remoteTextEl: {
    ListClass: HTMLTrackElementList,
    TrackClass: HTMLTrackElement,
    capitalName: 'RemoteTextTrackEls',
    getterName: 'remoteTextTrackEls',
    privateName: 'remoteTextTrackEls_'
  }
};

const ALL = mergeOptions(NORMAL, REMOTE);

REMOTE.names = Object.keys(REMOTE);
NORMAL.names = Object.keys(NORMAL);
ALL.names = [].concat(REMOTE.names).concat(NORMAL.names);

export {
  NORMAL,
  REMOTE,
  ALL
};


Video indirilirken bağlantı sorunu oluştu.

Video oynatılamadı, ağ ya da sunucu hatası veya belirtilen format desteklenmiyor.

Tarayıcınız desteklemediği için videoda hata oluştu.

Video, şifrelenmiş bir kaynaktan geliyor ve oynatmak için gerekli anahtar bulunamadı.

Bu modal ESC tuşuna basarak ya da kapata tıklanarak kapatılabilir.

Yazı

Context: Arka plan

Diyalog penceresinin başlangıcı. ESC tuşu işlemi iptal edip pencereyi kapatacaktır.


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

const seekBarRect = Dom.getBoundingClientRect(seekBarEl);

let seekBarPoint = Dom.getPointerPosition(seekBarEl, event).x;

const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay');


En netværksfejl fik download af videoen til at fejle.

Videoen kunne ikke indlæses, enten fordi serveren eller netværket fejlede, eller fordi formatet ikke er understøttet.

Videoafspilningen blev afbrudt på grund af ødelagte data eller fordi videoen benyttede faciliteter som din browser ikke understøtter.

Fandt ikke en kompatibel kilde for denne media.


زمان باقیمانده

فراخوانی شده

بدون زیرنویس

شما پخش را متوقف کردید.

مشکل در دریافت ویدئو ...

فرمت پشتیبانی نمیشود یا خطایی روی داده است.

مشکل در دریافت ویدئو ...

هیچ ورودی ای برای این رسانه شناسایی نشد.


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

process.exit(0);


* A throttled version of the {@link ProgressControl#handleMouseSeek} listener.

keep keep keep keep replace keep keep keep keep


Video se prestao preuzimati zbog greške na mreži.

Video se ne može reproducirati zbog servera, greške u mreži ili je format ne podržan.

Reprodukcija videa je zaustavljenja zbog greške u formatu ili zbog verzije vašeg pretraživača.

Nije nađen nijedan kompatibilan izvor ovog videa.


/**
 * @file obj.js
 * @module obj
 */

/**
 * @callback obj:EachCallback
 *
 * @param {Mixed} value
 *        The current key for the object that is being iterated over.
 *
 * @param {string} key
 *        The current key-value for object that is being iterated over
 */

/**
 * @callback obj:ReduceCallback
 *
 * @param {Mixed} accum
 *        The value that is accumulating over the reduce loop.
 *
 * @param {Mixed} value
 *        The current key for the object that is being iterated over.
 *
 * @param {string} key
 *        The current key-value for object that is being iterated over
 *
 * @return {Mixed}
 *         The new accumulated value.
 */
const toString = Object.prototype.toString;

/**
 * Array-like iteration for objects.
 *
 * @param {Object} object
 *        The object to iterate over
 *
 * @param {obj:EachCallback} fn
 *        The callback function which is called for each key in the object.
 */
export function each(object, fn) {
  Object.keys(object).forEach(key => fn(object[key], key));
}

/**
 * Array-like reduce for objects.
 *
 * @param {Object} object
 *        The Object that you want to reduce.
 *
 * @param {Function} fn
 *         A callback function which is called for each key in the object. It
 *         receives the accumulated value and the per-iteration value and key
 *         as arguments.
 *
 * @param {Mixed} [initial = 0]
 *        Starting value
 *
 * @return {Mixed}
 *         The final accumulated value.
 */
export function reduce(object, fn, initial = 0) {
  return Object.keys(object).reduce(
    (accum, key) => fn(accum, object[key], key), initial);
}

/**
 * Object.assign-style object shallow merge/extend.
 *
 * @param  {Object} target
 * @param  {Object} ...sources
 * @return {Object}
 */
export function assign(target, ...sources) {
  if (Object.assign) {
    return Object.assign(target, ...sources);
  }

  sources.forEach(source => {
    if (!source) {
      return;
    }

    each(source, (value, key) => {
      target[key] = value;
    });
  });

  return target;
}

/**
 * Returns whether a value is an object of any kind - including DOM nodes,
 * arrays, regular expressions, etc. Not functions, though.
 *
 * This avoids the gotcha where using `typeof` on a `null` value
 * results in `'object'`.
 *
 * @param  {Object} value
 * @return {Boolean}
 */
export function isObject(value) {
  return !!value && typeof value === 'object';
}

/**
 * Returns whether an object appears to be a "plain" object - that is, a
 * direct instance of `Object`.
 *
 * @param  {Object} value
 * @return {Boolean}
 */
export function isPlain(value) {
  return isObject(value) &&
    toString.call(value) === '[object Object]' &&
    value.constructor === Object;
}


/**
 * @file chapters-track-menu-item.js
 */
import MenuItem from '../../menu/menu-item.js';
import Component from '../../component.js';
import * as Fn from '../../utils/fn.js';

/**
 * The chapter track menu item
 *
 * @extends MenuItem
 */
class ChaptersTrackMenuItem extends MenuItem {

  /**
   * Creates an instance of this class.
   *
   * @param {Player} player
   *        The `Player` that this class should be attached to.
   *
   * @param {Object} [options]
   *        The key/value store of player options.
   */
  constructor(player, options) {
    const track = options.track;
    const cue = options.cue;
    const currentTime = player.currentTime();

    // Modify options for parent MenuItem class's init.
    options.selectable = true;
    options.label = cue.text;
    options.selected = (cue.startTime <= currentTime && currentTime < cue.endTime);
    super(player, options);

    this.track = track;
    this.cue = cue;
    track.addEventListener('cuechange', Fn.bind(this, this.update));
  }

  /**
   * This gets called when an `ChaptersTrackMenuItem` is "clicked". See
   * {@link ClickableComponent} for more detailed information on what a click can be.
   *
   * @param {EventTarget~Event} [event]
   *        The `keydown`, `tap`, or `click` event that caused this function to be
   *        called.
   *
   * @listens tap
   * @listens click
   */
  handleClick(event) {
    super.handleClick();
    this.player_.currentTime(this.cue.startTime);
    this.update(this.cue.startTime);
  }

  /**
   * Update chapter menu item
   *
   * @param {EventTarget~Event} [event]
   *        The `cuechange` event that caused this function to run.
   *
   * @listens TextTrack#cuechange
   */
  update(event) {
    const cue = this.cue;
    const currentTime = this.player_.currentTime();

    // vjs.log(currentTime, cue.startTime);
    this.selected(cue.startTime <= currentTime && currentTime < cue.endTime);
  }

}

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


import Slider from '../../slider/slider.js';

import formatTime from '../../utils/format-time.js';

import {IE_VERSION} from '../../utils/browser.js';


/* eslint-env qunit */
import { createTimeRanges, createTimeRange } from '../../../src/js/utils/time-ranges.js';

QUnit.module('time-ranges');

QUnit.test('should export the deprecated createTimeRange function', function(assert) {
  assert.equal(createTimeRange,
              createTimeRanges,
              'createTimeRange is an alias to createTimeRanges');
});

QUnit.test('should create a fake single timerange', function(assert) {
  const tr = createTimeRanges(0, 10);

  assert.equal(tr.length, 1, 'length should be 1');
  assert.equal(tr.start(0),
              0,
              'works if start is called with valid index');
  assert.equal(tr.end(0),
              10,
              'works if end is called with with valid index');
  assert.throws(()=>tr.start(1),
                /Failed to execute 'start'/,
                'fails if start is called with an invalid index');
  assert.throws(()=>tr.end(1),
                /Failed to execute 'end'/,
                'fails if end is called with with an invalid index');
});

QUnit.test('should create a fake multiple timerange', function(assert) {
  const tr = createTimeRanges([
    [0, 10],
    [11, 20]
  ]);

  assert.equal(tr.length, 2, 'length should equal 2');
  assert.equal(tr.start(1), 11, 'works if start is called with valid index');
  assert.equal(tr.end(1), 20, 'works if end is called with with valid index');
  assert.throws(()=>tr.start(-1),
                /Failed to execute 'start'/,
                'fails if start is called with an invalid index');
  assert.throws(()=>tr.end(-1),
                /Failed to execute 'end'/,
                'fails if end is called with with an invalid index');
});

QUnit.test('should throw without being given an index', function(assert) {
  const tr = createTimeRanges([
    [0, 10],
    [11, 20]
  ]);

  assert.throws(
    () => tr.start(),
    /Failed to execute 'start'/,
    'start throws if no index is given'
  );

  assert.throws(
    () => tr.end(),
    /Failed to execute 'end'/,
    'end throws if no index is given'
  );
});


// Emulated tracks
.vjs-text-track-display {
  position: absolute;
  bottom: 3em;
  left: 0;
  right: 0;
  top: 0;
  pointer-events: none;
}

// Move captions down when controls aren't being shown
.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display {
  bottom: 1em;
}

// Individual tracks
.video-js .vjs-text-track {
  font-size: 1.4em;
  text-align: center;
  margin-bottom: 0.1em;
  // Transparent black background, or fallback to all black (oldIE)
  @include background-color-with-alpha(#000, 0.5);
}

.vjs-subtitles { color: #fff; } // Subtitles are white
.vjs-captions { color: #fc6; } // Captions are yellow
.vjs-tt-cue { display: block; }

// Native tracks
video::-webkit-media-text-track-display {
  @include transform(translateY(-3em));
}

// Move captions down when controls aren't being shown
.video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display {
  @include transform(translateY(-1.5em));
}


SeekBar.prototype.options_ = {
children: [
'loadProgressBar',
'mouseTimeDisplay',
'playProgressBar'
],
barName: 'playProgressBar'
};

keep keep keep replace keep keep keep keep


/**
 * @file live-display.js
 */
import Component from '../component';
import * as Dom from '../utils/dom.js';

// TODO - Future make it click to snap to live

/**
 * Displays the live indicator when duration is Infinity.
 *
 * @extends Component
 */
class LiveDisplay extends Component {

  /**
   * Creates an instance of this class.
   *
   * @param {Player} 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.updateShowing();
    this.on(this.player(), 'durationchange', this.updateShowing);
  }

  /**
   * Create the `Component`'s DOM element
   *
   * @return {Element}
   *         The element that was created.
   */
  createEl() {
    const el = super.createEl('div', {
      className: 'vjs-live-control vjs-control'
    });

    this.contentEl_ = Dom.createEl('div', {
      className: 'vjs-live-display',
      innerHTML: `<span class="vjs-control-text">${this.localize('Stream Type')}</span>${this.localize('LIVE')}`
    }, {
      'aria-live': 'off'
    });

    el.appendChild(this.contentEl_);
    return el;
  }

  /**
   * Check the duration to see if the LiveDisplay should be showing or not. Then show/hide
   * it accordingly
   *
   * @param {EventTarget~Event} [event]
   *        The {@link Player#durationchange} event that caused this function to run.
   *
   * @listens Player#durationchange
   */
  updateShowing(event) {
    if (this.player().duration() === Infinity) {
      this.show();
    } else {
      this.hide();
    }
  }

}

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


module.exports = function(grunt) {
  grunt.registerTask('vjs-version', function() {
    let version = grunt.vjsVersion.full;
    grunt.log.writeln(version);
    return version;
  });
}


/* eslint-env qunit */
import document from 'global/document';
import * as DomData from '../../../src/js/utils/dom-data';

QUnit.module('dom-data');

QUnit.test('should get and remove data from an element', function(assert) {
  const el = document.createElement('div');
  const data = DomData.getData(el);

  assert.strictEqual(typeof data, 'object', 'data object created');

  // Add data
  const testData = {asdf: 'fdsa'};

  data.test = testData;
  assert.strictEqual(DomData.getData(el).test, testData, 'data added');

  // Remove all data
  DomData.removeData(el);

  assert.notOk(DomData.hasData(el), 'cached item emptied');
});


keep keep keep keep add keep keep keep keep keep

/**
 * @file seek-bar.js
 */
import Slider from '../../slider/slider.js';
import Component from '../../component.js';
import * as Dom from '../../utils/dom.js';
import * as Fn from '../../utils/fn.js';
import formatTime from '../../utils/format-time.js';

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;

/**
 * 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 {Player} 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.update = Fn.throttle(Fn.bind(this, this.update), 50);
    this.on(player, ['timeupdate', 'ended'], this.update);
  }

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

  /**
   * Update the seek bar's UI.
   *
   * @param {EventTarget~Event} [event]
   *        The `timeupdate` or `ended` event that caused this to run.
   *
   * @listens Player#timeupdate
   * @listens Player#ended
   */
  update() {
    const percent = super.update();
    const duration = this.player_.duration();

    // Allows for smooth scrubbing, when player can't keep up.
    const time = (this.player_.scrubbing()) ?
      this.player_.getCache().currentTime :
      this.player_.currentTime();

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

    // human readable value of progress bar (time complete)
    this.el_.setAttribute('aria-valuetext', formatTime(time, duration));

    // Update the `PlayProgressBar`.
    this.bar.update(Dom.getBoundingClientRect(this.el_), percent);

    return percent;
  }

  /**
   * Get the percentage of media played so far.
   *
   * @return {number}
   *         The percentage of media played so far (0 to 1).
   */
  getPercent() {

    // Allows for smooth scrubbing, when player can't keep up.
    const time = (this.player_.scrubbing()) ?
      this.player_.getCache().currentTime :
      this.player_.currentTime();

    const percent = time / this.player_.duration();

    return percent >= 1 ? 1 : percent;
  }

  /**
   * Handle mouse down on seek bar
   *
   * @param {EventTarget~Event} event
   *        The `mousedown` event that caused this to run.
   *
   * @listens mousedown
   */
  handleMouseDown(event) {
    this.player_.scrubbing(true);

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

    super.handleMouseDown(event);
  }

  /**
   * Handle mouse move on seek bar
   *
   * @param {EventTarget~Event} event
   *        The `mousemove` event that caused this to run.
   *
   * @listens mousemove
   */
  handleMouseMove(event) {
    let newTime = this.calculateDistance(event) * this.player_.duration();

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

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

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

    this.player_.scrubbing(false);
    if (this.videoWasPlaying) {
      this.player_.play();
    }
  }

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

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

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

/**
 * Call the update event for this Slider when this event happens on the player.
 *
 * @type {string}
 */
SeekBar.prototype.playerEvent = 'timeupdate';

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


/**
 * @file volume-level.js
 */
import Component from '../../component.js';

/**
 * Shows volume level
 *
 * @extends Component
 */
class VolumeLevel extends Component {

  /**
   * Create the `Component`'s DOM element
   *
   * @return {Element}
   *         The element that was created.
   */
  createEl() {
    return super.createEl('div', {
      className: 'vjs-volume-level',
      innerHTML: ''
    });
  }

}

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


/* eslint-env qunit */
import mergeOptions from '../../../src/js/utils/merge-options.js';

QUnit.module('merge-options');
QUnit.test('should merge options objects', function(assert) {
  const ob1 = {
    a: true,
    b: { b1: true, b2: true, b3: true },
    c: true
  };

  const ob2 = {
    // override value
    a: false,
    // merge sub-option values
    b: { b1: true, b2: false, b4: true },
    // add new option
    d: true
  };

  const ob3 = mergeOptions(ob1, ob2);

  assert.deepEqual(ob3, {
    a: false,
    b: { b1: true, b2: false, b3: true, b4: true },
    c: true,
    d: true
  }, 'options objects merged correctly');
});


.video-js .vjs-time-control {
  @include flex(none);
  font-size: 1em;
  line-height: 3em;
  min-width: 2em;
  width: auto;
  padding-left: 1em;
  padding-right: 1em;
}

.vjs-live .vjs-time-control {
  display: none;
}

// We need the extra specificity that referencing .vjs-no-flex provides.
.video-js .vjs-current-time,
.vjs-no-flex .vjs-current-time {
  display: none;
}

// IE 8 + IE 9 width: auto container fix
.vjs-no-flex .vjs-remaining-time.vjs-time-control.vjs-control {
  width: 0px !important;
  white-space: nowrap;
}

.video-js .vjs-duration,
.vjs-no-flex .vjs-duration {
  display: none;
}

.vjs-time-divider {
  display: none;
  line-height: 3em;
}

.vjs-live .vjs-time-divider {
  // Already the default, but we want to ensure when the player is live
  // this hides in the same way as the other time controls for other skins
  display: none;
}


"menu/type":          { "attribOption": ["context", "list", "toolbar"] },

"script/type":        { "attribOption": ["text/javascript", "text/ecmascript", "text/jscript", "text/livescript", "text/tcl", "text/x-javascript", "text/x-ecmascript",

"application/x-javascript", "application/x-ecmascript", "application/javascript", "application/ecmascript"] },

"style/type":         { "attribOption": ["text/css"] },

"input/type":         { "attribOption": ["button", "checkbox", "color", "date", "datetime", "datetime-local", "email", "file", "hidden", "image", "month",

"number", "password", "radio", "range", "reset", "search", "submit", "tel", "text", "time", "url", "week"] },

"application/x-javascript", "application/x-ecmascript", "application/javascript", "application/ecmascript",

after-edge alphabetic auto baseline before-edge central hanging ideographic inherit mathematical middle text-after-edge text-before-edge

auto inherit optimizeQuality optimizeSpeed

application/ecmascript application/javascript application/x-ecmascript application/x-javascript text/ecmascript text/javascript text/jscript text/livescript text/tcl text/x-ecmascript text/x-javascript

auto crosshair default e-resize help inherit move n-resize ne-resize nw-resize pointer s-resize se-resize sw-resize text w-resize wait

block compact inherit inline inline-table list-item marker none run-in table table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group

alphabetic auto central hanging ideographic inherit mathematical middle no-change reset-size text-after-edge text-before-edge use-script

hueRotate luminanceToAlpha matrix saturate

all condensed expanded extra-condensed extra-expanded inherit narrower normal semi-condensed semi-expanded ultra-condensed ultra-expanded wider

matrix() rotate() scale() skewX() skewY() translate()

BackgroundAlpha BackgroundImage FillPaint SourceAlpha SourceGraphic StrokePaint

aa ab af am ar as ay az ba be bg bh bi bn bo br ca co cs cy da de dz el en eo es et eu fa fi fj fo fr fy ga gd gl gn gu ha hi hr hu hy ia ie ik in is it iw ja ji jw ka kk kl km kn ko ks ku ky la ln lo lt lv mg mi mk ml mn mo mr ms mt my na ne nl no oc om or pa pl ps pt qu rm rn ro ru rw sa sd sg sh si sk sl sm sn so sq sr ss st su sv sw ta te tg th ti tk tl tn to tr ts tt tw uk ur uz vi vo wo xh yo zh zu

all fill inherit none painted stroke visible visibleFill visiblePainted visibleStroke

meet none slice xMaxYMax xMaxYMid xMaxYMin xMidYMax xMidYMid xMidYMin xMinYMax xMinYMid xMinYMin

absolute-colorimetric auto perceptual relative-colorimetric saturation

auto crispEdges geometricPrecision inherit optimizeSpeed

blink inherit line-through none overline underline

auto geometricPrecision inherit optimizeLegibility optimizeSpeed


var fs   = require('fs'),
    os   = require('osenv'),
    path = require('path'),
    cnst = require('constants');

/* HELPERS */

var RDWR_EXCL = cnst.O_CREAT | cnst.O_TRUNC | cnst.O_RDWR | cnst.O_EXCL;

var environmentVariables = ['TMPDIR', 'TMP', 'TEMP'];

var generateName = function(rawAffixes, defaultPrefix) {
  var affixes = parseAffixes(rawAffixes, defaultPrefix);
  var now = new Date();
  var name = [affixes.prefix,
              now.getYear(), now.getMonth(), now.getDate(),
              '-',
              process.pid,
              '-',
              (Math.random() * 0x100000000 + 1).toString(36),
              affixes.suffix].join('');
  return path.join(affixes.dir || exports.dir, name);
}

var parseAffixes = function(rawAffixes, defaultPrefix) {
  var affixes = {prefix: null, suffix: null};
  if(rawAffixes) {
    switch (typeof(rawAffixes)) {
    case 'string':
      affixes.prefix = rawAffixes;
      break;
    case 'object':
      affixes = rawAffixes;
      break
    default:
      throw("Unknown affix declaration: " + affixes);
    }
  } else {
    affixes.prefix = defaultPrefix;
  }
  return affixes;
}

/* -------------------------------------------------------------------------
 * Don't forget to call track() if you want file tracking and exit handlers!
 * -------------------------------------------------------------------------
 * When any temp file or directory is created, it is added to filesToDelete
 * or dirsToDelete. The first time any temp file is created, a listener is
 * added to remove all temp files and directories at exit.
 */
var tracking = false;
var track = function(value) {
  tracking = (value != false)
};
var exitListenerAttached = false;
var filesToDelete = [];
var dirsToDelete = [];

var deleteFileOnExit = function(filePath) {
  if (!tracking) return false;
  attachExitListener();
  filesToDelete.push(filePath);
};

var deleteDirOnExit = function(dirPath) {
  if (!tracking) return false;
  attachExitListener();
  dirsToDelete.push(dirPath);
};

var attachExitListener = function() {
  if (!tracking) return false;
  if (!exitListenerAttached) {
    process.addListener('exit', cleanup);
    exitListenerAttached = true;
  }
};

var cleanupFiles = function() {
  if (!tracking) return false;
  var counts = {removed: 0, missing: 0}
  var toDelete;
  while (toDelete = filesToDelete.shift()) {
    try {
      fs.unlinkSync(toDelete);
      counts.removed++;
    } catch (rmErr) {
      /* removed normally */
      counts.missing++;
    }
  }
  return counts;
};

var cleanupDirs = function() {
  if (!tracking) return false;
  var rimrafSync = require('rimraf').sync;
  var counts = {removed: 0, missing: 0}
  var toDelete;
  while (toDelete = dirsToDelete.shift()) {
    try {
      rimrafSync(toDelete, function (er) {
        if (er) {
          throw er;
        } else {
          counts.removed++;
        }
      });
    } catch (rmErr) {
      /* removed normally */
      counts.missing++;
    }
  }
  return counts;
};

var cleanup = function() {
  if (!tracking) return false;
  fileCount = cleanupFiles();
  dirCount  = cleanupDirs();
  return {files: fileCount, dirs: dirCount};
}

/* DIRECTORIES */

var mkdir = function(affixes, callback) {
  var dirPath = generateName(affixes, 'd-');
  fs.mkdir(dirPath, 0700, function(err) {
    if (!err) {
      deleteDirOnExit(dirPath);
    }
    if (callback)
      callback(err, dirPath);
  });
}
var mkdirSync = function(affixes) {
  var dirPath = generateName(affixes, 'd-');
  fs.mkdirSync(dirPath, 0700);
  deleteDirOnExit(dirPath);
  return dirPath;
}

/* FILES */

var open = function(affixes, callback) {
  var filePath = generateName(affixes, 'f-')
  fs.open(filePath, RDWR_EXCL, 0600, function(err, fd) {
    if (!err)
      deleteFileOnExit(filePath);
    if (callback)
      callback(err, {path: filePath, fd: fd});
  });
}

var openSync = function(affixes) {
  var filePath = generateName(affixes, 'f-')
  var fd = fs.openSync(filePath, RDWR_EXCL, 0600);
  deleteFileOnExit(filePath);
  return {path: filePath, fd: fd};
}

var createWriteStream = function(affixes) {
  var filePath = generateName(affixes, 's-')
  var stream = fs.createWriteStream(filePath, {flags: RDWR_EXCL, mode: 0600});
  deleteFileOnExit(filePath);
  return stream;
}

/* EXPORTS */
// Settings
exports.dir               = path.resolve(os.tmpdir());
exports.track             = track;
// Functions
exports.mkdir             = mkdir;
exports.mkdirSync         = mkdirSync;
exports.open              = open;
exports.openSync          = openSync;
exports.path              = generateName;
exports.cleanup           = cleanup;
exports.createWriteStream = createWriteStream;