diff options
Diffstat (limited to 'javascript/videojs/src/js/player.js')
| -rw-r--r-- | javascript/videojs/src/js/player.js | 5621 |
1 files changed, 0 insertions, 5621 deletions
diff --git a/javascript/videojs/src/js/player.js b/javascript/videojs/src/js/player.js deleted file mode 100644 index 6b2f424..0000000 --- a/javascript/videojs/src/js/player.js +++ /dev/null @@ -1,5621 +0,0 @@ -/** - * @file player.js - */ -// Subclasses Component -import Component from './component.js'; - -import {version} from '../../package.json'; -import document from 'global/document'; -import window from 'global/window'; -import evented from './mixins/evented'; -import {isEvented, addEventedCallback} from './mixins/evented'; -import * as Events from './utils/events.js'; -import * as Dom from './utils/dom.js'; -import * as Fn from './utils/fn.js'; -import * as Guid from './utils/guid.js'; -import * as browser from './utils/browser.js'; -import {IS_CHROME, IS_WINDOWS} from './utils/browser.js'; -import log, { createLogger } from './utils/log.js'; -import {toTitleCase, titleCaseEquals} from './utils/str.js'; -import { createTimeRange } from './utils/time.js'; -import { bufferedPercent } from './utils/buffer.js'; -import * as stylesheet from './utils/stylesheet.js'; -import FullscreenApi from './fullscreen-api.js'; -import MediaError from './media-error.js'; -import {merge} from './utils/obj'; -import {silencePromise, isPromise} from './utils/promise'; -import textTrackConverter from './tracks/text-track-list-converter.js'; -import ModalDialog from './modal-dialog'; -import Tech from './tech/tech.js'; -import * as middleware from './tech/middleware.js'; -import {ALL as TRACK_TYPES} from './tracks/track-types'; -import filterSource from './utils/filter-source'; -import {getMimetype, findMimetype} from './utils/mimetypes'; -import {hooks} from './utils/hooks'; -import {isObject} from './utils/obj'; -import icons from '../images/icons.svg'; -import SpatialNavigation from './spatial-navigation.js'; - -// The following imports are used only to ensure that the corresponding modules -// are always included in the video.js package. Importing the modules will -// execute them and they will register themselves with video.js. -import './tech/loader.js'; -import './poster-image.js'; -import './tracks/text-track-display.js'; -import './loading-spinner.js'; -import './big-play-button.js'; -import './close-button.js'; -import './control-bar/control-bar.js'; -import './error-display.js'; -import './tracks/text-track-settings.js'; -import './resize-manager.js'; -import './live-tracker.js'; -import './title-bar.js'; -import './transient-button.js'; - -// Import Html5 tech, at least for disposing the original video tag. -import './tech/html5.js'; - -/** @import AudioTrackList from './tracks/audio-track-list' */ -/** @import HtmlTrackElement from './tracks/html-track-element' */ -/** @import HtmlTrackElementList from './tracks/html-track-element-list' */ -/** @import TextTrackList from './tracks/text-track-list' */ -/** @import { TimeRange } from './utils/time' */ -/** @import VideoTrackList from './tracks/video-track-list' */ - -/** - * @callback PlayerReadyCallback - * @this {Player} - * @returns {void} - */ - -// The following tech events are simply re-triggered -// on the player when they happen -const TECH_EVENTS_RETRIGGER = [ - /** - * Fired while the user agent is downloading media data. - * - * @event Player#progress - * @type {Event} - */ - /** - * Retrigger the `progress` event that was triggered by the {@link Tech}. - * - * @private - * @method Player#handleTechProgress_ - * @fires Player#progress - * @listens Tech#progress - */ - 'progress', - - /** - * Fires when the loading of an audio/video is aborted. - * - * @event Player#abort - * @type {Event} - */ - /** - * Retrigger the `abort` event that was triggered by the {@link Tech}. - * - * @private - * @method Player#handleTechAbort_ - * @fires Player#abort - * @listens Tech#abort - */ - 'abort', - - /** - * Fires when the browser is intentionally not getting media data. - * - * @event Player#suspend - * @type {Event} - */ - /** - * Retrigger the `suspend` event that was triggered by the {@link Tech}. - * - * @private - * @method Player#handleTechSuspend_ - * @fires Player#suspend - * @listens Tech#suspend - */ - 'suspend', - - /** - * Fires when the current playlist is empty. - * - * @event Player#emptied - * @type {Event} - */ - /** - * Retrigger the `emptied` event that was triggered by the {@link Tech}. - * - * @private - * @method Player#handleTechEmptied_ - * @fires Player#emptied - * @listens Tech#emptied - */ - 'emptied', - /** - * Fires when the browser is trying to get media data, but data is not available. - * - * @event Player#stalled - * @type {Event} - */ - /** - * Retrigger the `stalled` event that was triggered by the {@link Tech}. - * - * @private - * @method Player#handleTechStalled_ - * @fires Player#stalled - * @listens Tech#stalled - */ - 'stalled', - - /** - * Fires when the browser has loaded meta data for the audio/video. - * - * @event Player#loadedmetadata - * @type {Event} - */ - /** - * Retrigger the `loadedmetadata` event that was triggered by the {@link Tech}. - * - * @private - * @method Player#handleTechLoadedmetadata_ - * @fires Player#loadedmetadata - * @listens Tech#loadedmetadata - */ - 'loadedmetadata', - - /** - * Fires when the browser has loaded the current frame of the audio/video. - * - * @event Player#loadeddata - * @type {event} - */ - /** - * Retrigger the `loadeddata` event that was triggered by the {@link Tech}. - * - * @private - * @method Player#handleTechLoaddeddata_ - * @fires Player#loadeddata - * @listens Tech#loadeddata - */ - 'loadeddata', - - /** - * Fires when the current playback position has changed. - * - * @event Player#timeupdate - * @type {event} - */ - /** - * Retrigger the `timeupdate` event that was triggered by the {@link Tech}. - * - * @private - * @method Player#handleTechTimeUpdate_ - * @fires Player#timeupdate - * @listens Tech#timeupdate - */ - 'timeupdate', - - /** - * Fires when the video's intrinsic dimensions change - * - * @event Player#resize - * @type {event} - */ - /** - * Retrigger the `resize` event that was triggered by the {@link Tech}. - * - * @private - * @method Player#handleTechResize_ - * @fires Player#resize - * @listens Tech#resize - */ - 'resize', - - /** - * Fires when the volume has been changed - * - * @event Player#volumechange - * @type {event} - */ - /** - * Retrigger the `volumechange` event that was triggered by the {@link Tech}. - * - * @private - * @method Player#handleTechVolumechange_ - * @fires Player#volumechange - * @listens Tech#volumechange - */ - 'volumechange', - - /** - * Fires when the text track has been changed - * - * @event Player#texttrackchange - * @type {event} - */ - /** - * Retrigger the `texttrackchange` event that was triggered by the {@link Tech}. - * - * @private - * @method Player#handleTechTexttrackchange_ - * @fires Player#texttrackchange - * @listens Tech#texttrackchange - */ - 'texttrackchange' -]; - -// events to queue when playback rate is zero -// this is a hash for the sole purpose of mapping non-camel-cased event names -// to camel-cased function names -const TECH_EVENTS_QUEUE = { - canplay: 'CanPlay', - canplaythrough: 'CanPlayThrough', - playing: 'Playing', - seeked: 'Seeked' -}; - -const BREAKPOINT_ORDER = [ - 'tiny', - 'xsmall', - 'small', - 'medium', - 'large', - 'xlarge', - 'huge' -]; - -const BREAKPOINT_CLASSES = {}; - -// grep: vjs-layout-tiny -// grep: vjs-layout-x-small -// grep: vjs-layout-small -// grep: vjs-layout-medium -// grep: vjs-layout-large -// grep: vjs-layout-x-large -// grep: vjs-layout-huge -BREAKPOINT_ORDER.forEach(k => { - const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k; - - BREAKPOINT_CLASSES[k] = `vjs-layout-${v}`; -}); - -const DEFAULT_BREAKPOINTS = { - tiny: 210, - xsmall: 320, - small: 425, - medium: 768, - large: 1440, - xlarge: 2560, - huge: Infinity -}; - -/** - * An instance of the `Player` class is created when any of the Video.js setup methods - * are used to initialize a video. - * - * After an instance has been created it can be accessed globally in three ways: - * 1. By calling `videojs.getPlayer('example_video_1');` - * 2. By calling `videojs('example_video_1');` (not recommended) - * 2. By using it directly via `videojs.players.example_video_1;` - * - * @extends Component - * @global - */ -class Player extends Component { - - /** - * Create an instance of this class. - * - * @param {Element} tag - * The original video DOM element used for configuring options. - * - * @param {Object} [options] - * Object of option names and values. - * - * @param {PlayerReadyCallback} [ready] - * Ready callback function. - */ - constructor(tag, options, ready) { - // Make sure tag ID exists - // also here.. probably better - tag.id = tag.id || options.id || `vjs_video_${Guid.newGUID()}`; - - // Set Options - // The options argument overrides options set in the video tag - // which overrides globally set options. - // This latter part coincides with the load order - // (tag must exist before Player) - options = Object.assign(Player.getTagSettings(tag), options); - - // Delay the initialization of children because we need to set up - // player properties first, and can't use `this` before `super()` - options.initChildren = false; - - // Same with creating the element - options.createEl = false; - - // don't auto mixin the evented mixin - options.evented = false; - - // we don't want the player to report touch activity on itself - // see enableTouchActivity in Component - options.reportTouchActivity = false; - - // If language is not set, get the closest lang attribute - if (!options.language) { - const closest = tag.closest('[lang]'); - - if (closest) { - options.language = closest.getAttribute('lang'); - } - } - - // Run base component initializing with new options - super(null, options, ready); - - // Create bound methods for document listeners. - this.boundDocumentFullscreenChange_ = (e) => this.documentFullscreenChange_(e); - this.boundFullWindowOnEscKey_ = (e) => this.fullWindowOnEscKey(e); - - this.boundUpdateStyleEl_ = (e) => this.updateStyleEl_(e); - this.boundApplyInitTime_ = (e) => this.applyInitTime_(e); - this.boundUpdateCurrentBreakpoint_ = (e) => this.updateCurrentBreakpoint_(e); - - this.boundHandleTechClick_ = (e) => this.handleTechClick_(e); - this.boundHandleTechDoubleClick_ = (e) => this.handleTechDoubleClick_(e); - this.boundHandleTechTouchStart_ = (e) => this.handleTechTouchStart_(e); - this.boundHandleTechTouchMove_ = (e) => this.handleTechTouchMove_(e); - this.boundHandleTechTouchEnd_ = (e) => this.handleTechTouchEnd_(e); - this.boundHandleTechTap_ = (e) => this.handleTechTap_(e); - - this.boundUpdatePlayerHeightOnAudioOnlyMode_ = (e) => this.updatePlayerHeightOnAudioOnlyMode_(e); - - // default isFullscreen_ to false - this.isFullscreen_ = false; - - // create logger - this.log = createLogger(this.id_); - - // Hold our own reference to fullscreen api so it can be mocked in tests - this.fsApi_ = FullscreenApi; - - // Tracks when a tech changes the poster - this.isPosterFromTech_ = false; - - // Holds callback info that gets queued when playback rate is zero - // and a seek is happening - this.queuedCallbacks_ = []; - - // Turn off API access because we're loading a new tech that might load asynchronously - this.isReady_ = false; - - // Init state hasStarted_ - this.hasStarted_ = false; - - // Init state userActive_ - this.userActive_ = false; - - // Init debugEnabled_ - this.debugEnabled_ = false; - - // Init state audioOnlyMode_ - this.audioOnlyMode_ = false; - - // Init state audioPosterMode_ - this.audioPosterMode_ = false; - - // Init state audioOnlyCache_ - this.audioOnlyCache_ = { - controlBarHeight: null, - playerHeight: null, - hiddenChildren: [] - }; - - // if the global option object was accidentally blown away by - // someone, bail early with an informative error - if (!this.options_ || - !this.options_.techOrder || - !this.options_.techOrder.length) { - throw new Error('No techOrder specified. Did you overwrite ' + - 'videojs.options instead of just changing the ' + - 'properties you want to override?'); - } - - // Store the original tag used to set options - this.tag = tag; - - // Store the tag attributes used to restore html5 element - this.tagAttributes = tag && Dom.getAttributes(tag); - - // Update current language - this.language(this.options_.language); - - // Update Supported Languages - if (options.languages) { - // Normalise player option languages to lowercase - const languagesToLower = {}; - - Object.getOwnPropertyNames(options.languages).forEach(function(name) { - languagesToLower[name.toLowerCase()] = options.languages[name]; - }); - this.languages_ = languagesToLower; - } else { - this.languages_ = Player.prototype.options_.languages; - } - - this.resetCache_(); - - // Set poster - /** @type string */ - this.poster_ = options.poster || ''; - - // Set controls - /** @type {boolean} */ - this.controls_ = !!options.controls; - - // Original tag settings stored in options - // now remove immediately so native controls don't flash. - // May be turned back on by HTML5 tech if nativeControlsForTouch is true - tag.controls = false; - tag.removeAttribute('controls'); - - this.changingSrc_ = false; - this.playCallbacks_ = []; - this.playTerminatedQueue_ = []; - - // the attribute overrides the option - if (tag.hasAttribute('autoplay')) { - this.autoplay(true); - } else { - // otherwise use the setter to validate and - // set the correct value. - this.autoplay(this.options_.autoplay); - } - - // check plugins - if (options.plugins) { - Object.keys(options.plugins).forEach((name) => { - if (typeof this[name] !== 'function') { - throw new Error(`plugin "${name}" does not exist`); - } - }); - } - - /* - * Store the internal state of scrubbing - * - * @private - * @return {Boolean} True if the user is scrubbing - */ - this.scrubbing_ = false; - - this.el_ = this.createEl(); - - // Make this an evented object and use `el_` as its event bus. - evented(this, {eventBusKey: 'el_'}); - - // listen to document and player fullscreenchange handlers so we receive those events - // before a user can receive them so we can update isFullscreen appropriately. - // make sure that we listen to fullscreenchange events before everything else to make sure that - // our isFullscreen method is updated properly for internal components as well as external. - if (this.fsApi_.requestFullscreen) { - Events.on(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_); - this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_); - } - - if (this.fluid_) { - this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_); - } - // We also want to pass the original player options to each component and plugin - // as well so they don't need to reach back into the player for options later. - // We also need to do another copy of this.options_ so we don't end up with - // an infinite loop. - const playerOptionsCopy = merge(this.options_); - - // Load plugins - if (options.plugins) { - Object.keys(options.plugins).forEach((name) => { - this[name](options.plugins[name]); - }); - } - - // Enable debug mode to fire debugon event for all plugins. - if (options.debug) { - this.debug(true); - } - - this.options_.playerOptions = playerOptionsCopy; - - this.middleware_ = []; - - this.playbackRates(options.playbackRates); - - if (options.experimentalSvgIcons) { - // Add SVG Sprite to the DOM - const parser = new window.DOMParser(); - const parsedSVG = parser.parseFromString(icons, 'image/svg+xml'); - const errorNode = parsedSVG.querySelector('parsererror'); - - if (errorNode) { - log.warn('Failed to load SVG Icons. Falling back to Font Icons.'); - this.options_.experimentalSvgIcons = null; - } else { - const sprite = parsedSVG.documentElement; - - sprite.style.display = 'none'; - this.el_.appendChild(sprite); - - this.addClass('vjs-svg-icons-enabled'); - } - } - - this.initChildren(); - - // Set isAudio based on whether or not an audio tag was used - this.isAudio(tag.nodeName.toLowerCase() === 'audio'); - - // Update controls className. Can't do this when the controls are initially - // set because the element doesn't exist yet. - if (this.controls()) { - this.addClass('vjs-controls-enabled'); - } else { - this.addClass('vjs-controls-disabled'); - } - - // Set ARIA label and region role depending on player type - this.el_.setAttribute('role', 'region'); - if (this.isAudio()) { - this.el_.setAttribute('aria-label', this.localize('Audio Player')); - } else { - this.el_.setAttribute('aria-label', this.localize('Video Player')); - } - - if (this.isAudio()) { - this.addClass('vjs-audio'); - } - - // Check if spatial navigation is enabled in the options. - // If enabled, instantiate the SpatialNavigation class. - if (options.spatialNavigation && options.spatialNavigation.enabled) { - this.spatialNavigation = new SpatialNavigation(this); - this.addClass('vjs-spatial-navigation-enabled'); - } - - // TODO: Make this smarter. Toggle user state between touching/mousing - // using events, since devices can have both touch and mouse events. - // TODO: Make this check be performed again when the window switches between monitors - // (See https://github.com/videojs/video.js/issues/5683) - if (browser.TOUCH_ENABLED) { - this.addClass('vjs-touch-enabled'); - } - - // iOS Safari has broken hover handling - if (!browser.IS_IOS) { - this.addClass('vjs-workinghover'); - } - - // Make player easily findable by ID - Player.players[this.id_] = this; - - // Add a major version class to aid css in plugins - const majorVersion = version.split('.')[0]; - - this.addClass(`vjs-v${majorVersion}`); - - // When the player is first initialized, trigger activity so components - // like the control bar show themselves if needed - this.userActive(true); - this.reportUserActivity(); - - this.one('play', (e) => this.listenForUserActivity_(e)); - this.on('keydown', (e) => this.handleKeyDown(e)); - this.on('languagechange', (e) => this.handleLanguagechange(e)); - - this.breakpoints(this.options_.breakpoints); - this.responsive(this.options_.responsive); - - // Calling both the audio mode methods after the player is fully - // setup to be able to listen to the events triggered by them - this.on('ready', () => { - // Calling the audioPosterMode method first so that - // the audioOnlyMode can take precedence when both options are set to true - this.audioPosterMode(this.options_.audioPosterMode); - this.audioOnlyMode(this.options_.audioOnlyMode); - }); - } - - /** - * Destroys the video player and does any necessary cleanup. - * - * This is especially helpful if you are dynamically adding and removing videos - * to/from the DOM. - * - * @fires Player#dispose - */ - dispose() { - /** - * Called when the player is being disposed of. - * - * @event Player#dispose - * @type {Event} - */ - this.trigger('dispose'); - // prevent dispose from being called twice - this.off('dispose'); - - // Make sure all player-specific document listeners are unbound. This is - Events.off(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_); - Events.off(document, 'keydown', this.boundFullWindowOnEscKey_); - - if (this.styleEl_ && this.styleEl_.parentNode) { - this.styleEl_.parentNode.removeChild(this.styleEl_); - this.styleEl_ = null; - } - - // Kill reference to this player - Player.players[this.id_] = null; - - if (this.tag && this.tag.player) { - this.tag.player = null; - } - - if (this.el_ && this.el_.player) { - this.el_.player = null; - } - - if (this.tech_) { - this.tech_.dispose(); - this.isPosterFromTech_ = false; - this.poster_ = ''; - } - - if (this.playerElIngest_) { - this.playerElIngest_ = null; - } - - if (this.tag) { - this.tag = null; - } - - middleware.clearCacheForPlayer(this); - - // remove all event handlers for track lists - // all tracks and track listeners are removed on - // tech dispose - TRACK_TYPES.names.forEach((name) => { - const props = TRACK_TYPES[name]; - const list = this[props.getterName](); - - // if it is not a native list - // we have to manually remove event listeners - if (list && list.off) { - list.off(); - } - }); - - // the actual .el_ is removed here, or replaced if - super.dispose({restoreEl: this.options_.restoreEl}); - } - - /** - * Create the `Player`'s DOM element. - * - * @return {Element} - * The DOM element that gets created. - */ - createEl() { - let tag = this.tag; - let el; - let playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player'); - const divEmbed = this.tag.tagName.toLowerCase() === 'video-js'; - - if (playerElIngest) { - el = this.el_ = tag.parentNode; - } else if (!divEmbed) { - el = this.el_ = super.createEl('div'); - } - - // Copy over all the attributes from the tag, including ID and class - // ID will now reference player box, not the video tag - const attrs = Dom.getAttributes(tag); - - if (divEmbed) { - el = this.el_ = tag; - tag = this.tag = document.createElement('video'); - while (el.children.length) { - tag.appendChild(el.firstChild); - } - - if (!Dom.hasClass(el, 'video-js')) { - Dom.addClass(el, 'video-js'); - } - - el.appendChild(tag); - - playerElIngest = this.playerElIngest_ = el; - // move properties over from our custom `video-js` element - // to our new `video` element. This will move things like - // `src` or `controls` that were set via js before the player - // was initialized. - Object.keys(el).forEach((k) => { - try { - tag[k] = el[k]; - } catch (e) { - // we got a a property like outerHTML which we can't actually copy, ignore it - } - }); - } - - // set tabindex to -1 to remove the video element from the focus order - tag.setAttribute('tabindex', '-1'); - attrs.tabindex = '-1'; - - // Workaround for #4583 on Chrome (on Windows) with JAWS. - // See https://github.com/FreedomScientific/VFO-standards-support/issues/78 - // Note that we can't detect if JAWS is being used, but this ARIA attribute - // doesn't change behavior of Chrome if JAWS is not being used - if (IS_CHROME && IS_WINDOWS) { - tag.setAttribute('role', 'application'); - attrs.role = 'application'; - } - - // Remove width/height attrs from tag so CSS can make it 100% width/height - tag.removeAttribute('width'); - tag.removeAttribute('height'); - - if ('width' in attrs) { - delete attrs.width; - } - if ('height' in attrs) { - delete attrs.height; - } - - Object.getOwnPropertyNames(attrs).forEach(function(attr) { - // don't copy over the class attribute to the player element when we're in a div embed - // the class is already set up properly in the divEmbed case - // and we want to make sure that the `video-js` class doesn't get lost - if (!(divEmbed && attr === 'class')) { - el.setAttribute(attr, attrs[attr]); - } - - if (divEmbed) { - tag.setAttribute(attr, attrs[attr]); - } - }); - - // Update tag id/class for use as HTML5 playback tech - // Might think we should do this after embedding in container so .vjs-tech class - // doesn't flash 100% width/height, but class only applies with .video-js parent - tag.playerId = tag.id; - tag.id += '_html5_api'; - tag.className = 'vjs-tech'; - - // Make player findable on elements - tag.player = el.player = this; - // Default state of video is paused - this.addClass('vjs-paused'); - - const deviceClassNames = [ - 'IS_SMART_TV', - 'IS_TIZEN', - 'IS_WEBOS', - 'IS_ANDROID', - 'IS_IPAD', - 'IS_IPHONE', - 'IS_CHROMECAST_RECEIVER' - ].filter(key => browser[key]).map(key => { - return 'vjs-device-' + key.substring(3).toLowerCase().replace(/\_/g, '-'); - }); - - this.addClass(...deviceClassNames); - - // Add a style element in the player that we'll use to set the width/height - // of the player in a way that's still overridable by CSS, just like the - // video element - if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true) { - this.styleEl_ = stylesheet.createStyleElement('vjs-styles-dimensions'); - const defaultsStyleEl = Dom.$('.vjs-styles-defaults'); - const head = Dom.$('head'); - - head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild); - } - - this.fill_ = false; - this.fluid_ = false; - - // Pass in the width/height/aspectRatio options which will update the style el - this.width(this.options_.width); - this.height(this.options_.height); - this.fill(this.options_.fill); - this.fluid(this.options_.fluid); - this.aspectRatio(this.options_.aspectRatio); - // support both crossOrigin and crossorigin to reduce confusion and issues around the name - this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin); - - // Hide any links within the video/audio tag, - // because IE doesn't hide them completely from screen readers. - const links = tag.getElementsByTagName('a'); - - for (let i = 0; i < links.length; i++) { - const linkEl = links.item(i); - - Dom.addClass(linkEl, 'vjs-hidden'); - linkEl.setAttribute('hidden', 'hidden'); - } - - // insertElFirst seems to cause the networkState to flicker from 3 to 2, so - // keep track of the original for later so we can know if the source originally failed - tag.initNetworkState_ = tag.networkState; - - // Wrap video tag in div (el/box) container - if (tag.parentNode && !playerElIngest) { - tag.parentNode.insertBefore(el, tag); - } - - // insert the tag as the first child of the player element - // then manually add it to the children array so that this.addChild - // will work properly for other components - // - // Breaks iPhone, fixed in HTML5 setup. - Dom.prependTo(tag, el); - this.children_.unshift(tag); - - // Set lang attr on player to ensure CSS :lang() in consistent with player - // if it's been set to something different to the doc - this.el_.setAttribute('lang', this.language_); - - this.el_.setAttribute('translate', 'no'); - - this.el_ = el; - - return el; - } - - /** - * Get or set the `Player`'s crossOrigin option. For the HTML5 player, this - * sets the `crossOrigin` property on the `<video>` tag to control the CORS - * behavior. - * - * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin} - * - * @param {string|null} [value] - * The value to set the `Player`'s crossOrigin to. If an argument is - * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'. - * - * @return {string|null|undefined} - * - The current crossOrigin value of the `Player` when getting. - * - undefined when setting - */ - crossOrigin(value) { - // `null` can be set to unset a value - if (typeof value === 'undefined') { - return this.techGet_('crossOrigin'); - } - - if (value !== null && value !== 'anonymous' && value !== 'use-credentials') { - log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`); - return; - } - - this.techCall_('setCrossOrigin', value); - if (this.posterImage) { - this.posterImage.crossOrigin(value); - } - - return; - } - - /** - * A getter/setter for the `Player`'s width. Returns the player's configured value. - * To get the current width use `currentWidth()`. - * - * @param {number|string} [value] - * CSS value to set the `Player`'s width to. - * - * @return {number|undefined} - * - The current width of the `Player` when getting. - * - Nothing when setting - */ - width(value) { - return this.dimension('width', value); - } - - /** - * A getter/setter for the `Player`'s height. Returns the player's configured value. - * To get the current height use `currentheight()`. - * - * @param {number|string} [value] - * CSS value to set the `Player`'s height to. - * - * @return {number|undefined} - * - The current height of the `Player` when getting. - * - Nothing when setting - */ - height(value) { - return this.dimension('height', value); - } - - /** - * A getter/setter for the `Player`'s width & height. - * - * @param {string} dimension - * This string can be: - * - 'width' - * - 'height' - * - * @param {number|string} [value] - * Value for dimension specified in the first argument. - * - * @return {number} - * The dimension arguments value when getting (width/height). - */ - dimension(dimension, value) { - const privDimension = dimension + '_'; - - if (value === undefined) { - return this[privDimension] || 0; - } - - if (value === '' || value === 'auto') { - // If an empty string is given, reset the dimension to be automatic - this[privDimension] = undefined; - this.updateStyleEl_(); - return; - } - - const parsedVal = parseFloat(value); - - if (isNaN(parsedVal)) { - log.error(`Improper value "${value}" supplied for for ${dimension}`); - return; - } - - this[privDimension] = parsedVal; - this.updateStyleEl_(); - } - - /** - * A getter/setter/toggler for the vjs-fluid `className` on the `Player`. - * - * Turning this on will turn off fill mode. - * - * @param {boolean} [bool] - * - A value of true adds the class. - * - A value of false removes the class. - * - No value will be a getter. - * - * @return {boolean|undefined} - * - The value of fluid when getting. - * - `undefined` when setting. - */ - fluid(bool) { - if (bool === undefined) { - return !!this.fluid_; - } - - this.fluid_ = !!bool; - - if (isEvented(this)) { - this.off(['playerreset', 'resize'], this.boundUpdateStyleEl_); - } - if (bool) { - this.addClass('vjs-fluid'); - this.fill(false); - addEventedCallback(this, () => { - this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_); - }); - } else { - this.removeClass('vjs-fluid'); - } - - this.updateStyleEl_(); - } - - /** - * A getter/setter/toggler for the vjs-fill `className` on the `Player`. - * - * Turning this on will turn off fluid mode. - * - * @param {boolean} [bool] - * - A value of true adds the class. - * - A value of false removes the class. - * - No value will be a getter. - * - * @return {boolean|undefined} - * - The value of fluid when getting. - * - `undefined` when setting. - */ - fill(bool) { - if (bool === undefined) { - return !!this.fill_; - } - - this.fill_ = !!bool; - - if (bool) { - this.addClass('vjs-fill'); - this.fluid(false); - } else { - this.removeClass('vjs-fill'); - } - } - - /** - * Get/Set the aspect ratio - * - * @param {string} [ratio] - * Aspect ratio for player - * - * @return {string|undefined} - * returns the current aspect ratio when getting - */ - - /** - * A getter/setter for the `Player`'s aspect ratio. - * - * @param {string} [ratio] - * The value to set the `Player`'s aspect ratio to. - * - * @return {string|undefined} - * - The current aspect ratio of the `Player` when getting. - * - undefined when setting - */ - aspectRatio(ratio) { - if (ratio === undefined) { - return this.aspectRatio_; - } - - // Check for width:height format - if (!(/^\d+\:\d+$/).test(ratio)) { - throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.'); - } - this.aspectRatio_ = ratio; - - // We're assuming if you set an aspect ratio you want fluid mode, - // because in fixed mode you could calculate width and height yourself. - this.fluid(true); - - this.updateStyleEl_(); - } - - /** - * Update styles of the `Player` element (height, width and aspect ratio). - * - * @private - * @listens Tech#loadedmetadata - */ - updateStyleEl_() { - if (window.VIDEOJS_NO_DYNAMIC_STYLE === true) { - const width = typeof this.width_ === 'number' ? this.width_ : this.options_.width; - const height = typeof this.height_ === 'number' ? this.height_ : this.options_.height; - const techEl = this.tech_ && this.tech_.el(); - - if (techEl) { - if (width >= 0) { - techEl.width = width; - } - if (height >= 0) { - techEl.height = height; - } - } - - return; - } - - let width; - let height; - let aspectRatio; - let idClass; - - // The aspect ratio is either used directly or to calculate width and height. - if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') { - // Use any aspectRatio that's been specifically set - aspectRatio = this.aspectRatio_; - } else if (this.videoWidth() > 0) { - // Otherwise try to get the aspect ratio from the video metadata - aspectRatio = this.videoWidth() + ':' + this.videoHeight(); - } else { - // Or use a default. The video element's is 2:1, but 16:9 is more common. - aspectRatio = '16:9'; - } - - // Get the ratio as a decimal we can use to calculate dimensions - const ratioParts = aspectRatio.split(':'); - const ratioMultiplier = ratioParts[1] / ratioParts[0]; - - if (this.width_ !== undefined) { - // Use any width that's been specifically set - width = this.width_; - } else if (this.height_ !== undefined) { - // Or calculate the width from the aspect ratio if a height has been set - width = this.height_ / ratioMultiplier; - } else { - // Or use the video's metadata, or use the video el's default of 300 - width = this.videoWidth() || 300; - } - - if (this.height_ !== undefined) { - // Use any height that's been specifically set - height = this.height_; - } else { - // Otherwise calculate the height from the ratio and the width - height = width * ratioMultiplier; - } - - // Ensure the CSS class is valid by starting with an alpha character - if ((/^[^a-zA-Z]/).test(this.id())) { - idClass = 'dimensions-' + this.id(); - } else { - idClass = this.id() + '-dimensions'; - } - - // Ensure the right class is still on the player for the style element - this.addClass(idClass); - - stylesheet.setTextContent(this.styleEl_, ` - .${idClass} { - width: ${width}px; - height: ${height}px; - } - - .${idClass}.vjs-fluid:not(.vjs-audio-only-mode) { - padding-top: ${ratioMultiplier * 100}%; - } - `); - } - - /** - * Load/Create an instance of playback {@link Tech} including element - * and API methods. Then append the `Tech` element in `Player` as a child. - * - * @param {string} techName - * name of the playback technology - * - * @param {string} source - * video source - * - * @private - */ - loadTech_(techName, source) { - - // Pause and remove current playback technology - if (this.tech_) { - this.unloadTech_(); - } - - const titleTechName = toTitleCase(techName); - const camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1); - - // get rid of the HTML5 video tag as soon as we are using another tech - if (titleTechName !== 'Html5' && this.tag) { - Tech.getTech('Html5').disposeMediaElement(this.tag); - this.tag.player = null; - this.tag = null; - } - - this.techName_ = titleTechName; - - // Turn off API access because we're loading a new tech that might load asynchronously - this.isReady_ = false; - - let autoplay = this.autoplay(); - - // if autoplay is a string (or `true` with normalizeAutoplay: true) we pass false to the tech - // because the player is going to handle autoplay on `loadstart` - if (typeof this.autoplay() === 'string' || this.autoplay() === true && this.options_.normalizeAutoplay) { - autoplay = false; - } - - // Grab tech-specific options from player options and add source and parent element to use. - const techOptions = { - source, - autoplay, - 'nativeControlsForTouch': this.options_.nativeControlsForTouch, - 'playerId': this.id(), - 'techId': `${this.id()}_${camelTechName}_api`, - 'playsinline': this.options_.playsinline, - 'preload': this.options_.preload, - 'loop': this.options_.loop, - 'disablePictureInPicture': this.options_.disablePictureInPicture, - 'muted': this.options_.muted, - 'poster': this.poster(), - 'language': this.language(), - 'playerElIngest': this.playerElIngest_ || false, - 'vtt.js': this.options_['vtt.js'], - 'canOverridePoster': !!this.options_.techCanOverridePoster, - 'enableSourceset': this.options_.enableSourceset - }; - - TRACK_TYPES.names.forEach((name) => { - const props = TRACK_TYPES[name]; - - techOptions[props.getterName] = this[props.privateName]; - }); - - Object.assign(techOptions, this.options_[titleTechName]); - Object.assign(techOptions, this.options_[camelTechName]); - Object.assign(techOptions, this.options_[techName.toLowerCase()]); - - if (this.tag) { - techOptions.tag = this.tag; - } - - if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) { - techOptions.startTime = this.cache_.currentTime; - } - - // Initialize tech instance - const TechClass = Tech.getTech(techName); - - if (!TechClass) { - throw new Error(`No Tech named '${titleTechName}' exists! '${titleTechName}' should be registered using videojs.registerTech()'`); - } - - this.tech_ = new TechClass(techOptions); - - // player.triggerReady is always async, so don't need this to be async - this.tech_.ready(Fn.bind_(this, this.handleTechReady_), true); - - textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_); - - // Listen to all HTML5-defined events and trigger them on the player - TECH_EVENTS_RETRIGGER.forEach((event) => { - this.on(this.tech_, event, (e) => this[`handleTech${toTitleCase(event)}_`](e)); - }); - - Object.keys(TECH_EVENTS_QUEUE).forEach((event) => { - this.on(this.tech_, event, (eventObj) => { - if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) { - this.queuedCallbacks_.push({ - callback: this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this), - event: eventObj - }); - return; - } - this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj); - }); - }); - - this.on(this.tech_, 'loadstart', (e) => this.handleTechLoadStart_(e)); - this.on(this.tech_, 'sourceset', (e) => this.handleTechSourceset_(e)); - this.on(this.tech_, 'waiting', (e) => this.handleTechWaiting_(e)); - this.on(this.tech_, 'ended', (e) => this.handleTechEnded_(e)); - this.on(this.tech_, 'seeking', (e) => this.handleTechSeeking_(e)); - this.on(this.tech_, 'play', (e) => this.handleTechPlay_(e)); - this.on(this.tech_, 'pause', (e) => this.handleTechPause_(e)); - this.on(this.tech_, 'durationchange', (e) => this.handleTechDurationChange_(e)); - this.on(this.tech_, 'fullscreenchange', (e, data) => this.handleTechFullscreenChange_(e, data)); - this.on(this.tech_, 'fullscreenerror', (e, err) => this.handleTechFullscreenError_(e, err)); - this.on(this.tech_, 'enterpictureinpicture', (e) => this.handleTechEnterPictureInPicture_(e)); - this.on(this.tech_, 'leavepictureinpicture', (e) => this.handleTechLeavePictureInPicture_(e)); - this.on(this.tech_, 'error', (e) => this.handleTechError_(e)); - this.on(this.tech_, 'posterchange', (e) => this.handleTechPosterChange_(e)); - this.on(this.tech_, 'textdata', (e) => this.handleTechTextData_(e)); - this.on(this.tech_, 'ratechange', (e) => this.handleTechRateChange_(e)); - this.on(this.tech_, 'loadedmetadata', this.boundUpdateStyleEl_); - - this.usingNativeControls(this.techGet_('controls')); - - if (this.controls() && !this.usingNativeControls()) { - this.addTechControlsListeners_(); - } - - // Add the tech element in the DOM if it was not already there - // Make sure to not insert the original video element if using Html5 - if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) { - Dom.prependTo(this.tech_.el(), this.el()); - } - - // Get rid of the original video tag reference after the first tech is loaded - if (this.tag) { - this.tag.player = null; - this.tag = null; - } - } - - /** - * Unload and dispose of the current playback {@link Tech}. - * - * @private - */ - unloadTech_() { - // Save the current text tracks so that we can reuse the same text tracks with the next tech - TRACK_TYPES.names.forEach((name) => { - const props = TRACK_TYPES[name]; - - this[props.privateName] = this[props.getterName](); - }); - this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_); - - this.isReady_ = false; - - this.tech_.dispose(); - - this.tech_ = false; - - if (this.isPosterFromTech_) { - this.poster_ = ''; - this.trigger('posterchange'); - } - - this.isPosterFromTech_ = false; - } - - /** - * Return a reference to the current {@link Tech}. - * It will print a warning by default about the danger of using the tech directly - * but any argument that is passed in will silence the warning. - * - * @param {*} [safety] - * Anything passed in to silence the warning - * - * @return {Tech} - * The Tech - */ - tech(safety) { - if (safety === undefined) { - log.warn('Using the tech directly can be dangerous. I hope you know what you\'re doing.\n' + - 'See https://github.com/videojs/video.js/issues/2617 for more info.\n'); - } - - return this.tech_; - } - - /** - * An object that contains Video.js version. - * - * @typedef {Object} PlayerVersion - * - * @property {string} 'video.js' - Video.js version - */ - - /** - * Returns an object with Video.js version. - * - * @return {PlayerVersion} - * An object with Video.js version. - */ - version() { - return { - 'video.js': version - }; - } - - /** - * Set up click and touch listeners for the playback element - * - * - On desktops: a click on the video itself will toggle playback - * - On mobile devices: a click on the video toggles controls - * which is done by toggling the user state between active and - * inactive - * - A tap can signal that a user has become active or has become inactive - * e.g. a quick tap on an iPhone movie should reveal the controls. Another - * quick tap should hide them again (signaling the user is in an inactive - * viewing state) - * - In addition to this, we still want the user to be considered inactive after - * a few seconds of inactivity. - * - * > Note: the only part of iOS interaction we can't mimic with this setup - * is a touch and hold on the video element counting as activity in order to - * keep the controls showing, but that shouldn't be an issue. A touch and hold - * on any controls will still keep the user active - * - * @private - */ - addTechControlsListeners_() { - // Make sure to remove all the previous listeners in case we are called multiple times. - this.removeTechControlsListeners_(); - - this.on(this.tech_, 'click', this.boundHandleTechClick_); - this.on(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_); - - // If the controls were hidden we don't want that to change without a tap event - // so we'll check if the controls were already showing before reporting user - // activity - this.on(this.tech_, 'touchstart', this.boundHandleTechTouchStart_); - this.on(this.tech_, 'touchmove', this.boundHandleTechTouchMove_); - this.on(this.tech_, 'touchend', this.boundHandleTechTouchEnd_); - - // The tap listener needs to come after the touchend listener because the tap - // listener cancels out any reportedUserActivity when setting userActive(false) - this.on(this.tech_, 'tap', this.boundHandleTechTap_); - } - - /** - * Remove the listeners used for click and tap controls. This is needed for - * toggling to controls disabled, where a tap/touch should do nothing. - * - * @private - */ - removeTechControlsListeners_() { - // We don't want to just use `this.off()` because there might be other needed - // listeners added by techs that extend this. - this.off(this.tech_, 'tap', this.boundHandleTechTap_); - this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_); - this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_); - this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_); - this.off(this.tech_, 'click', this.boundHandleTechClick_); - this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_); - } - - /** - * Player waits for the tech to be ready - * - * @private - */ - handleTechReady_() { - this.triggerReady(); - - // Keep the same volume as before - if (this.cache_.volume) { - this.techCall_('setVolume', this.cache_.volume); - } - - // Look if the tech found a higher resolution poster while loading - this.handleTechPosterChange_(); - - // Update the duration if available - this.handleTechDurationChange_(); - } - - /** - * Retrigger the `loadstart` event that was triggered by the {@link Tech}. - * - * @fires Player#loadstart - * @listens Tech#loadstart - * @private - */ - handleTechLoadStart_() { - // TODO: Update to use `emptied` event instead. See #1277. - - this.removeClass('vjs-ended', 'vjs-seeking'); - - // reset the error state - this.error(null); - - // Update the duration - this.handleTechDurationChange_(); - - if (!this.paused()) { - /** - * Fired when the user agent begins looking for media data - * - * @event Player#loadstart - * @type {Event} - */ - this.trigger('loadstart'); - } else { - // reset the hasStarted state - this.hasStarted(false); - this.trigger('loadstart'); - } - - // autoplay happens after loadstart for the browser, - // so we mimic that behavior - this.manualAutoplay_(this.autoplay() === true && this.options_.normalizeAutoplay ? 'play' : this.autoplay()); - } - - /** - * Handle autoplay string values, rather than the typical boolean - * values that should be handled by the tech. Note that this is not - * part of any specification. Valid values and what they do can be - * found on the autoplay getter at Player#autoplay() - */ - manualAutoplay_(type) { - if (!this.tech_ || typeof type !== 'string') { - return; - } - - // Save original muted() value, set muted to true, and attempt to play(). - // On promise rejection, restore muted from saved value - const resolveMuted = () => { - const previouslyMuted = this.muted(); - - this.muted(true); - - const restoreMuted = () => { - this.muted(previouslyMuted); - }; - - // restore muted on play terminatation - this.playTerminatedQueue_.push(restoreMuted); - - const mutedPromise = this.play(); - - if (!isPromise(mutedPromise)) { - return; - } - - return mutedPromise.catch(err => { - restoreMuted(); - throw new Error(`Rejection at manualAutoplay. Restoring muted value. ${err ? err : ''}`); - }); - }; - - let promise; - - // if muted defaults to true - // the only thing we can do is call play - if (type === 'any' && !this.muted()) { - promise = this.play(); - - if (isPromise(promise)) { - promise = promise.catch(resolveMuted); - } - } else if (type === 'muted' && !this.muted()) { - promise = resolveMuted(); - } else { - promise = this.play(); - } - - if (!isPromise(promise)) { - return; - } - - return promise.then(() => { - this.trigger({type: 'autoplay-success', autoplay: type}); - }).catch(() => { - this.trigger({type: 'autoplay-failure', autoplay: type}); - }); - } - - /** - * Update the internal source caches so that we return the correct source from - * `src()`, `currentSource()`, and `currentSources()`. - * - * > Note: `currentSources` will not be updated if the source that is passed in exists - * in the current `currentSources` cache. - * - * - * @param {Tech~SourceObject} srcObj - * A string or object source to update our caches to. - */ - updateSourceCaches_(srcObj = '') { - - let src = srcObj; - let type = ''; - - if (typeof src !== 'string') { - src = srcObj.src; - type = srcObj.type; - } - - // make sure all the caches are set to default values - // to prevent null checking - this.cache_.source = this.cache_.source || {}; - this.cache_.sources = this.cache_.sources || []; - - // try to get the type of the src that was passed in - if (src && !type) { - type = findMimetype(this, src); - } - - // update `currentSource` cache always - this.cache_.source = merge({}, srcObj, {src, type}); - - const matchingSources = this.cache_.sources.filter((s) => s.src && s.src === src); - const sourceElSources = []; - const sourceEls = this.$$('source'); - const matchingSourceEls = []; - - for (let i = 0; i < sourceEls.length; i++) { - const sourceObj = Dom.getAttributes(sourceEls[i]); - - sourceElSources.push(sourceObj); - - if (sourceObj.src && sourceObj.src === src) { - matchingSourceEls.push(sourceObj.src); - } - } - - // if we have matching source els but not matching sources - // the current source cache is not up to date - if (matchingSourceEls.length && !matchingSources.length) { - this.cache_.sources = sourceElSources; - // if we don't have matching source or source els set the - // sources cache to the `currentSource` cache - } else if (!matchingSources.length) { - this.cache_.sources = [this.cache_.source]; - } - - // update the tech `src` cache - this.cache_.src = src; - } - - /** - * *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech} - * causing the media element to reload. - * - * It will fire for the initial source and each subsequent source. - * This event is a custom event from Video.js and is triggered by the {@link Tech}. - * - * The event object for this event contains a `src` property that will contain the source - * that was available when the event was triggered. This is generally only necessary if Video.js - * is switching techs while the source was being changed. - * - * It is also fired when `load` is called on the player (or media element) - * because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`} - * says that the resource selection algorithm needs to be aborted and restarted. - * In this case, it is very likely that the `src` property will be set to the - * empty string `""` to indicate we do not know what the source will be but - * that it is changing. - * - * *This event is currently still experimental and may change in minor releases.* - * __To use this, pass `enableSourceset` option to the player.__ - * - * @event Player#sourceset - * @type {Event} - * @prop {string} src - * The source url available when the `sourceset` was triggered. - * It will be an empty string if we cannot know what the source is - * but know that the source will change. - */ - /** - * Retrigger the `sourceset` event that was triggered by the {@link Tech}. - * - * @fires Player#sourceset - * @listens Tech#sourceset - * @private - */ - handleTechSourceset_(event) { - // only update the source cache when the source - // was not updated using the player api - if (!this.changingSrc_) { - let updateSourceCaches = (src) => this.updateSourceCaches_(src); - const playerSrc = this.currentSource().src; - const eventSrc = event.src; - - // if we have a playerSrc that is not a blob, and a tech src that is a blob - if (playerSrc && !(/^blob:/).test(playerSrc) && (/^blob:/).test(eventSrc)) { - - // if both the tech source and the player source were updated we assume - // something like @videojs/http-streaming did the sourceset and skip updating the source cache. - if (!this.lastSource_ || (this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc)) { - updateSourceCaches = () => {}; - } - } - - // update the source to the initial source right away - // in some cases this will be empty string - updateSourceCaches(eventSrc); - - // if the `sourceset` `src` was an empty string - // wait for a `loadstart` to update the cache to `currentSrc`. - // If a sourceset happens before a `loadstart`, we reset the state - if (!event.src) { - this.tech_.any(['sourceset', 'loadstart'], (e) => { - // if a sourceset happens before a `loadstart` there - // is nothing to do as this `handleTechSourceset_` - // will be called again and this will be handled there. - if (e.type === 'sourceset') { - return; - } - - const techSrc = this.techGet_('currentSrc'); - - this.lastSource_.tech = techSrc; - this.updateSourceCaches_(techSrc); - }); - } - } - this.lastSource_ = {player: this.currentSource().src, tech: event.src}; - - this.trigger({ - src: event.src, - type: 'sourceset' - }); - } - - /** - * Add/remove the vjs-has-started class - * - * - * @param {boolean} request - * - true: adds the class - * - false: remove the class - * - * @return {boolean} - * the boolean value of hasStarted_ - */ - hasStarted(request) { - if (request === undefined) { - // act as getter, if we have no request to change - return this.hasStarted_; - } - - if (request === this.hasStarted_) { - return; - } - - this.hasStarted_ = request; - - if (this.hasStarted_) { - this.addClass('vjs-has-started'); - } else { - this.removeClass('vjs-has-started'); - } - } - - /** - * Fired whenever the media begins or resumes playback - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play} - * @fires Player#play - * @listens Tech#play - * @private - */ - handleTechPlay_() { - this.removeClass('vjs-ended', 'vjs-paused'); - this.addClass('vjs-playing'); - - // hide the poster when the user hits play - this.hasStarted(true); - /** - * Triggered whenever an {@link Tech#play} event happens. Indicates that - * playback has started or resumed. - * - * @event Player#play - * @type {Event} - */ - this.trigger('play'); - } - - /** - * Retrigger the `ratechange` event that was triggered by the {@link Tech}. - * - * If there were any events queued while the playback rate was zero, fire - * those events now. - * - * @private - * @method Player#handleTechRateChange_ - * @fires Player#ratechange - * @listens Tech#ratechange - */ - handleTechRateChange_() { - if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) { - this.queuedCallbacks_.forEach((queued) => queued.callback(queued.event)); - this.queuedCallbacks_ = []; - } - this.cache_.lastPlaybackRate = this.tech_.playbackRate(); - /** - * Fires when the playing speed of the audio/video is changed - * - * @event Player#ratechange - * @type {event} - */ - this.trigger('ratechange'); - } - - /** - * Retrigger the `waiting` event that was triggered by the {@link Tech}. - * - * @fires Player#waiting - * @listens Tech#waiting - * @private - */ - handleTechWaiting_() { - this.addClass('vjs-waiting'); - /** - * A readyState change on the DOM element has caused playback to stop. - * - * @event Player#waiting - * @type {Event} - */ - this.trigger('waiting'); - - // Browsers may emit a timeupdate event after a waiting event. In order to prevent - // premature removal of the waiting class, wait for the time to change. - const timeWhenWaiting = this.currentTime(); - const timeUpdateListener = () => { - if (timeWhenWaiting !== this.currentTime()) { - this.removeClass('vjs-waiting'); - this.off('timeupdate', timeUpdateListener); - } - }; - - this.on('timeupdate', timeUpdateListener); - } - - /** - * Retrigger the `canplay` event that was triggered by the {@link Tech}. - * > Note: This is not consistent between browsers. See #1351 - * - * @fires Player#canplay - * @listens Tech#canplay - * @private - */ - handleTechCanPlay_() { - this.removeClass('vjs-waiting'); - /** - * The media has a readyState of HAVE_FUTURE_DATA or greater. - * - * @event Player#canplay - * @type {Event} - */ - this.trigger('canplay'); - } - - /** - * Retrigger the `canplaythrough` event that was triggered by the {@link Tech}. - * - * @fires Player#canplaythrough - * @listens Tech#canplaythrough - * @private - */ - handleTechCanPlayThrough_() { - this.removeClass('vjs-waiting'); - /** - * The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the - * entire media file can be played without buffering. - * - * @event Player#canplaythrough - * @type {Event} - */ - this.trigger('canplaythrough'); - } - - /** - * Retrigger the `playing` event that was triggered by the {@link Tech}. - * - * @fires Player#playing - * @listens Tech#playing - * @private - */ - handleTechPlaying_() { - this.removeClass('vjs-waiting'); - /** - * The media is no longer blocked from playback, and has started playing. - * - * @event Player#playing - * @type {Event} - */ - this.trigger('playing'); - } - - /** - * Retrigger the `seeking` event that was triggered by the {@link Tech}. - * - * @fires Player#seeking - * @listens Tech#seeking - * @private - */ - handleTechSeeking_() { - this.addClass('vjs-seeking'); - /** - * Fired whenever the player is jumping to a new time - * - * @event Player#seeking - * @type {Event} - */ - this.trigger('seeking'); - } - - /** - * Retrigger the `seeked` event that was triggered by the {@link Tech}. - * - * @fires Player#seeked - * @listens Tech#seeked - * @private - */ - handleTechSeeked_() { - this.removeClass('vjs-seeking', 'vjs-ended'); - /** - * Fired when the player has finished jumping to a new time - * - * @event Player#seeked - * @type {Event} - */ - this.trigger('seeked'); - } - - /** - * Retrigger the `pause` event that was triggered by the {@link Tech}. - * - * @fires Player#pause - * @listens Tech#pause - * @private - */ - handleTechPause_() { - this.removeClass('vjs-playing'); - this.addClass('vjs-paused'); - /** - * Fired whenever the media has been paused - * - * @event Player#pause - * @type {Event} - */ - this.trigger('pause'); - } - - /** - * Retrigger the `ended` event that was triggered by the {@link Tech}. - * - * @fires Player#ended - * @listens Tech#ended - * @private - */ - handleTechEnded_() { - this.addClass('vjs-ended'); - this.removeClass('vjs-waiting'); - if (this.options_.loop) { - this.currentTime(0); - this.play(); - } else if (!this.paused()) { - this.pause(); - } - - /** - * Fired when the end of the media resource is reached (currentTime == duration) - * - * @event Player#ended - * @type {Event} - */ - this.trigger('ended'); - } - - /** - * Fired when the duration of the media resource is first known or changed - * - * @listens Tech#durationchange - * @private - */ - handleTechDurationChange_() { - this.duration(this.techGet_('duration')); - } - - /** - * Handle a click on the media element to play/pause - * - * @param {Event} event - * the event that caused this function to trigger - * - * @listens Tech#click - * @private - */ - handleTechClick_(event) { - // When controls are disabled a click should not toggle playback because - // the click is considered a control - if (!this.controls_) { - return; - } - - if ( - this.options_ === undefined || - this.options_.userActions === undefined || - this.options_.userActions.click === undefined || - this.options_.userActions.click !== false - ) { - - if ( - this.options_ !== undefined && - this.options_.userActions !== undefined && - typeof this.options_.userActions.click === 'function' - ) { - - this.options_.userActions.click.call(this, event); - - } else if (this.paused()) { - silencePromise(this.play()); - } else { - this.pause(); - } - } - } - - /** - * Handle a double-click on the media element to enter/exit fullscreen, - * or exit documentPictureInPicture mode - * - * @param {Event} event - * the event that caused this function to trigger - * - * @listens Tech#dblclick - * @private - */ - handleTechDoubleClick_(event) { - if (!this.controls_) { - return; - } - - // we do not want to toggle fullscreen state - // when double-clicking inside a control bar or a modal - const inAllowedEls = Array.prototype.some.call( - this.$$('.vjs-control-bar, .vjs-modal-dialog'), - el => el.contains(event.target) - ); - - if (!inAllowedEls) { - /* - * options.userActions.doubleClick - * - * If `undefined` or `true`, double-click toggles fullscreen if controls are present - * Set to `false` to disable double-click handling - * Set to a function to substitute an external double-click handler - */ - if ( - this.options_ === undefined || - this.options_.userActions === undefined || - this.options_.userActions.doubleClick === undefined || - this.options_.userActions.doubleClick !== false - ) { - - if ( - this.options_ !== undefined && - this.options_.userActions !== undefined && - typeof this.options_.userActions.doubleClick === 'function' - ) { - - this.options_.userActions.doubleClick.call(this, event); - } else if (this.isInPictureInPicture() && !document.pictureInPictureElement) { - // Checking the presence of `window.documentPictureInPicture.window` complicates - // tests, checking `document.pictureInPictureElement` also works. It wouldn't - // be null in regular picture in picture. - // Exit picture in picture mode. This gesture can't trigger pip on the main window. - this.exitPictureInPicture(); - } else if (this.isFullscreen()) { - this.exitFullscreen(); - } else { - this.requestFullscreen(); - } - } - } - } - - /** - * Handle a tap on the media element. It will toggle the user - * activity state, which hides and shows the controls. - * - * @listens Tech#tap - * @private - */ - handleTechTap_() { - this.userActive(!this.userActive()); - } - - /** - * Handle touch to start - * - * @listens Tech#touchstart - * @private - */ - handleTechTouchStart_() { - this.userWasActive = this.userActive(); - } - - /** - * Handle touch to move - * - * @listens Tech#touchmove - * @private - */ - handleTechTouchMove_() { - if (this.userWasActive) { - this.reportUserActivity(); - } - } - - /** - * Handle touch to end - * - * @param {Event} event - * the touchend event that triggered - * this function - * - * @listens Tech#touchend - * @private - */ - handleTechTouchEnd_(event) { - // Stop the mouse events from also happening - if (event.cancelable) { - event.preventDefault(); - } - } - - /** - * @private - */ - toggleFullscreenClass_() { - if (this.isFullscreen()) { - this.addClass('vjs-fullscreen'); - } else { - this.removeClass('vjs-fullscreen'); - } - } - - /** - * when the document fschange event triggers it calls this - */ - documentFullscreenChange_(e) { - const targetPlayer = e.target.player; - - // if another player was fullscreen - // do a null check for targetPlayer because older firefox's would put document as e.target - if (targetPlayer && targetPlayer !== this) { - return; - } - - const el = this.el(); - let isFs = document[this.fsApi_.fullscreenElement] === el; - - if (!isFs && el.matches) { - isFs = el.matches(':' + this.fsApi_.fullscreen); - } - - this.isFullscreen(isFs); - } - - /** - * Handle Tech Fullscreen Change - * - * @param {Event} event - * the fullscreenchange event that triggered this function - * - * @param {Object} data - * the data that was sent with the event - * - * @private - * @listens Tech#fullscreenchange - * @fires Player#fullscreenchange - */ - handleTechFullscreenChange_(event, data) { - if (data) { - if (data.nativeIOSFullscreen) { - this.addClass('vjs-ios-native-fs'); - this.tech_.one('webkitendfullscreen', () => { - this.removeClass('vjs-ios-native-fs'); - }); - } - this.isFullscreen(data.isFullscreen); - } - } - - handleTechFullscreenError_(event, err) { - this.trigger('fullscreenerror', err); - } - - /** - * @private - */ - togglePictureInPictureClass_() { - if (this.isInPictureInPicture()) { - this.addClass('vjs-picture-in-picture'); - } else { - this.removeClass('vjs-picture-in-picture'); - } - } - - /** - * Handle Tech Enter Picture-in-Picture. - * - * @param {Event} event - * the enterpictureinpicture event that triggered this function - * - * @private - * @listens Tech#enterpictureinpicture - */ - handleTechEnterPictureInPicture_(event) { - this.isInPictureInPicture(true); - } - - /** - * Handle Tech Leave Picture-in-Picture. - * - * @param {Event} event - * the leavepictureinpicture event that triggered this function - * - * @private - * @listens Tech#leavepictureinpicture - */ - handleTechLeavePictureInPicture_(event) { - this.isInPictureInPicture(false); - } - - /** - * Fires when an error occurred during the loading of an audio/video. - * - * @private - * @listens Tech#error - */ - handleTechError_() { - const error = this.tech_.error(); - - if (error) { - this.error(error); - } - } - - /** - * Retrigger the `textdata` event that was triggered by the {@link Tech}. - * - * @fires Player#textdata - * @listens Tech#textdata - * @private - */ - handleTechTextData_() { - let data = null; - - if (arguments.length > 1) { - data = arguments[1]; - } - - /** - * Fires when we get a textdata event from tech - * - * @event Player#textdata - * @type {Event} - */ - this.trigger('textdata', data); - } - - /** - * Get object for cached values. - * - * @return {Object} - * get the current object cache - */ - getCache() { - return this.cache_; - } - - /** - * Resets the internal cache object. - * - * Using this function outside the player constructor or reset method may - * have unintended side-effects. - * - * @private - */ - resetCache_() { - this.cache_ = { - - // Right now, the currentTime is not _really_ cached because it is always - // retrieved from the tech (see: currentTime). However, for completeness, - // we set it to zero here to ensure that if we do start actually caching - // it, we reset it along with everything else. - currentTime: 0, - initTime: 0, - inactivityTimeout: this.options_.inactivityTimeout, - duration: NaN, - lastVolume: 1, - lastPlaybackRate: this.defaultPlaybackRate(), - media: null, - src: '', - source: {}, - sources: [], - playbackRates: [], - volume: 1 - }; - } - - /** - * Pass values to the playback tech - * - * @param {string} [method] - * the method to call - * - * @param {Object} [arg] - * the argument to pass - * - * @private - */ - techCall_(method, arg) { - // If it's not ready yet, call method when it is - - this.ready(function() { - if (method in middleware.allowedSetters) { - return middleware.set(this.middleware_, this.tech_, method, arg); - - } else if (method in middleware.allowedMediators) { - return middleware.mediate(this.middleware_, this.tech_, method, arg); - } - - try { - if (this.tech_) { - this.tech_[method](arg); - } - } catch (e) { - log(e); - throw e; - } - }, true); - } - - /** - * Mediate attempt to call playback tech method - * and return the value of the method called. - * - * @param {string} method - * Tech method - * - * @return {*} - * Value returned by the tech method called, undefined if tech - * is not ready or tech method is not present - * - * @private - */ - techGet_(method) { - if (!this.tech_ || !this.tech_.isReady_) { - return; - } - - if (method in middleware.allowedGetters) { - return middleware.get(this.middleware_, this.tech_, method); - - } else if (method in middleware.allowedMediators) { - return middleware.mediate(this.middleware_, this.tech_, method); - } - - // Log error when playback tech object is present but method - // is undefined or unavailable - try { - return this.tech_[method](); - } catch (e) { - - // When building additional tech libs, an expected method may not be defined yet - if (this.tech_[method] === undefined) { - log(`Video.js: ${method} method not defined for ${this.techName_} playback technology.`, e); - throw e; - } - - // When a method isn't available on the object it throws a TypeError - if (e.name === 'TypeError') { - log(`Video.js: ${method} unavailable on ${this.techName_} playback technology element.`, e); - this.tech_.isReady_ = false; - throw e; - } - - // If error unknown, just log and throw - log(e); - throw e; - } - } - - /** - * Attempt to begin playback at the first opportunity. - * - * @return {Promise|undefined} - * Returns a promise if the browser supports Promises (or one - * was passed in as an option). This promise will be resolved on - * the return value of play. If this is undefined it will fulfill the - * promise chain otherwise the promise chain will be fulfilled when - * the promise from play is fulfilled. - */ - play() { - return new Promise((resolve) => { - this.play_(resolve); - }); - } - - /** - * The actual logic for play, takes a callback that will be resolved on the - * return value of play. This allows us to resolve to the play promise if there - * is one on modern browsers. - * - * @private - * @param {Function} [callback] - * The callback that should be called when the techs play is actually called - */ - play_(callback = silencePromise) { - this.playCallbacks_.push(callback); - - const isSrcReady = Boolean(!this.changingSrc_ && (this.src() || this.currentSrc())); - const isSafariOrIOS = Boolean(browser.IS_ANY_SAFARI || browser.IS_IOS); - - // treat calls to play_ somewhat like the `one` event function - if (this.waitToPlay_) { - this.off(['ready', 'loadstart'], this.waitToPlay_); - this.waitToPlay_ = null; - } - - // if the player/tech is not ready or the src itself is not ready - // queue up a call to play on `ready` or `loadstart` - if (!this.isReady_ || !isSrcReady) { - this.waitToPlay_ = (e) => { - this.play_(); - }; - this.one(['ready', 'loadstart'], this.waitToPlay_); - - // if we are in Safari, there is a high chance that loadstart will trigger after the gesture timeperiod - // in that case, we need to prime the video element by calling load so it'll be ready in time - if (!isSrcReady && isSafariOrIOS) { - this.load(); - } - return; - } - - // If the player/tech is ready and we have a source, we can attempt playback. - const val = this.techGet_('play'); - - // For native playback, reset the progress bar if we get a play call from a replay. - const isNativeReplay = isSafariOrIOS && this.hasClass('vjs-ended'); - - if (isNativeReplay) { - this.resetProgressBar_(); - } - // play was terminated if the returned value is null - if (val === null) { - this.runPlayTerminatedQueue_(); - } else { - this.runPlayCallbacks_(val); - } - } - - /** - * These functions will be run when if play is terminated. If play - * runPlayCallbacks_ is run these function will not be run. This allows us - * to differentiate between a terminated play and an actual call to play. - */ - runPlayTerminatedQueue_() { - const queue = this.playTerminatedQueue_.slice(0); - - this.playTerminatedQueue_ = []; - - queue.forEach(function(q) { - q(); - }); - } - - /** - * When a callback to play is delayed we have to run these - * callbacks when play is actually called on the tech. This function - * runs the callbacks that were delayed and accepts the return value - * from the tech. - * - * @param {undefined|Promise} val - * The return value from the tech. - */ - runPlayCallbacks_(val) { - const callbacks = this.playCallbacks_.slice(0); - - this.playCallbacks_ = []; - // clear play terminatedQueue since we finished a real play - this.playTerminatedQueue_ = []; - - callbacks.forEach(function(cb) { - cb(val); - }); - } - - /** - * Pause the video playback - */ - pause() { - this.techCall_('pause'); - } - - /** - * Check if the player is paused or has yet to play - * - * @return {boolean} - * - false: if the media is currently playing - * - true: if media is not currently playing - */ - paused() { - // The initial state of paused should be true (in Safari it's actually false) - return (this.techGet_('paused') === false) ? false : true; - } - - /** - * Get a TimeRange object representing the current ranges of time that the user - * has played. - * - * @return {TimeRange} - * A time range object that represents all the increments of time that have - * been played. - */ - played() { - return this.techGet_('played') || createTimeRange(0, 0); - } - - /** - * Sets or returns whether or not the user is "scrubbing". Scrubbing is - * when the user has clicked the progress bar handle and is - * dragging it along the progress bar. - * - * @param {boolean} [isScrubbing] - * whether the user is or is not scrubbing - * - * @return {boolean|undefined} - * - The value of scrubbing when getting - * - Nothing when setting - */ - scrubbing(isScrubbing) { - if (typeof isScrubbing === 'undefined') { - return this.scrubbing_; - } - this.scrubbing_ = !!isScrubbing; - this.techCall_('setScrubbing', this.scrubbing_); - - if (isScrubbing) { - this.addClass('vjs-scrubbing'); - } else { - this.removeClass('vjs-scrubbing'); - } - } - - /** - * Get or set the current time (in seconds) - * - * @param {number|string} [seconds] - * The time to seek to in seconds - * - * @return {number|undefined} - * - the current time in seconds when getting - * - Nothing when setting - */ - currentTime(seconds) { - if (seconds === undefined) { - // cache last currentTime and return. default to 0 seconds - // - // Caching the currentTime is meant to prevent a massive amount of reads on the tech's - // currentTime when scrubbing, but may not provide much performance benefit after all. - // Should be tested. Also something has to read the actual current time or the cache will - // never get updated. - this.cache_.currentTime = (this.techGet_('currentTime') || 0); - return this.cache_.currentTime; - } - - if (seconds < 0) { - seconds = 0; - } - - if (!this.isReady_ || this.changingSrc_ || !this.tech_ || !this.tech_.isReady_) { - this.cache_.initTime = seconds; - this.off('canplay', this.boundApplyInitTime_); - this.one('canplay', this.boundApplyInitTime_); - return; - } - - this.techCall_('setCurrentTime', seconds); - this.cache_.initTime = 0; - - if (isFinite(seconds)) { - this.cache_.currentTime = Number(seconds); - } - } - - /** - * Apply the value of initTime stored in cache as currentTime. - * - * @private - */ - applyInitTime_() { - this.currentTime(this.cache_.initTime); - } - - /** - * Normally gets the length in time of the video in seconds; - * in all but the rarest use cases an argument will NOT be passed to the method - * - * > **NOTE**: The video must have started loading before the duration can be - * known, and depending on preload behaviour may not be known until the video starts - * playing. - * - * @fires Player#durationchange - * - * @param {number} [seconds] - * The duration of the video to set in seconds - * - * @return {number|undefined} - * - The duration of the video in seconds when getting - * - Nothing when setting - */ - duration(seconds) { - if (seconds === undefined) { - // return NaN if the duration is not known - return this.cache_.duration !== undefined ? this.cache_.duration : NaN; - } - - seconds = parseFloat(seconds); - - // Standardize on Infinity for signaling video is live - if (seconds < 0) { - seconds = Infinity; - } - - if (seconds !== this.cache_.duration) { - // Cache the last set value for optimized scrubbing - this.cache_.duration = seconds; - - if (seconds === Infinity) { - this.addClass('vjs-live'); - } else { - this.removeClass('vjs-live'); - } - if (!isNaN(seconds)) { - // Do not fire durationchange unless the duration value is known. - // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm} - - /** - * @event Player#durationchange - * @type {Event} - */ - this.trigger('durationchange'); - } - } - } - - /** - * Calculates how much time is left in the video. Not part - * of the native video API. - * - * @return {number} - * The time remaining in seconds - */ - remainingTime() { - return this.duration() - this.currentTime(); - } - - /** - * A remaining time function that is intended to be used when - * the time is to be displayed directly to the user. - * - * @return {number} - * The rounded time remaining in seconds - */ - remainingTimeDisplay() { - return Math.floor(this.duration()) - Math.floor(this.currentTime()); - } - - // - // Kind of like an array of portions of the video that have been downloaded. - - /** - * Get a TimeRange object with an array of the times of the video - * that have been downloaded. If you just want the percent of the - * video that's been downloaded, use bufferedPercent. - * - * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered} - * - * @return {TimeRange} - * A mock {@link TimeRanges} object (following HTML spec) - */ - buffered() { - let buffered = this.techGet_('buffered'); - - if (!buffered || !buffered.length) { - buffered = createTimeRange(0, 0); - } - - return buffered; - } - - /** - * Get the TimeRanges of the media that are currently available - * for seeking to. - * - * @see [Seekable Spec]{@link https://html.spec.whatwg.org/multipage/media.html#dom-media-seekable} - * - * @return {TimeRange} - * A mock {@link TimeRanges} object (following HTML spec) - */ - seekable() { - let seekable = this.techGet_('seekable'); - - if (!seekable || !seekable.length) { - seekable = createTimeRange(0, 0); - } - - return seekable; - } - - /** - * Returns whether the player is in the "seeking" state. - * - * @return {boolean} True if the player is in the seeking state, false if not. - */ - seeking() { - return this.techGet_('seeking'); - } - - /** - * Returns whether the player is in the "ended" state. - * - * @return {boolean} True if the player is in the ended state, false if not. - */ - ended() { - return this.techGet_('ended'); - } - - /** - * Returns the current state of network activity for the element, from - * the codes in the list below. - * - NETWORK_EMPTY (numeric value 0) - * The element has not yet been initialised. All attributes are in - * their initial states. - * - NETWORK_IDLE (numeric value 1) - * The element's resource selection algorithm is active and has - * selected a resource, but it is not actually using the network at - * this time. - * - NETWORK_LOADING (numeric value 2) - * The user agent is actively trying to download data. - * - NETWORK_NO_SOURCE (numeric value 3) - * The element's resource selection algorithm is active, but it has - * not yet found a resource to use. - * - * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states - * @return {number} the current network activity state - */ - networkState() { - return this.techGet_('networkState'); - } - - /** - * Returns a value that expresses the current state of the element - * with respect to rendering the current playback position, from the - * codes in the list below. - * - HAVE_NOTHING (numeric value 0) - * No information regarding the media resource is available. - * - HAVE_METADATA (numeric value 1) - * Enough of the resource has been obtained that the duration of the - * resource is available. - * - HAVE_CURRENT_DATA (numeric value 2) - * Data for the immediate current playback position is available. - * - HAVE_FUTURE_DATA (numeric value 3) - * Data for the immediate current playback position is available, as - * well as enough data for the user agent to advance the current - * playback position in the direction of playback. - * - HAVE_ENOUGH_DATA (numeric value 4) - * The user agent estimates that enough data is available for - * playback to proceed uninterrupted. - * - * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate - * @return {number} the current playback rendering state - */ - readyState() { - return this.techGet_('readyState'); - } - - /** - * Get the percent (as a decimal) of the video that's been downloaded. - * This method is not a part of the native HTML video API. - * - * @return {number} - * A decimal between 0 and 1 representing the percent - * that is buffered 0 being 0% and 1 being 100% - */ - bufferedPercent() { - return bufferedPercent(this.buffered(), this.duration()); - } - - /** - * Get the ending time of the last buffered time range - * This is used in the progress bar to encapsulate all time ranges. - * - * @return {number} - * The end of the last buffered time range - */ - bufferedEnd() { - const buffered = this.buffered(); - const duration = this.duration(); - let end = buffered.end(buffered.length - 1); - - if (end > duration) { - end = duration; - } - - return end; - } - - /** - * Get or set the current volume of the media - * - * @param {number} [percentAsDecimal] - * The new volume as a decimal percent: - * - 0 is muted/0%/off - * - 1.0 is 100%/full - * - 0.5 is half volume or 50% - * - * @return {number|undefined} - * The current volume as a percent when getting - */ - volume(percentAsDecimal) { - let vol; - - if (percentAsDecimal !== undefined) { - // Force value to between 0 and 1 - vol = Math.max(0, Math.min(1, percentAsDecimal)); - this.cache_.volume = vol; - this.techCall_('setVolume', vol); - - if (vol > 0) { - this.lastVolume_(vol); - } - - return; - } - - // Default to 1 when returning current volume. - vol = parseFloat(this.techGet_('volume')); - return (isNaN(vol)) ? 1 : vol; - } - - /** - * Get the current muted state, or turn mute on or off - * - * @param {boolean} [muted] - * - true to mute - * - false to unmute - * - * @return {boolean|undefined} - * - true if mute is on and getting - * - false if mute is off and getting - * - nothing if setting - */ - muted(muted) { - if (muted !== undefined) { - this.techCall_('setMuted', muted); - return; - } - return this.techGet_('muted') || false; - } - - /** - * Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted - * indicates the state of muted on initial playback. - * - * ```js - * var myPlayer = videojs('some-player-id'); - * - * myPlayer.src("http://www.example.com/path/to/video.mp4"); - * - * // get, should be false - * console.log(myPlayer.defaultMuted()); - * // set to true - * myPlayer.defaultMuted(true); - * // get should be true - * console.log(myPlayer.defaultMuted()); - * ``` - * - * @param {boolean} [defaultMuted] - * - true to mute - * - false to unmute - * - * @return {boolean|undefined} - * - true if defaultMuted is on and getting - * - false if defaultMuted is off and getting - * - Nothing when setting - */ - defaultMuted(defaultMuted) { - if (defaultMuted !== undefined) { - this.techCall_('setDefaultMuted', defaultMuted); - } - return this.techGet_('defaultMuted') || false; - } - - /** - * Get the last volume, or set it - * - * @param {number} [percentAsDecimal] - * The new last volume as a decimal percent: - * - 0 is muted/0%/off - * - 1.0 is 100%/full - * - 0.5 is half volume or 50% - * - * @return {number|undefined} - * - The current value of lastVolume as a percent when getting - * - Nothing when setting - * - * @private - */ - lastVolume_(percentAsDecimal) { - if (percentAsDecimal !== undefined && percentAsDecimal !== 0) { - this.cache_.lastVolume = percentAsDecimal; - return; - } - return this.cache_.lastVolume; - } - - /** - * Check if current tech can support native fullscreen - * (e.g. with built in controls like iOS) - * - * @return {boolean} - * if native fullscreen is supported - */ - supportsFullScreen() { - return this.techGet_('supportsFullScreen') || false; - } - - /** - * Check if the player is in fullscreen mode or tell the player that it - * is or is not in fullscreen mode. - * - * > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official - * property and instead document.fullscreenElement is used. But isFullscreen is - * still a valuable property for internal player workings. - * - * @param {boolean} [isFS] - * Set the players current fullscreen state - * - * @return {boolean|undefined} - * - true if fullscreen is on and getting - * - false if fullscreen is off and getting - * - Nothing when setting - */ - isFullscreen(isFS) { - if (isFS !== undefined) { - const oldValue = this.isFullscreen_; - - this.isFullscreen_ = Boolean(isFS); - - // if we changed fullscreen state and we're in prefixed mode, trigger fullscreenchange - // this is the only place where we trigger fullscreenchange events for older browsers - // fullWindow mode is treated as a prefixed event and will get a fullscreenchange event as well - if (this.isFullscreen_ !== oldValue && this.fsApi_.prefixed) { - /** - * @event Player#fullscreenchange - * @type {Event} - */ - this.trigger('fullscreenchange'); - } - - this.toggleFullscreenClass_(); - return; - } - return this.isFullscreen_; - } - - /** - * Increase the size of the video to full screen - * In some browsers, full screen is not supported natively, so it enters - * "full window mode", where the video fills the browser window. - * In browsers and devices that support native full screen, sometimes the - * browser's default controls will be shown, and not the Video.js custom skin. - * This includes most mobile devices (iOS, Android) and older versions of - * Safari. - * - * @param {Object} [fullscreenOptions] - * Override the player fullscreen options - * - * @fires Player#fullscreenchange - */ - requestFullscreen(fullscreenOptions) { - if (this.isInPictureInPicture()) { - this.exitPictureInPicture(); - } - - const self = this; - - return new Promise((resolve, reject) => { - function offHandler() { - self.off('fullscreenerror', errorHandler); - self.off('fullscreenchange', changeHandler); - } - function changeHandler() { - offHandler(); - resolve(); - } - function errorHandler(e, err) { - offHandler(); - reject(err); - } - - self.one('fullscreenchange', changeHandler); - self.one('fullscreenerror', errorHandler); - - const promise = self.requestFullscreenHelper_(fullscreenOptions); - - if (promise) { - promise.then(offHandler, offHandler); - promise.then(resolve, reject); - } - }); - } - - requestFullscreenHelper_(fullscreenOptions) { - let fsOptions; - - // Only pass fullscreen options to requestFullscreen in spec-compliant browsers. - // Use defaults or player configured option unless passed directly to this method. - if (!this.fsApi_.prefixed) { - fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {}; - if (fullscreenOptions !== undefined) { - fsOptions = fullscreenOptions; - } - } - - // This method works as follows: - // 1. if a fullscreen api is available, use it - // 1. call requestFullscreen with potential options - // 2. if we got a promise from above, use it to update isFullscreen() - // 2. otherwise, if the tech supports fullscreen, call `enterFullScreen` on it. - // This is particularly used for iPhone, older iPads, and non-safari browser on iOS. - // 3. otherwise, use "fullWindow" mode - if (this.fsApi_.requestFullscreen) { - const promise = this.el_[this.fsApi_.requestFullscreen](fsOptions); - - // Even on browsers with promise support this may not return a promise - if (promise) { - promise.then(() => this.isFullscreen(true), () => this.isFullscreen(false)); - } - - return promise; - } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) { - // we can't take the video.js controls fullscreen but we can go fullscreen - // with native controls - this.techCall_('enterFullScreen'); - } else { - // fullscreen isn't supported so we'll just stretch the video element to - // fill the viewport - this.enterFullWindow(); - } - } - - /** - * Return the video to its normal size after having been in full screen mode - * - * @fires Player#fullscreenchange - */ - exitFullscreen() { - const self = this; - - return new Promise((resolve, reject) => { - function offHandler() { - self.off('fullscreenerror', errorHandler); - self.off('fullscreenchange', changeHandler); - } - function changeHandler() { - offHandler(); - resolve(); - } - function errorHandler(e, err) { - offHandler(); - reject(err); - } - - self.one('fullscreenchange', changeHandler); - self.one('fullscreenerror', errorHandler); - - const promise = self.exitFullscreenHelper_(); - - if (promise) { - promise.then(offHandler, offHandler); - // map the promise to our resolve/reject methods - promise.then(resolve, reject); - } - }); - } - - exitFullscreenHelper_() { - if (this.fsApi_.requestFullscreen) { - const promise = document[this.fsApi_.exitFullscreen](); - - // Even on browsers with promise support this may not return a promise - if (promise) { - // we're splitting the promise here, so, we want to catch the - // potential error so that this chain doesn't have unhandled errors - silencePromise(promise.then(() => this.isFullscreen(false))); - } - - return promise; - } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) { - this.techCall_('exitFullScreen'); - } else { - this.exitFullWindow(); - } - } - - /** - * When fullscreen isn't supported we can stretch the - * video container to as wide as the browser will let us. - * - * @fires Player#enterFullWindow - */ - enterFullWindow() { - this.isFullscreen(true); - this.isFullWindow = true; - - // Storing original doc overflow value to return to when fullscreen is off - this.docOrigOverflow = document.documentElement.style.overflow; - - // Add listener for esc key to exit fullscreen - Events.on(document, 'keydown', this.boundFullWindowOnEscKey_); - - // Hide any scroll bars - document.documentElement.style.overflow = 'hidden'; - - // Apply fullscreen styles - Dom.addClass(document.body, 'vjs-full-window'); - - /** - * @event Player#enterFullWindow - * @type {Event} - */ - this.trigger('enterFullWindow'); - } - - /** - * Check for call to either exit full window or - * full screen on ESC key - * - * @param {string} event - * Event to check for key press - */ - fullWindowOnEscKey(event) { - if (event.key === 'Escape') { - if (this.isFullscreen() === true) { - if (!this.isFullWindow) { - this.exitFullscreen(); - } else { - this.exitFullWindow(); - } - } - } - } - - /** - * Exit full window - * - * @fires Player#exitFullWindow - */ - exitFullWindow() { - this.isFullscreen(false); - this.isFullWindow = false; - Events.off(document, 'keydown', this.boundFullWindowOnEscKey_); - - // Unhide scroll bars. - document.documentElement.style.overflow = this.docOrigOverflow; - - // Remove fullscreen styles - Dom.removeClass(document.body, 'vjs-full-window'); - - // Resize the box, controller, and poster to original sizes - // this.positionAll(); - /** - * @event Player#exitFullWindow - * @type {Event} - */ - this.trigger('exitFullWindow'); - } - - /** - * Get or set disable Picture-in-Picture mode. - * - * @param {boolean} [value] - * - true will disable Picture-in-Picture mode - * - false will enable Picture-in-Picture mode - */ - disablePictureInPicture(value) { - if (value === undefined) { - return this.techGet_('disablePictureInPicture'); - } - this.techCall_('setDisablePictureInPicture', value); - this.options_.disablePictureInPicture = value; - this.trigger('disablepictureinpicturechanged'); - } - - /** - * Check if the player is in Picture-in-Picture mode or tell the player that it - * is or is not in Picture-in-Picture mode. - * - * @param {boolean} [isPiP] - * Set the players current Picture-in-Picture state - * - * @return {boolean|undefined} - * - true if Picture-in-Picture is on and getting - * - false if Picture-in-Picture is off and getting - * - nothing if setting - */ - isInPictureInPicture(isPiP) { - if (isPiP !== undefined) { - this.isInPictureInPicture_ = !!isPiP; - this.togglePictureInPictureClass_(); - return; - } - return !!this.isInPictureInPicture_; - } - - /** - * Create a floating video window always on top of other windows so that users may - * continue consuming media while they interact with other content sites, or - * applications on their device. - * - * This can use document picture-in-picture or element picture in picture - * - * Set `enableDocumentPictureInPicture` to `true` to use docPiP on a supported browser - * Else set `disablePictureInPicture` to `false` to disable elPiP on a supported browser - * - * - * @see [Spec]{@link https://w3c.github.io/picture-in-picture/} - * @see [Spec]{@link https://wicg.github.io/document-picture-in-picture/} - * - * @fires Player#enterpictureinpicture - * - * @return {Promise} - * A promise with a Picture-in-Picture window. - */ - requestPictureInPicture() { - if (this.options_.enableDocumentPictureInPicture && window.documentPictureInPicture) { - const pipContainer = document.createElement(this.el().tagName); - - pipContainer.classList = this.el().classList; - pipContainer.classList.add('vjs-pip-container'); - if (this.posterImage) { - pipContainer.appendChild(this.posterImage.el().cloneNode(true)); - } - if (this.titleBar) { - pipContainer.appendChild(this.titleBar.el().cloneNode(true)); - } - pipContainer.appendChild(Dom.createEl('p', { className: 'vjs-pip-text' }, {}, this.localize('Playing in picture-in-picture'))); - - return window.documentPictureInPicture.requestWindow({ - // The aspect ratio won't be correct, Chrome bug https://crbug.com/1407629 - width: this.videoWidth(), - height: this.videoHeight() - }).then(pipWindow => { - Dom.copyStyleSheetsToWindow(pipWindow); - this.el_.parentNode.insertBefore(pipContainer, this.el_); - - pipWindow.document.body.appendChild(this.el_); - pipWindow.document.body.classList.add('vjs-pip-window'); - - this.player_.isInPictureInPicture(true); - this.player_.trigger({type: 'enterpictureinpicture', pipWindow}); - - // Listen for the PiP closing event to move the video back. - pipWindow.addEventListener('pagehide', (event) => { - const pipVideo = event.target.querySelector('.video-js'); - - pipContainer.parentNode.replaceChild(pipVideo, pipContainer); - this.player_.isInPictureInPicture(false); - this.player_.trigger('leavepictureinpicture'); - }); - - return pipWindow; - }); - } - if ('pictureInPictureEnabled' in document && this.disablePictureInPicture() === false) { - /** - * This event fires when the player enters picture in picture mode - * - * @event Player#enterpictureinpicture - * @type {Event} - */ - return this.techGet_('requestPictureInPicture'); - } - return Promise.reject('No PiP mode is available'); - } - - /** - * Exit Picture-in-Picture mode. - * - * @see [Spec]{@link https://wicg.github.io/picture-in-picture} - * - * @fires Player#leavepictureinpicture - * - * @return {Promise} - * A promise. - */ - exitPictureInPicture() { - if (window.documentPictureInPicture && window.documentPictureInPicture.window) { - // With documentPictureInPicture, Player#leavepictureinpicture is fired in the pagehide handler - window.documentPictureInPicture.window.close(); - return Promise.resolve(); - } - if ('pictureInPictureEnabled' in document) { - - /** - * This event fires when the player leaves picture in picture mode - * - * @event Player#leavepictureinpicture - * @type {Event} - */ - return document.exitPictureInPicture(); - } - } - - /** - * Called when this Player has focus and a key gets pressed down, or when - * any Component of this player receives a key press that it doesn't handle. - * This allows player-wide hotkeys (either as defined below, or optionally - * by an external function). - * - * @param {KeyboardEvent} event - * The `keydown` event that caused this function to be called. - * - * @listens keydown - */ - handleKeyDown(event) { - const {userActions} = this.options_; - - // Bail out if hotkeys are not configured. - if (!userActions || !userActions.hotkeys) { - return; - } - - // Function that determines whether or not to exclude an element from - // hotkeys handling. - const excludeElement = (el) => { - const tagName = el.tagName.toLowerCase(); - - // The first and easiest test is for `contenteditable` elements. - if (el.isContentEditable) { - return true; - } - - // Inputs matching these types will still trigger hotkey handling as - // they are not text inputs. - const allowedInputTypes = [ - 'button', - 'checkbox', - 'hidden', - 'radio', - 'reset', - 'submit' - ]; - - if (tagName === 'input') { - return allowedInputTypes.indexOf(el.type) === -1; - } - - // The final test is by tag name. These tags will be excluded entirely. - const excludedTags = ['textarea']; - - return excludedTags.indexOf(tagName) !== -1; - }; - - // Bail out if the user is focused on an interactive form element. - if (excludeElement(this.el_.ownerDocument.activeElement)) { - return; - } - - if (typeof userActions.hotkeys === 'function') { - userActions.hotkeys.call(this, event); - } else { - this.handleHotkeys(event); - } - } - - /** - * Called when this Player receives a hotkey keydown event. - * Supported player-wide hotkeys are: - * - * f - toggle fullscreen - * m - toggle mute - * k or Space - toggle play/pause - * - * @param {Event} event - * The `keydown` event that caused this function to be called. - */ - handleHotkeys(event) { - const hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {}; - - // set fullscreenKey, muteKey, playPauseKey from `hotkeys`, use defaults if not set - const { - fullscreenKey = keydownEvent => (event.key.toLowerCase() === 'f'), - muteKey = keydownEvent => (event.key.toLowerCase() === 'm'), - playPauseKey = keydownEvent => (event.key.toLowerCase() === 'k' || event.key.toLowerCase() === ' ') - } = hotkeys; - - if (fullscreenKey.call(this, event)) { - event.preventDefault(); - event.stopPropagation(); - - const FSToggle = Component.getComponent('FullscreenToggle'); - - if (document[this.fsApi_.fullscreenEnabled] !== false) { - FSToggle.prototype.handleClick.call(this, event); - } - - } else if (muteKey.call(this, event)) { - event.preventDefault(); - event.stopPropagation(); - - const MuteToggle = Component.getComponent('MuteToggle'); - - MuteToggle.prototype.handleClick.call(this, event); - - } else if (playPauseKey.call(this, event)) { - event.preventDefault(); - event.stopPropagation(); - - const PlayToggle = Component.getComponent('PlayToggle'); - - PlayToggle.prototype.handleClick.call(this, event); - } - } - - /** - * Check whether the player can play a given mimetype - * - * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype - * - * @param {string} type - * The mimetype to check - * - * @return {string} - * 'probably', 'maybe', or '' (empty string) - */ - canPlayType(type) { - let can; - - // Loop through each playback technology in the options order - for (let i = 0, j = this.options_.techOrder; i < j.length; i++) { - const techName = j[i]; - let tech = Tech.getTech(techName); - - // Support old behavior of techs being registered as components. - // Remove once that deprecated behavior is removed. - if (!tech) { - tech = Component.getComponent(techName); - } - - // Check if the current tech is defined before continuing - if (!tech) { - log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`); - continue; - } - - // Check if the browser supports this technology - if (tech.isSupported()) { - can = tech.canPlayType(type); - - if (can) { - return can; - } - } - } - - return ''; - } - - /** - * Select source based on tech-order or source-order - * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise, - * defaults to tech-order selection - * - * @param {Array} sources - * The sources for a media asset - * - * @return {Object|boolean} - * Object of source and tech order or false - */ - selectSource(sources) { - // Get only the techs specified in `techOrder` that exist and are supported by the - // current platform - const techs = - this.options_.techOrder - .map((techName) => { - return [techName, Tech.getTech(techName)]; - }) - .filter(([techName, tech]) => { - // Check if the current tech is defined before continuing - if (tech) { - // Check if the browser supports this technology - return tech.isSupported(); - } - - log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`); - return false; - }); - - // Iterate over each `innerArray` element once per `outerArray` element and execute - // `tester` with both. If `tester` returns a non-falsy value, exit early and return - // that value. - const findFirstPassingTechSourcePair = function(outerArray, innerArray, tester) { - let found; - - outerArray.some((outerChoice) => { - return innerArray.some((innerChoice) => { - found = tester(outerChoice, innerChoice); - - if (found) { - return true; - } - }); - }); - - return found; - }; - - let foundSourceAndTech; - const flip = (fn) => (a, b) => fn(b, a); - const finder = ([techName, tech], source) => { - if (tech.canPlaySource(source, this.options_[techName.toLowerCase()])) { - return {source, tech: techName}; - } - }; - - // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources - // to select from them based on their priority. - if (this.options_.sourceOrder) { - // Source-first ordering - foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder)); - } else { - // Tech-first ordering - foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder); - } - - return foundSourceAndTech || false; - } - - /** - * Executes source setting and getting logic - * - * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source] - * A SourceObject, an array of SourceObjects, or a string referencing - * a URL to a media source. It is _highly recommended_ that an object - * or array of objects is used here, so that source selection - * algorithms can take the `type` into account. - * - * If not provided, this method acts as a getter. - * @param {boolean} [isRetry] - * Indicates whether this is being called internally as a result of a retry - * - * @return {string|undefined} - * If the `source` argument is missing, returns the current source - * URL. Otherwise, returns nothing/undefined. - */ - handleSrc_(source, isRetry) { - // getter usage - if (typeof source === 'undefined') { - return this.cache_.src || ''; - } - - // Reset retry behavior for new source - if (this.resetRetryOnError_) { - this.resetRetryOnError_(); - } - - // filter out invalid sources and turn our source into - // an array of source objects - const sources = filterSource(source); - - // if a source was passed in then it is invalid because - // it was filtered to a zero length Array. So we have to - // show an error - if (!sources.length) { - this.setTimeout(function() { - this.error({ code: 4, message: this.options_.notSupportedMessage }); - }, 0); - return; - } - - // initial sources - this.changingSrc_ = true; - - // Only update the cached source list if we are not retrying a new source after error, - // since in that case we want to include the failed source(s) in the cache - if (!isRetry) { - this.cache_.sources = sources; - } - - this.updateSourceCaches_(sources[0]); - - // middlewareSource is the source after it has been changed by middleware - middleware.setSource(this, sources[0], (middlewareSource, mws) => { - this.middleware_ = mws; - - // since sourceSet is async we have to update the cache again after we select a source since - // the source that is selected could be out of order from the cache update above this callback. - if (!isRetry) { - this.cache_.sources = sources; - } - - this.updateSourceCaches_(middlewareSource); - - const err = this.src_(middlewareSource); - - if (err) { - if (sources.length > 1) { - return this.handleSrc_(sources.slice(1)); - } - - this.changingSrc_ = false; - - // We need to wrap this in a timeout to give folks a chance to add error event handlers - this.setTimeout(function() { - this.error({ code: 4, message: this.options_.notSupportedMessage }); - }, 0); - - // we could not find an appropriate tech, but let's still notify the delegate that this is it - // this needs a better comment about why this is needed - this.triggerReady(); - - return; - } - - middleware.setTech(mws, this.tech_); - }); - - // Try another available source if this one fails before playback. - if (sources.length > 1) { - const retry = () => { - // Remove the error modal - this.error(null); - this.handleSrc_(sources.slice(1), true); - }; - - const stopListeningForErrors = () => { - this.off('error', retry); - }; - - this.one('error', retry); - this.one('playing', stopListeningForErrors); - - this.resetRetryOnError_ = () => { - this.off('error', retry); - this.off('playing', stopListeningForErrors); - }; - } - } - - /** - * Get or set the video source. - * - * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source] - * A SourceObject, an array of SourceObjects, or a string referencing - * a URL to a media source. It is _highly recommended_ that an object - * or array of objects is used here, so that source selection - * algorithms can take the `type` into account. - * - * If not provided, this method acts as a getter. - * - * @return {string|undefined} - * If the `source` argument is missing, returns the current source - * URL. Otherwise, returns nothing/undefined. - */ - src(source) { - return this.handleSrc_(source, false); - } - - /** - * Set the source object on the tech, returns a boolean that indicates whether - * there is a tech that can play the source or not - * - * @param {Tech~SourceObject} source - * The source object to set on the Tech - * - * @return {boolean} - * - True if there is no Tech to playback this source - * - False otherwise - * - * @private - */ - src_(source) { - const sourceTech = this.selectSource([source]); - - if (!sourceTech) { - return true; - } - - if (!titleCaseEquals(sourceTech.tech, this.techName_)) { - this.changingSrc_ = true; - // load this technology with the chosen source - this.loadTech_(sourceTech.tech, sourceTech.source); - this.tech_.ready(() => { - this.changingSrc_ = false; - }); - return false; - } - - // wait until the tech is ready to set the source - // and set it synchronously if possible (#2326) - this.ready(function() { - - // The setSource tech method was added with source handlers - // so older techs won't support it - // We need to check the direct prototype for the case where subclasses - // of the tech do not support source handlers - if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) { - this.techCall_('setSource', source); - } else { - this.techCall_('src', source.src); - } - - this.changingSrc_ = false; - }, true); - - return false; - } - - /** - * Add a <source> element to the <video> element. - * - * @param {string} srcUrl - * The URL of the video source. - * - * @param {string} [mimeType] - * The MIME type of the video source. Optional but recommended. - * - * @return {boolean} - * Returns true if the source element was successfully added, false otherwise. - */ - addSourceElement(srcUrl, mimeType) { - if (!this.tech_) { - return false; - } - - return this.tech_.addSourceElement(srcUrl, mimeType); - } - - /** - * Remove a <source> element from the <video> element by its URL. - * - * @param {string} srcUrl - * The URL of the source to remove. - * - * @return {boolean} - * Returns true if the source element was successfully removed, false otherwise. - */ - removeSourceElement(srcUrl) { - if (!this.tech_) { - return false; - } - - return this.tech_.removeSourceElement(srcUrl); - } - - /** - * Begin loading the src data. - */ - load() { - // Workaround to use the load method with the VHS. - // Does not cover the case when the load method is called directly from the mediaElement. - if (this.tech_ && this.tech_.vhs) { - this.src(this.currentSource()); - - return; - } - - this.techCall_('load'); - } - - /** - * Reset the player. Loads the first tech in the techOrder, - * removes all the text tracks in the existing `tech`, - * and calls `reset` on the `tech`. - */ - reset() { - if (this.paused()) { - this.doReset_(); - } else { - const playPromise = this.play(); - - silencePromise(playPromise.then(() => this.doReset_())); - } - } - - doReset_() { - if (this.tech_) { - this.tech_.clearTracks('text'); - } - - this.removeClass('vjs-playing'); - this.addClass('vjs-paused'); - - this.resetCache_(); - this.poster(''); - this.loadTech_(this.options_.techOrder[0], null); - this.techCall_('reset'); - this.resetControlBarUI_(); - - this.error(null); - - if (this.titleBar) { - this.titleBar.update({ - title: undefined, - description: undefined - }); - } - - if (isEvented(this)) { - this.trigger('playerreset'); - } - } - - /** - * Reset Control Bar's UI by calling sub-methods that reset - * all of Control Bar's components - */ - resetControlBarUI_() { - this.resetProgressBar_(); - this.resetPlaybackRate_(); - this.resetVolumeBar_(); - } - - /** - * Reset tech's progress so progress bar is reset in the UI - */ - resetProgressBar_() { - this.currentTime(0); - - const { - currentTimeDisplay, - durationDisplay, - progressControl, - remainingTimeDisplay - } = this.controlBar || {}; - const { seekBar } = progressControl || {}; - - if (currentTimeDisplay) { - currentTimeDisplay.updateContent(); - } - - if (durationDisplay) { - durationDisplay.updateContent(); - } - - if (remainingTimeDisplay) { - remainingTimeDisplay.updateContent(); - } - - if (seekBar) { - seekBar.update(); - - if (seekBar.loadProgressBar) { - seekBar.loadProgressBar.update(); - } - } - } - - /** - * Reset Playback ratio - */ - resetPlaybackRate_() { - this.playbackRate(this.defaultPlaybackRate()); - this.handleTechRateChange_(); - } - - /** - * Reset Volume bar - */ - resetVolumeBar_() { - this.volume(1.0); - this.trigger('volumechange'); - } - - /** - * Returns all of the current source objects. - * - * @return {Tech~SourceObject[]} - * The current source objects - */ - currentSources() { - const source = this.currentSource(); - const sources = []; - - // assume `{}` or `{ src }` - if (Object.keys(source).length !== 0) { - sources.push(source); - } - - return this.cache_.sources || sources; - } - - /** - * Returns the current source object. - * - * @return {Tech~SourceObject} - * The current source object - */ - currentSource() { - return this.cache_.source || {}; - } - - /** - * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4 - * Can be used in conjunction with `currentType` to assist in rebuilding the current source object. - * - * @return {string} - * The current source - */ - currentSrc() { - return this.currentSource() && this.currentSource().src || ''; - } - - /** - * Get the current source type e.g. video/mp4 - * This can allow you rebuild the current source object so that you could load the same - * source and tech later - * - * @return {string} - * The source MIME type - */ - currentType() { - return this.currentSource() && this.currentSource().type || ''; - } - - /** - * Get or set the preload attribute - * - * @param {'none'|'auto'|'metadata'} [value] - * Preload mode to pass to tech - * - * @return {string|undefined} - * - The preload attribute value when getting - * - Nothing when setting - */ - preload(value) { - if (value !== undefined) { - this.techCall_('setPreload', value); - this.options_.preload = value; - return; - } - return this.techGet_('preload'); - } - - /** - * Get or set the autoplay option. When this is a boolean it will - * modify the attribute on the tech. When this is a string the attribute on - * the tech will be removed and `Player` will handle autoplay on loadstarts. - * - * @param {boolean|'play'|'muted'|'any'} [value] - * - true: autoplay using the browser behavior - * - false: do not autoplay - * - 'play': call play() on every loadstart - * - 'muted': call muted() then play() on every loadstart - * - 'any': call play() on every loadstart. if that fails call muted() then play(). - * - *: values other than those listed here will be set `autoplay` to true - * - * @return {boolean|string|undefined} - * - The current value of autoplay when getting - * - Nothing when setting - */ - autoplay(value) { - // getter usage - if (value === undefined) { - return this.options_.autoplay || false; - } - - let techAutoplay; - - // if the value is a valid string set it to that, or normalize `true` to 'play', if need be - if (typeof value === 'string' && (/(any|play|muted)/).test(value) || value === true && this.options_.normalizeAutoplay) { - this.options_.autoplay = value; - this.manualAutoplay_(typeof value === 'string' ? value : 'play'); - techAutoplay = false; - - // any falsy value sets autoplay to false in the browser, - // lets do the same - } else if (!value) { - this.options_.autoplay = false; - - // any other value (ie truthy) sets autoplay to true - } else { - this.options_.autoplay = true; - } - - techAutoplay = typeof techAutoplay === 'undefined' ? this.options_.autoplay : techAutoplay; - - // if we don't have a tech then we do not queue up - // a setAutoplay call on tech ready. We do this because the - // autoplay option will be passed in the constructor and we - // do not need to set it twice - if (this.tech_) { - this.techCall_('setAutoplay', techAutoplay); - } - } - - /** - * Set or unset the playsinline attribute. - * Playsinline tells the browser that non-fullscreen playback is preferred. - * - * @param {boolean} [value] - * - true means that we should try to play inline by default - * - false means that we should use the browser's default playback mode, - * which in most cases is inline. iOS Safari is a notable exception - * and plays fullscreen by default. - * - * @return {string|undefined} - * - the current value of playsinline - * - Nothing when setting - * - * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} - */ - playsinline(value) { - if (value !== undefined) { - this.techCall_('setPlaysinline', value); - this.options_.playsinline = value; - } - return this.techGet_('playsinline'); - } - - /** - * Get or set the loop attribute on the video element. - * - * @param {boolean} [value] - * - true means that we should loop the video - * - false means that we should not loop the video - * - * @return {boolean|undefined} - * - The current value of loop when getting - * - Nothing when setting - */ - loop(value) { - if (value !== undefined) { - this.techCall_('setLoop', value); - this.options_.loop = value; - return; - } - return this.techGet_('loop'); - } - - /** - * Get or set the poster image source url - * - * @fires Player#posterchange - * - * @param {string} [src] - * Poster image source URL - * - * @return {string|undefined} - * - The current value of poster when getting - * - Nothing when setting - */ - poster(src) { - if (src === undefined) { - return this.poster_; - } - - // The correct way to remove a poster is to set as an empty string - // other falsey values will throw errors - if (!src) { - src = ''; - } - - if (src === this.poster_) { - return; - } - - // update the internal poster variable - this.poster_ = src; - - // update the tech's poster - this.techCall_('setPoster', src); - - this.isPosterFromTech_ = false; - - // alert components that the poster has been set - /** - * This event fires when the poster image is changed on the player. - * - * @event Player#posterchange - * @type {Event} - */ - this.trigger('posterchange'); - } - - /** - * Some techs (e.g. YouTube) can provide a poster source in an - * asynchronous way. We want the poster component to use this - * poster source so that it covers up the tech's controls. - * (YouTube's play button). However we only want to use this - * source if the player user hasn't set a poster through - * the normal APIs. - * - * @fires Player#posterchange - * @listens Tech#posterchange - * @private - */ - handleTechPosterChange_() { - if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) { - const newPoster = this.tech_.poster() || ''; - - if (newPoster !== this.poster_) { - this.poster_ = newPoster; - this.isPosterFromTech_ = true; - - // Let components know the poster has changed - this.trigger('posterchange'); - } - } - } - - /** - * Get or set whether or not the controls are showing. - * - * @fires Player#controlsenabled - * - * @param {boolean} [bool] - * - true to turn controls on - * - false to turn controls off - * - * @return {boolean|undefined} - * - The current value of controls when getting - * - Nothing when setting - */ - controls(bool) { - if (bool === undefined) { - return !!this.controls_; - } - - bool = !!bool; - - // Don't trigger a change event unless it actually changed - if (this.controls_ === bool) { - return; - } - - this.controls_ = bool; - - if (this.usingNativeControls()) { - this.techCall_('setControls', bool); - } - - if (this.controls_) { - this.removeClass('vjs-controls-disabled'); - this.addClass('vjs-controls-enabled'); - /** - * @event Player#controlsenabled - * @type {Event} - */ - this.trigger('controlsenabled'); - if (!this.usingNativeControls()) { - this.addTechControlsListeners_(); - } - } else { - this.removeClass('vjs-controls-enabled'); - this.addClass('vjs-controls-disabled'); - /** - * @event Player#controlsdisabled - * @type {Event} - */ - this.trigger('controlsdisabled'); - if (!this.usingNativeControls()) { - this.removeTechControlsListeners_(); - } - } - } - - /** - * Toggle native controls on/off. Native controls are the controls built into - * devices (e.g. default iPhone controls) or other techs - * (e.g. Vimeo Controls) - * **This should only be set by the current tech, because only the tech knows - * if it can support native controls** - * - * @fires Player#usingnativecontrols - * @fires Player#usingcustomcontrols - * - * @param {boolean} [bool] - * - true to turn native controls on - * - false to turn native controls off - * - * @return {boolean|undefined} - * - The current value of native controls when getting - * - Nothing when setting - */ - usingNativeControls(bool) { - if (bool === undefined) { - return !!this.usingNativeControls_; - } - - bool = !!bool; - - // Don't trigger a change event unless it actually changed - if (this.usingNativeControls_ === bool) { - return; - } - - this.usingNativeControls_ = bool; - - if (this.usingNativeControls_) { - this.addClass('vjs-using-native-controls'); - - /** - * player is using the native device controls - * - * @event Player#usingnativecontrols - * @type {Event} - */ - this.trigger('usingnativecontrols'); - } else { - this.removeClass('vjs-using-native-controls'); - - /** - * player is using the custom HTML controls - * - * @event Player#usingcustomcontrols - * @type {Event} - */ - this.trigger('usingcustomcontrols'); - } - } - - /** - * Set or get the current MediaError - * - * @fires Player#error - * - * @param {MediaError|string|number} [err] - * A MediaError or a string/number to be turned - * into a MediaError - * - * @return {MediaError|null|undefined} - * - The current MediaError when getting (or null) - * - Nothing when setting - */ - error(err) { - if (err === undefined) { - return this.error_ || null; - } - - // allow hooks to modify error object - hooks('beforeerror').forEach((hookFunction) => { - const newErr = hookFunction(this, err); - - if (!( - (isObject(newErr) && !Array.isArray(newErr)) || - typeof newErr === 'string' || - typeof newErr === 'number' || - newErr === null - )) { - this.log.error('please return a value that MediaError expects in beforeerror hooks'); - return; - } - - err = newErr; - }); - - // Suppress the first error message for no compatible source until - // user interaction - if (this.options_.suppressNotSupportedError && - err && err.code === 4 - ) { - const triggerSuppressedError = function() { - this.error(err); - }; - - this.options_.suppressNotSupportedError = false; - this.any(['click', 'touchstart'], triggerSuppressedError); - this.one('loadstart', function() { - this.off(['click', 'touchstart'], triggerSuppressedError); - }); - return; - } - - // restoring to default - if (err === null) { - this.error_ = null; - this.removeClass('vjs-error'); - if (this.errorDisplay) { - this.errorDisplay.close(); - } - return; - } - - this.error_ = new MediaError(err); - - // add the vjs-error classname to the player - this.addClass('vjs-error'); - - // log the name of the error type and any message - // IE11 logs "[object object]" and required you to expand message to see error object - log.error(`(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, this.error_.message, this.error_); - - /** - * @event Player#error - * @type {Event} - */ - this.trigger('error'); - - // notify hooks of the per player error - hooks('error').forEach((hookFunction) => hookFunction(this, this.error_)); - - return; - } - - /** - * Report user activity - * - * @param {Object} event - * Event object - */ - reportUserActivity(event) { - this.userActivity_ = true; - } - - /** - * Get/set if user is active - * - * @fires Player#useractive - * @fires Player#userinactive - * - * @param {boolean} [bool] - * - true if the user is active - * - false if the user is inactive - * - * @return {boolean|undefined} - * - The current value of userActive when getting - * - Nothing when setting - */ - userActive(bool) { - if (bool === undefined) { - return this.userActive_; - } - - bool = !!bool; - - if (bool === this.userActive_) { - return; - } - - this.userActive_ = bool; - - if (this.userActive_) { - this.userActivity_ = true; - this.removeClass('vjs-user-inactive'); - this.addClass('vjs-user-active'); - /** - * @event Player#useractive - * @type {Event} - */ - this.trigger('useractive'); - return; - } - - // Chrome/Safari/IE have bugs where when you change the cursor it can - // trigger a mousemove event. This causes an issue when you're hiding - // the cursor when the user is inactive, and a mousemove signals user - // activity. Making it impossible to go into inactive mode. Specifically - // this happens in fullscreen when we really need to hide the cursor. - // - // When this gets resolved in ALL browsers it can be removed - // https://code.google.com/p/chromium/issues/detail?id=103041 - if (this.tech_) { - this.tech_.one('mousemove', function(e) { - e.stopPropagation(); - e.preventDefault(); - }); - } - - this.userActivity_ = false; - this.removeClass('vjs-user-active'); - this.addClass('vjs-user-inactive'); - /** - * @event Player#userinactive - * @type {Event} - */ - this.trigger('userinactive'); - } - - /** - * Listen for user activity based on timeout value - * - * @private - */ - listenForUserActivity_() { - let mouseInProgress; - let lastMoveX; - let lastMoveY; - const handleActivity = Fn.bind_(this, this.reportUserActivity); - - const handleMouseMove = function(e) { - // #1068 - Prevent mousemove spamming - // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970 - if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) { - lastMoveX = e.screenX; - lastMoveY = e.screenY; - handleActivity(); - } - }; - - const handleMouseDown = function() { - handleActivity(); - // For as long as the they are touching the device or have their mouse down, - // we consider them active even if they're not moving their finger or mouse. - // So we want to continue to update that they are active - this.clearInterval(mouseInProgress); - // Setting userActivity=true now and setting the interval to the same time - // as the activityCheck interval (250) should ensure we never miss the - // next activityCheck - mouseInProgress = this.setInterval(handleActivity, 250); - }; - - const handleMouseUpAndMouseLeave = function(event) { - handleActivity(); - // Stop the interval that maintains activity if the mouse/touch is down - this.clearInterval(mouseInProgress); - }; - - // Any mouse movement will be considered user activity - this.on('mousedown', handleMouseDown); - this.on('mousemove', handleMouseMove); - this.on('mouseup', handleMouseUpAndMouseLeave); - this.on('mouseleave', handleMouseUpAndMouseLeave); - - const controlBar = this.getChild('controlBar'); - - // Fixes bug on Android & iOS where when tapping progressBar (when control bar is displayed) - // controlBar would no longer be hidden by default timeout. - if (controlBar && !browser.IS_IOS && !browser.IS_ANDROID) { - - controlBar.on('mouseenter', function(event) { - if (this.player().options_.inactivityTimeout !== 0) { - this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout; - } - this.player().options_.inactivityTimeout = 0; - }); - - controlBar.on('mouseleave', function(event) { - this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout; - }); - - } - - // Listen for keyboard navigation - // Shouldn't need to use inProgress interval because of key repeat - this.on('keydown', handleActivity); - this.on('keyup', handleActivity); - - // Run an interval every 250 milliseconds instead of stuffing everything into - // the mousemove/touchmove function itself, to prevent performance degradation. - // `this.reportUserActivity` simply sets this.userActivity_ to true, which - // then gets picked up by this loop - // http://ejohn.org/blog/learning-from-twitter/ - let inactivityTimeout; - - /** @this Player */ - const activityCheck = function() { - // Check to see if mouse/touch activity has happened - if (!this.userActivity_) { - return; - } - - // Reset the activity tracker - this.userActivity_ = false; - - // If the user state was inactive, set the state to active - this.userActive(true); - - // Clear any existing inactivity timeout to start the timer over - this.clearTimeout(inactivityTimeout); - - const timeout = this.options_.inactivityTimeout; - - if (timeout <= 0) { - return; - } - - // In <timeout> milliseconds, if no more activity has occurred the - // user will be considered inactive - inactivityTimeout = this.setTimeout(function() { - // Protect against the case where the inactivityTimeout can trigger just - // before the next user activity is picked up by the activity check loop - // causing a flicker - if (!this.userActivity_) { - this.userActive(false); - } - }, timeout); - - }; - - this.setInterval(activityCheck, 250); - } - - /** - * Gets or sets the current playback rate. A playback rate of - * 1.0 represents normal speed and 0.5 would indicate half-speed - * playback, for instance. - * - * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate - * - * @param {number} [rate] - * New playback rate to set. - * - * @return {number|undefined} - * - The current playback rate when getting or 1.0 - * - Nothing when setting - */ - playbackRate(rate) { - if (rate !== undefined) { - // NOTE: this.cache_.lastPlaybackRate is set from the tech handler - // that is registered above - this.techCall_('setPlaybackRate', rate); - return; - } - - if (this.tech_ && this.tech_.featuresPlaybackRate) { - return this.cache_.lastPlaybackRate || this.techGet_('playbackRate'); - } - return 1.0; - } - - /** - * Gets or sets the current default playback rate. A default playback rate of - * 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance. - * defaultPlaybackRate will only represent what the initial playbackRate of a video was, not - * not the current playbackRate. - * - * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate - * - * @param {number} [rate] - * New default playback rate to set. - * - * @return {number|undefined} - * - The default playback rate when getting or 1.0 - * - Nothing when setting - */ - defaultPlaybackRate(rate) { - if (rate !== undefined) { - return this.techCall_('setDefaultPlaybackRate', rate); - } - - if (this.tech_ && this.tech_.featuresPlaybackRate) { - return this.techGet_('defaultPlaybackRate'); - } - return 1.0; - } - - /** - * Gets or sets the audio flag - * - * @param {boolean} [bool] - * - true signals that this is an audio player - * - false signals that this is not an audio player - * - * @return {boolean|undefined} - * - The current value of isAudio when getting - * - Nothing when setting - */ - isAudio(bool) { - if (bool !== undefined) { - this.isAudio_ = !!bool; - return; - } - - return !!this.isAudio_; - } - - updatePlayerHeightOnAudioOnlyMode_() { - const controlBar = this.getChild('ControlBar'); - - if (!controlBar || this.audioOnlyCache_.controlBarHeight === controlBar.currentHeight()) { - return; - } - - this.audioOnlyCache_.controlBarHeight = controlBar.currentHeight(); - this.height(this.audioOnlyCache_.controlBarHeight); - } - - enableAudioOnlyUI_() { - // Update styling immediately to show the control bar so we can get its height - this.addClass('vjs-audio-only-mode'); - - const playerChildren = this.children(); - const controlBar = this.getChild('ControlBar'); - const controlBarHeight = controlBar && controlBar.currentHeight(); - - // Hide all player components except the control bar. Control bar components - // needed only for video are hidden with CSS - playerChildren.forEach(child => { - if (child === controlBar) { - return; - } - - if (child.el_ && !child.hasClass('vjs-hidden')) { - child.hide(); - - this.audioOnlyCache_.hiddenChildren.push(child); - } - }); - - this.audioOnlyCache_.playerHeight = this.currentHeight(); - this.audioOnlyCache_.controlBarHeight = controlBarHeight; - - this.on('playerresize', this.boundUpdatePlayerHeightOnAudioOnlyMode_); - - // Set the player height the same as the control bar - this.height(controlBarHeight); - this.trigger('audioonlymodechange'); - } - - disableAudioOnlyUI_() { - this.removeClass('vjs-audio-only-mode'); - this.off('playerresize', this.boundUpdatePlayerHeightOnAudioOnlyMode_); - - // Show player components that were previously hidden - this.audioOnlyCache_.hiddenChildren.forEach(child => child.show()); - - // Reset player height - this.height(this.audioOnlyCache_.playerHeight); - this.trigger('audioonlymodechange'); - } - - /** - * Get the current audioOnlyMode state or set audioOnlyMode to true or false. - * - * Setting this to `true` will hide all player components except the control bar, - * as well as control bar components needed only for video. - * - * @param {boolean} [value] - * The value to set audioOnlyMode to. - * - * @return {Promise|boolean} - * A Promise is returned when setting the state, and a boolean when getting - * the present state - */ - audioOnlyMode(value) { - if (typeof value !== 'boolean' || value === this.audioOnlyMode_) { - return this.audioOnlyMode_; - } - - this.audioOnlyMode_ = value; - - // Enable Audio Only Mode - if (value) { - const exitPromises = []; - - // Fullscreen and PiP are not supported in audioOnlyMode, so exit if we need to. - if (this.isInPictureInPicture()) { - exitPromises.push(this.exitPictureInPicture()); - } - - if (this.isFullscreen()) { - exitPromises.push(this.exitFullscreen()); - } - - if (this.audioPosterMode()) { - exitPromises.push(this.audioPosterMode(false)); - } - - return Promise.all(exitPromises).then(() => this.enableAudioOnlyUI_()); - } - - // Disable Audio Only Mode - return Promise.resolve().then(() => this.disableAudioOnlyUI_()); - } - - enablePosterModeUI_() { - // Hide the video element and show the poster image to enable posterModeUI - const tech = this.tech_ && this.tech_; - - tech.hide(); - this.addClass('vjs-audio-poster-mode'); - this.trigger('audiopostermodechange'); - } - - disablePosterModeUI_() { - // Show the video element and hide the poster image to disable posterModeUI - const tech = this.tech_ && this.tech_; - - tech.show(); - this.removeClass('vjs-audio-poster-mode'); - this.trigger('audiopostermodechange'); - } - - /** - * Get the current audioPosterMode state or set audioPosterMode to true or false - * - * @param {boolean} [value] - * The value to set audioPosterMode to. - * - * @return {Promise|boolean} - * A Promise is returned when setting the state, and a boolean when getting - * the present state - */ - audioPosterMode(value) { - - if (typeof value !== 'boolean' || value === this.audioPosterMode_) { - return this.audioPosterMode_; - } - - this.audioPosterMode_ = value; - - if (value) { - - if (this.audioOnlyMode()) { - const audioOnlyModePromise = this.audioOnlyMode(false); - - return audioOnlyModePromise.then(() => { - // enable audio poster mode after audio only mode is disabled - this.enablePosterModeUI_(); - }); - } - - return Promise.resolve().then(() => { - // enable audio poster mode - this.enablePosterModeUI_(); - }); - } - - return Promise.resolve().then(() => { - // disable audio poster mode - this.disablePosterModeUI_(); - }); - } - - /** - * A helper method for adding a {@link TextTrack} to our - * {@link TextTrackList}. - * - * In addition to the W3C settings we allow adding additional info through options. - * - * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack - * - * @param {string} [kind] - * the kind of TextTrack you are adding - * - * @param {string} [label] - * the label to give the TextTrack label - * - * @param {string} [language] - * the language to set on the TextTrack - * - * @return {TextTrack|undefined} - * the TextTrack that was added or undefined - * if there is no tech - */ - addTextTrack(kind, label, language) { - if (this.tech_) { - return this.tech_.addTextTrack(kind, label, language); - } - } - - /** - * Create a remote {@link TextTrack} and an {@link HTMLTrackElement}. - * - * @param {Object} options - * Options to pass to {@link HTMLTrackElement} during creation. See - * {@link HTMLTrackElement} for object properties that you should use. - * - * @param {boolean} [manualCleanup=false] if set to true, the TextTrack will not be removed - * from the TextTrackList and HtmlTrackElementList - * after a source change - * - * @return {HtmlTrackElement} - * the HTMLTrackElement that was created and added - * to the HtmlTrackElementList and the remote - * TextTrackList - * - */ - addRemoteTextTrack(options, manualCleanup) { - if (this.tech_) { - return this.tech_.addRemoteTextTrack(options, manualCleanup); - } - } - - /** - * Remove a remote {@link TextTrack} from the respective - * {@link TextTrackList} and {@link HtmlTrackElementList}. - * - * @param {Object} track - * Remote {@link TextTrack} to remove - * - * @return {undefined} - * does not return anything - */ - removeRemoteTextTrack(obj = {}) { - let {track} = obj; - - if (!track) { - track = obj; - } - - // destructure the input into an object with a track argument, defaulting to arguments[0] - // default the whole argument to an empty object if nothing was passed in - - if (this.tech_) { - return this.tech_.removeRemoteTextTrack(track); - } - } - - /** - * Gets available media playback quality metrics as specified by the W3C's Media - * Playback Quality API. - * - * @see [Spec]{@link https://wicg.github.io/media-playback-quality} - * - * @return {Object|undefined} - * An object with supported media playback quality metrics or undefined if there - * is no tech or the tech does not support it. - */ - getVideoPlaybackQuality() { - return this.techGet_('getVideoPlaybackQuality'); - } - - /** - * Get video width - * - * @return {number} - * current video width - */ - videoWidth() { - return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0; - } - - /** - * Get video height - * - * @return {number} - * current video height - */ - videoHeight() { - return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0; - } - - /** - * Set or get the player's language code. - * - * Changing the language will trigger - * [languagechange]{@link Player#event:languagechange} - * which Components can use to update control text. - * ClickableComponent will update its control text by default on - * [languagechange]{@link Player#event:languagechange}. - * - * @fires Player#languagechange - * - * @param {string} [code] - * the language code to set the player to - * - * @return {string|undefined} - * - The current language code when getting - * - Nothing when setting - */ - language(code) { - if (code === undefined) { - return this.language_; - } - - if (this.language_ !== String(code).toLowerCase()) { - this.language_ = String(code).toLowerCase(); - - // during first init, it's possible some things won't be evented - if (isEvented(this)) { - /** - * fires when the player language change - * - * @event Player#languagechange - * @type {Event} - */ - this.trigger('languagechange'); - } - } - } - - /** - * Get the player's language dictionary - * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time - * Languages specified directly in the player options have precedence - * - * @return {Array} - * An array of of supported languages - */ - languages() { - return merge(Player.prototype.options_.languages, this.languages_); - } - - /** - * returns a JavaScript object representing the current track - * information. **DOES not return it as JSON** - * - * @return {Object} - * Object representing the current of track info - */ - toJSON() { - const options = merge(this.options_); - const tracks = options.tracks; - - options.tracks = []; - - for (let i = 0; i < tracks.length; i++) { - let track = tracks[i]; - - // deep merge tracks and null out player so no circular references - track = merge(track); - track.player = undefined; - options.tracks[i] = track; - } - - return options; - } - - /** - * Creates a simple modal dialog (an instance of the {@link ModalDialog} - * component) that immediately overlays the player with arbitrary - * content and removes itself when closed. - * - * @param {string|Function|Element|Array|null} content - * Same as {@link ModalDialog#content}'s param of the same name. - * The most straight-forward usage is to provide a string or DOM - * element. - * - * @param {Object} [options] - * Extra options which will be passed on to the {@link ModalDialog}. - * - * @return {ModalDialog} - * the {@link ModalDialog} that was created - */ - createModal(content, options) { - options = options || {}; - options.content = content || ''; - - const modal = new ModalDialog(this, options); - - this.addChild(modal); - modal.on('dispose', () => { - this.removeChild(modal); - }); - - modal.open(); - return modal; - } - - /** - * Change breakpoint classes when the player resizes. - * - * @private - */ - updateCurrentBreakpoint_() { - if (!this.responsive()) { - return; - } - - const currentBreakpoint = this.currentBreakpoint(); - const currentWidth = this.currentWidth(); - - for (let i = 0; i < BREAKPOINT_ORDER.length; i++) { - const candidateBreakpoint = BREAKPOINT_ORDER[i]; - const maxWidth = this.breakpoints_[candidateBreakpoint]; - - if (currentWidth <= maxWidth) { - - // The current breakpoint did not change, nothing to do. - if (currentBreakpoint === candidateBreakpoint) { - return; - } - - // Only remove a class if there is a current breakpoint. - if (currentBreakpoint) { - this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]); - } - - this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]); - this.breakpoint_ = candidateBreakpoint; - break; - } - } - } - - /** - * Removes the current breakpoint. - * - * @private - */ - removeCurrentBreakpoint_() { - const className = this.currentBreakpointClass(); - - this.breakpoint_ = ''; - - if (className) { - this.removeClass(className); - } - } - - /** - * Get or set breakpoints on the player. - * - * Calling this method with an object or `true` will remove any previous - * custom breakpoints and start from the defaults again. - * - * @param {Object|boolean} [breakpoints] - * If an object is given, it can be used to provide custom - * breakpoints. If `true` is given, will set default breakpoints. - * If this argument is not given, will simply return the current - * breakpoints. - * - * @param {number} [breakpoints.tiny] - * The maximum width for the "vjs-layout-tiny" class. - * - * @param {number} [breakpoints.xsmall] - * The maximum width for the "vjs-layout-x-small" class. - * - * @param {number} [breakpoints.small] - * The maximum width for the "vjs-layout-small" class. - * - * @param {number} [breakpoints.medium] - * The maximum width for the "vjs-layout-medium" class. - * - * @param {number} [breakpoints.large] - * The maximum width for the "vjs-layout-large" class. - * - * @param {number} [breakpoints.xlarge] - * The maximum width for the "vjs-layout-x-large" class. - * - * @param {number} [breakpoints.huge] - * The maximum width for the "vjs-layout-huge" class. - * - * @return {Object} - * An object mapping breakpoint names to maximum width values. - */ - breakpoints(breakpoints) { - - // Used as a getter. - if (breakpoints === undefined) { - return Object.assign(this.breakpoints_); - } - - this.breakpoint_ = ''; - this.breakpoints_ = Object.assign({}, DEFAULT_BREAKPOINTS, breakpoints); - - // When breakpoint definitions change, we need to update the currently - // selected breakpoint. - this.updateCurrentBreakpoint_(); - - // Clone the breakpoints before returning. - return Object.assign(this.breakpoints_); - } - - /** - * Get or set a flag indicating whether or not this player should adjust - * its UI based on its dimensions. - * - * @param {boolean} [value] - * Should be `true` if the player should adjust its UI based on its - * dimensions; otherwise, should be `false`. - * - * @return {boolean|undefined} - * Will be `true` if this player should adjust its UI based on its - * dimensions; otherwise, will be `false`. - * Nothing if setting - */ - responsive(value) { - - // Used as a getter. - if (value === undefined) { - return this.responsive_; - } - - value = Boolean(value); - const current = this.responsive_; - - // Nothing changed. - if (value === current) { - return; - } - - // The value actually changed, set it. - this.responsive_ = value; - - // Start listening for breakpoints and set the initial breakpoint if the - // player is now responsive. - if (value) { - this.on('playerresize', this.boundUpdateCurrentBreakpoint_); - this.updateCurrentBreakpoint_(); - - // Stop listening for breakpoints if the player is no longer responsive. - } else { - this.off('playerresize', this.boundUpdateCurrentBreakpoint_); - this.removeCurrentBreakpoint_(); - } - - return value; - } - - /** - * Get current breakpoint name, if any. - * - * @return {string} - * If there is currently a breakpoint set, returns a the key from the - * breakpoints object matching it. Otherwise, returns an empty string. - */ - currentBreakpoint() { - return this.breakpoint_; - } - - /** - * Get the current breakpoint class name. - * - * @return {string} - * The matching class name (e.g. `"vjs-layout-tiny"` or - * `"vjs-layout-large"`) for the current breakpoint. Empty string if - * there is no current breakpoint. - */ - currentBreakpointClass() { - return BREAKPOINT_CLASSES[this.breakpoint_] || ''; - } - - /** - * An object that describes a single piece of media. - * - * Properties that are not part of this type description will be retained; so, - * this can be viewed as a generic metadata storage mechanism as well. - * - * @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface} - * @typedef {Object} Player~MediaObject - * - * @property {string} [album] - * Unused, except if this object is passed to the `MediaSession` - * API. - * - * @property {string} [artist] - * Unused, except if this object is passed to the `MediaSession` - * API. - * - * @property {Object[]} [artwork] - * Unused, except if this object is passed to the `MediaSession` - * API. If not specified, will be populated via the `poster`, if - * available. - * - * @property {string} [poster] - * URL to an image that will display before playback. - * - * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src] - * A single source object, an array of source objects, or a string - * referencing a URL to a media source. It is _highly recommended_ - * that an object or array of objects is used here, so that source - * selection algorithms can take the `type` into account. - * - * @property {string} [title] - * Unused, except if this object is passed to the `MediaSession` - * API. - * - * @property {Object[]} [textTracks] - * An array of objects to be used to create text tracks, following - * the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}. - * For ease of removal, these will be created as "remote" text - * tracks and set to automatically clean up on source changes. - * - * These objects may have properties like `src`, `kind`, `label`, - * and `language`, see {@link Tech#createRemoteTextTrack}. - */ - - /** - * Populate the player using a {@link Player~MediaObject|MediaObject}. - * - * @param {Player~MediaObject} media - * A media object. - * - * @param {Function} ready - * A callback to be called when the player is ready. - */ - loadMedia(media, ready) { - if (!media || typeof media !== 'object') { - return; - } - - const crossOrigin = this.crossOrigin(); - - this.reset(); - - // Clone the media object so it cannot be mutated from outside. - this.cache_.media = merge(media); - - const {artist, artwork, description, poster, src, textTracks, title} = this.cache_.media; - - // If `artwork` is not given, create it using `poster`. - if (!artwork && poster) { - this.cache_.media.artwork = [{ - src: poster, - type: getMimetype(poster) - }]; - } - - if (crossOrigin) { - this.crossOrigin(crossOrigin); - } - - if (src) { - this.src(src); - } - - if (poster) { - this.poster(poster); - } - - if (Array.isArray(textTracks)) { - textTracks.forEach(tt => this.addRemoteTextTrack(tt, false)); - } - - if (this.titleBar) { - this.titleBar.update({ - title, - description: description || artist || '' - }); - } - - this.ready(ready); - } - - /** - * Get a clone of the current {@link Player~MediaObject} for this player. - * - * If the `loadMedia` method has not been used, will attempt to return a - * {@link Player~MediaObject} based on the current state of the player. - * - * @return {Player~MediaObject} - */ - getMedia() { - if (!this.cache_.media) { - const poster = this.poster(); - const src = this.currentSources(); - const textTracks = Array.prototype.map.call(this.remoteTextTracks(), (tt) => ({ - kind: tt.kind, - label: tt.label, - language: tt.language, - src: tt.src - })); - - const media = {src, textTracks}; - - if (poster) { - media.poster = poster; - media.artwork = [{ - src: media.poster, - type: getMimetype(media.poster) - }]; - } - - return media; - } - - return merge(this.cache_.media); - } - - /** - * Gets tag settings - * - * @param {Element} tag - * The player tag - * - * @return {Object} - * An object containing all of the settings - * for a player tag - */ - static getTagSettings(tag) { - const baseOptions = { - sources: [], - tracks: [] - }; - - const tagOptions = Dom.getAttributes(tag); - const dataSetup = tagOptions['data-setup']; - - if (Dom.hasClass(tag, 'vjs-fill')) { - tagOptions.fill = true; - } - if (Dom.hasClass(tag, 'vjs-fluid')) { - tagOptions.fluid = true; - } - - // Check if data-setup attr exists. - if (dataSetup !== null) { - // Parse options JSON - try { - // If empty string, make it a parsable json object. - Object.assign(tagOptions, JSON.parse(dataSetup || '{}')); - } catch (e) { - log.error('data-setup', e); - } - } - - Object.assign(baseOptions, tagOptions); - - // Get tag children settings - if (tag.hasChildNodes()) { - const children = tag.childNodes; - - for (let i = 0, j = children.length; i < j; i++) { - const child = children[i]; - // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/ - const childName = child.nodeName.toLowerCase(); - - if (childName === 'source') { - baseOptions.sources.push(Dom.getAttributes(child)); - } else if (childName === 'track') { - baseOptions.tracks.push(Dom.getAttributes(child)); - } - } - } - - return baseOptions; - } - - /** - * Set debug mode to enable/disable logs at info level. - * - * @param {boolean} enabled - * @fires Player#debugon - * @fires Player#debugoff - * @return {boolean|undefined} - */ - debug(enabled) { - if (enabled === undefined) { - return this.debugEnabled_; - } - if (enabled) { - this.trigger('debugon'); - this.previousLogLevel_ = this.log.level; - this.log.level('debug'); - this.debugEnabled_ = true; - } else { - this.trigger('debugoff'); - this.log.level(this.previousLogLevel_); - this.previousLogLevel_ = undefined; - this.debugEnabled_ = false; - } - - } - - /** - * Set or get current playback rates. - * Takes an array and updates the playback rates menu with the new items. - * Pass in an empty array to hide the menu. - * Values other than arrays are ignored. - * - * @fires Player#playbackrateschange - * @param {number[]} [newRates] - * The new rates that the playback rates menu should update to. - * An empty array will hide the menu - * @return {number[]} When used as a getter will return the current playback rates - */ - playbackRates(newRates) { - if (newRates === undefined) { - return this.cache_.playbackRates; - } - - // ignore any value that isn't an array - if (!Array.isArray(newRates)) { - return; - } - - // ignore any arrays that don't only contain numbers - if (!newRates.every((rate) => typeof rate === 'number')) { - return; - } - - this.cache_.playbackRates = newRates; - - /** - * fires when the playback rates in a player are changed - * - * @event Player#playbackrateschange - * @type {Event} - */ - this.trigger('playbackrateschange'); - } - - /** - * Reports whether or not a player has a plugin available. - * - * This does not report whether or not the plugin has ever been initialized - * on this player. For that, [usingPlugin]{@link Player#usingPlugin}. - * - * @method hasPlugin - * @param {string} name - * The name of a plugin. - * - * @return {boolean} - * Whether or not this player has the requested plugin available. - */ - /* start-delete-from-build */ - hasPlugin(name) { - return false; - } - /* end-delete-from-build */ - - /** - * Reports whether or not a player is using a plugin by name. - * - * For basic plugins, this only reports whether the plugin has _ever_ been - * initialized on this player. - * - * @method Player#usingPlugin - * @param {string} name - * The name of a plugin. - * - * @return {boolean} - * Whether or not this player is using the requested plugin. - */ - /* start-delete-from-build */ - usingPlugin(name) { - return false; - } - /* end-delete-from-build */ -} - -/** - * Get the {@link VideoTrackList} - * - * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist - * - * @return {VideoTrackList} - * the current video track list - * - * @method Player.prototype.videoTracks - */ -Player.prototype.videoTracks = () => {}; - -/** - * Get the {@link AudioTrackList} - * - * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist - * - * @return {AudioTrackList} - * the current audio track list - * - * @method Player.prototype.audioTracks - */ -Player.prototype.audioTracks = () => {}; - -/** - * Get the {@link TextTrackList} - * - * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks - * - * @return {TextTrackList} - * the current text track list - * - * @method Player.prototype.textTracks - */ -Player.prototype.textTracks = () => {}; - -/** - * Get the remote {@link TextTrackList} - * - * @return {TextTrackList} - * The current remote text track list - * - * @method Player.prototype.remoteTextTracks - */ -Player.prototype.remoteTextTracks = () => {}; - -/** - * Get the remote {@link HtmlTrackElementList} tracks. - * - * @return {HtmlTrackElementList} - * The current remote text track element list - * - * @method Player.prototype.remoteTextTrackEls - */ -Player.prototype.remoteTextTrackEls = () => {}; - -TRACK_TYPES.names.forEach(function(name) { - const props = TRACK_TYPES[name]; - - Player.prototype[props.getterName] = function() { - if (this.tech_) { - return this.tech_[props.getterName](); - } - - // if we have not yet loadTech_, we create {video,audio,text}Tracks_ - // these will be passed to the tech during loading - this[props.privateName] = this[props.privateName] || new props.ListClass(); - return this[props.privateName]; - }; -}); - -/** - * Get or set the `Player`'s crossorigin option. For the HTML5 player, this - * sets the `crossOrigin` property on the `<video>` tag to control the CORS - * behavior. - * - * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin} - * - * @param {string} [value] - * The value to set the `Player`'s crossorigin to. If an argument is - * given, must be one of `anonymous` or `use-credentials`. - * - * @return {string|undefined} - * - The current crossorigin value of the `Player` when getting. - * - undefined when setting - */ -Player.prototype.crossorigin = Player.prototype.crossOrigin; - -/** - * Global enumeration of players. - * - * The keys are the player IDs and the values are either the {@link Player} - * instance or `null` for disposed players. - * - * @type {Object} - */ -Player.players = {}; - -const navigator = window.navigator; - -/* - * Player instance options, surfaced using options - * options = Player.prototype.options_ - * Make changes in options, not here. - * - * @type {Object} - * @private - */ -Player.prototype.options_ = { - // Default order of fallback technology - techOrder: Tech.defaultTechOrder_, - - html5: {}, - - // enable sourceset by default - enableSourceset: true, - - // default inactivity timeout - inactivityTimeout: 2000, - - // default playback rates - playbackRates: [], - // Add playback rate selection by adding rates - // 'playbackRates': [0.5, 1, 1.5, 2], - liveui: false, - - // Included control sets - children: [ - 'mediaLoader', - 'posterImage', - 'titleBar', - 'textTrackDisplay', - 'loadingSpinner', - 'bigPlayButton', - 'liveTracker', - 'controlBar', - 'errorDisplay', - 'textTrackSettings', - 'resizeManager' - ], - - language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en', - - // locales and their language translations - languages: {}, - - // Default message to show when a video cannot be played. - notSupportedMessage: 'No compatible source was found for this media.', - - normalizeAutoplay: false, - - fullscreen: { - options: { - navigationUI: 'hide' - } - }, - - breakpoints: {}, - responsive: false, - audioOnlyMode: false, - audioPosterMode: false, - spatialNavigation: { - enabled: false, - horizontalSeek: false - }, - // Default smooth seeking to false - enableSmoothSeeking: false, - disableSeekWhileScrubbingOnMobile: false, - disableSeekWhileScrubbingOnSTV: false -}; - -TECH_EVENTS_RETRIGGER.forEach(function(event) { - Player.prototype[`handleTech${toTitleCase(event)}_`] = function() { - return this.trigger(event); - }; -}); - -/** - * Fired when the player has initial duration and dimension information - * - * @event Player#loadedmetadata - * @type {Event} - */ - -/** - * Fired when the player has downloaded data at the current playback position - * - * @event Player#loadeddata - * @type {Event} - */ - -/** - * Fired when the current playback position has changed * - * During playback this is fired every 15-250 milliseconds, depending on the - * playback technology in use. - * - * @event Player#timeupdate - * @type {Event} - */ - -/** - * Fired when the volume changes - * - * @event Player#volumechange - * @type {Event} - */ - -Component.registerComponent('Player', Player); -export default Player; |
