/**
* @file html5.js
*/
import Tech from './tech.js';
import Component from '../component';
import * as Dom from '../utils/dom.js';
import * as Url from '../utils/url.js';
import * as Fn from '../utils/fn.js';
import log from '../utils/log.js';
import tsml from 'tsml';
import * as browser from '../utils/browser.js';
import document from 'global/document';
import window from 'global/window';
import {assign} from '../utils/obj';
import mergeOptions from '../utils/merge-options.js';
import toTitleCase from '../utils/to-title-case.js';
/**
* HTML5 Media Controller - Wrapper for HTML5 Media API
*
* @mixes Tech~SouceHandlerAdditions
* @extends Tech
*/
class Html5 extends Tech {
/**
* Create an instance of this Tech.
*
* @param {Object} [options]
* The key/value store of player options.
*
* @param {Component~ReadyCallback} ready
* Callback function to call when the `HTML5` Tech is ready.
*/
constructor(options, ready) {
super(options, ready);
const source = options.source;
let crossoriginTracks = false;
// Set the source if one is provided
// 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted)
// 2) Check to see if the network state of the tag was failed at init, and if so, reset the source
// anyway so the error gets fired.
if (source && (this.el_.currentSrc !== source.src || (options.tag && options.tag.initNetworkState_ === 3))) {
this.setSource(source);
} else {
this.handleLateInit_(this.el_);
}
if (this.el_.hasChildNodes()) {
const nodes = this.el_.childNodes;
let nodesLength = nodes.length;
const removeNodes = [];
while (nodesLength--) {
const node = nodes[nodesLength];
const nodeName = node.nodeName.toLowerCase();
if (nodeName === 'track') {
if (!this.featuresNativeTextTracks) {
// Empty video tag tracks so the built-in player doesn't use them also.
// This may not be fast enough to stop HTML5 browsers from reading the tags
// so we'll need to turn off any default tracks if we're manually doing
// captions and subtitles. videoElement.textTracks
removeNodes.push(node);
} else {
// store HTMLTrackElement and TextTrack to remote list
this.remoteTextTrackEls().addTrackElement_(node);
this.remoteTextTracks().addTrack_(node.track);
if (!crossoriginTracks &&
!this.el_.hasAttribute('crossorigin') &&
Url.isCrossOrigin(node.src)) {
crossoriginTracks = true;
}
}
}
}
for (let i = 0; i < removeNodes.length; i++) {
this.el_.removeChild(removeNodes[i]);
}
}
// TODO: add text tracks into this list
const trackTypes = ['audio', 'video'];
// ProxyNative Video/Audio Track
trackTypes.forEach((type) => {
const elTracks = this.el()[`${type}Tracks`];
const techTracks = this[`${type}Tracks`]();
const capitalType = toTitleCase(type);
if (!this[`featuresNative${capitalType}Tracks`] ||
!elTracks ||
!elTracks.addEventListener) {
return;
}
this[`handle${capitalType}TrackChange_`] = (e) => {
techTracks.trigger({
type: 'change',
target: techTracks,
currentTarget: techTracks,
srcElement: techTracks
});
};
this[`handle${capitalType}TrackAdd_`] = (e) => techTracks.addTrack(e.track);
this[`handle${capitalType}TrackRemove_`] = (e) => techTracks.removeTrack(e.track);
elTracks.addEventListener('change', this[`handle${capitalType}TrackChange_`]);
elTracks.addEventListener('addtrack', this[`handle${capitalType}TrackAdd_`]);
elTracks.addEventListener('removetrack', this[`handle${capitalType}TrackRemove_`]);
this[`removeOld${capitalType}Tracks_`] = (e) => this.removeOldTracks_(techTracks, elTracks);
// Remove (native) tracks that are not used anymore
this.on('loadstart', this[`removeOld${capitalType}Tracks_`]);
});
if (this.featuresNativeTextTracks) {
if (crossoriginTracks) {
log.warn(tsml`Text Tracks are being loaded from another origin but the crossorigin attribute isn't used.
This may prevent text tracks from loading.`);
}
this.handleTextTrackChange_ = Fn.bind(this, this.handleTextTrackChange);
this.handleTextTrackAdd_ = Fn.bind(this, this.handleTextTrackAdd);
this.handleTextTrackRemove_ = Fn.bind(this, this.handleTextTrackRemove);
this.proxyNativeTextTracks_();
}
// Determine if native controls should be used
// Our goal should be to get the custom controls on mobile solid everywhere
// so we can remove this all together. Right now this will block custom
// controls on touch enabled laptops like the Chrome Pixel
if ((browser.TOUCH_ENABLED || browser.IS_IPHONE ||
browser.IS_NATIVE_ANDROID) && options.nativeControlsForTouch === true) {
this.setControls(true);
}
// on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen`
// into a `fullscreenchange` event
this.proxyWebkitFullscreen_();
this.triggerReady();
}
/**
* Dispose of `HTML5` media element and remove all tracks.
*/
dispose() {
// Un-ProxyNativeTracks
['audio', 'video', 'text'].forEach((type) => {
const capitalType = toTitleCase(type);
const tl = this.el_[`${type}Tracks`];
if (tl && tl.removeEventListener) {
tl.removeEventListener('change', this[`handle${capitalType}TrackChange_`]);
tl.removeEventListener('addtrack', this[`handle${capitalType}TrackAdd_`]);
tl.removeEventListener('removetrack', this[`handle${capitalType}TrackRemove_`]);
}
// Stop removing old text tracks
if (tl) {
this.off('loadstart', this[`removeOld${capitalType}Tracks_`]);
}
});
Html5.disposeMediaElement(this.el_);
// tech will handle clearing of the emulated track list
super.dispose();
}
/**
* Create the `Html5` Tech's DOM element.
*
* @return {Element}
* The element that gets created.
*/
createEl() {
let el = this.options_.tag;
// Check if this browser supports moving the element into the box.
// On the iPhone video will break if you move the element,
// So we have to create a brand new element.
// If we ingested the player div, we do not need to move the media element.
if (!el ||
!(this.options_.playerElIngest ||
this.movingMediaElementInDOM)) {
// If the original tag is still there, clone and remove it.
if (el) {
const clone = el.cloneNode(true);
if (el.parentNode) {
el.parentNode.insertBefore(clone, el);
}
Html5.disposeMediaElement(el);
el = clone;
} else {
el = document.createElement('video');
// determine if native controls should be used
const tagAttributes = this.options_.tag && Dom.getElAttributes(this.options_.tag);
const attributes = mergeOptions({}, tagAttributes);
if (!browser.TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) {
delete attributes.controls;
}
Dom.setElAttributes(el,
assign(attributes, {
id: this.options_.techId,
class: 'vjs-tech'
})
);
}
el.playerId = this.options_.playerId;
}
// Update specific tag settings, in case they were overridden
const settingsAttrs = ['autoplay', 'preload', 'loop', 'muted'];
for (let i = settingsAttrs.length - 1; i >= 0; i--) {
const attr = settingsAttrs[i];
const overwriteAttrs = {};
if (typeof this.options_[attr] !== 'undefined') {
overwriteAttrs[attr] = this.options_[attr];
}
Dom.setElAttributes(el, overwriteAttrs);
}
return el;
}
/**
* This will be triggered if the loadstart event has already fired, before videojs was
* ready. Two known examples of when this can happen are:
* 1. If we're loading the playback object after it has started loading
* 2. The media is already playing the (often with autoplay on) then
*
* This function will fire another loadstart so that videojs can catchup.
*
* @fires Tech#loadstart
*
* @return {undefined}
* returns nothing.
*/
handleLateInit_(el) {
if (el.networkState === 0 || el.networkState === 3) {
// The video element hasn't started loading the source yet
// or didn't find a source
return;
}
if (el.readyState === 0) {
// NetworkState is set synchronously BUT loadstart is fired at the
// end of the current stack, usually before setInterval(fn, 0).
// So at this point we know loadstart may have already fired or is
// about to fire, and either way the player hasn't seen it yet.
// We don't want to fire loadstart prematurely here and cause a
// double loadstart so we'll wait and see if it happens between now
// and the next loop, and fire it if not.
// HOWEVER, we also want to make sure it fires before loadedmetadata
// which could also happen between now and the next loop, so we'll
// watch for that also.
let loadstartFired = false;
const setLoadstartFired = function() {
loadstartFired = true;
};
this.on('loadstart', setLoadstartFired);
const triggerLoadstart = function() {
// We did miss the original loadstart. Make sure the player
// sees loadstart before loadedmetadata
if (!loadstartFired) {
this.trigger('loadstart');
}
};
this.on('loadedmetadata', triggerLoadstart);
this.ready(function() {
this.off('loadstart', setLoadstartFired);
this.off('loadedmetadata', triggerLoadstart);
if (!loadstartFired) {
// We did miss the original native loadstart. Fire it now.
this.trigger('loadstart');
}
});
return;
}
// From here on we know that loadstart already fired and we missed it.
// The other readyState events aren't as much of a problem if we double
// them, so not going to go to as much trouble as loadstart to prevent
// that unless we find reason to.
const eventsToTrigger = ['loadstart'];
// loadedmetadata: newly equal to HAVE_METADATA (1) or greater
eventsToTrigger.push('loadedmetadata');
// loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater
if (el.readyState >= 2) {
eventsToTrigger.push('loadeddata');
}
// canplay: newly increased to HAVE_FUTURE_DATA (3) or greater
if (el.readyState >= 3) {
eventsToTrigger.push('canplay');
}
// canplaythrough: newly equal to HAVE_ENOUGH_DATA (4)
if (el.readyState >= 4) {
eventsToTrigger.push('canplaythrough');
}
// We still need to give the player time to add event listeners
this.ready(function() {
eventsToTrigger.forEach(function(type) {
this.trigger(type);
}, this);
});
}
/**
* Add event listeners to native text track events. This adds the native text tracks
* to our emulated {@link TextTrackList}.
*/
proxyNativeTextTracks_() {
const tt = this.el().textTracks;
if (tt) {
// Add tracks - if player is initialised after DOM loaded, textTracks
// will not trigger addtrack
for (let i = 0; i < tt.length; i++) {
this.textTracks().addTrack_(tt[i]);
}
if (tt.addEventListener) {
tt.addEventListener('change', this.handleTextTrackChange_);
tt.addEventListener('addtrack', this.handleTextTrackAdd_);
tt.addEventListener('removetrack', this.handleTextTrackRemove_);
}
// Remove (native) texttracks that are not used anymore
this.on('loadstart', this.removeOldTextTracks_);
}
}
/**
* Handle any {@link TextTrackList} `change` event.
*
* @param {EventTarget~Event} e
* The `change` event that caused this to run.
*
* @listens TextTrackList#change
*/
handleTextTrackChange(e) {
const tt = this.textTracks();
this.textTracks().trigger({
type: 'change',
target: tt,
currentTarget: tt,
srcElement: tt
});
}
/**
* Handle any {@link TextTrackList} `addtrack` event.
*
* @param {EventTarget~Event} e
* The `addtrack` event that caused this to run.
*
* @listens TextTrackList#addtrack
*/
handleTextTrackAdd(e) {
this.textTracks().addTrack_(e.track);
}
/**
* Handle any {@link TextTrackList} `removetrack` event.
*
* @param {EventTarget~Event} e
* The `removetrack` event that caused this to run.
*
* @listens TextTrackList#removetrack
*/
handleTextTrackRemove(e) {
this.textTracks().removeTrack_(e.track);
}
/**
* This function removes any {@link AudioTrack}s, {@link VideoTrack}s, or
* {@link TextTrack}s that are not in the media elements TrackList.
*
* @param {TrackList} techTracks
* HTML5 Tech's TrackList to search through
*
* @param {TrackList} elTracks
* HTML5 media elements TrackList to search trough.
*
* @private
*/
removeOldTracks_(techTracks, elTracks) {
// This will loop over the techTracks and check if they are still used by the HTML5 media element
// If not, they will be removed from the emulated list
const removeTracks = [];
if (!elTracks) {
return;
}
for (let i = 0; i < techTracks.length; i++) {
const techTrack = techTracks[i];
let found = false;
for (let j = 0; j < elTracks.length; j++) {
if (elTracks[j] === techTrack) {
found = true;
break;
}
}
if (!found) {
removeTracks.push(techTrack);
}
}
for (let i = 0; i < removeTracks.length; i++) {
const track = removeTracks[i];
techTracks.removeTrack_(track);
}
}
/**
* Remove {@link TextTrack}s that dont exist in the native track list from our
* emulated {@link TextTrackList}.
*
* @listens Tech#loadstart
*/
removeOldTextTracks_(e) {
const techTracks = this.textTracks();
const elTracks = this.el().textTracks;
this.removeOldTracks_(techTracks, elTracks);
}
/**
* Called by {@link Player#play} to play using the `Html5` `Tech`.
*/
play() {
const playPromise = this.el_.play();
// Catch/silence error when a pause interrupts a play request
// on browsers which return a promise
if (playPromise !== undefined && typeof playPromise.then === 'function') {
playPromise.then(null, (e) => {});
}
}
/**
* Set current time for the `HTML5` tech.
*
* @param {number} seconds
* Set the current time of the media to this.
*/
setCurrentTime(seconds) {
try {
this.el_.currentTime = seconds;
} catch (e) {
log(e, 'Video is not ready. (Video.js)');
// this.warning(VideoJS.warnings.videoNotReady);
}
}
/**
* Get the current duration of the HTML5 media element.
*
* @return {number}
* The duration of the media or 0 if there is no duration.
*/
duration() {
// Android Chrome will report duration as Infinity for VOD HLS until after
// playback has started, which triggers the live display erroneously.
// Return NaN if playback has not started and trigger a durationupdate once
// the duration can be reliably known.
if (this.el_.duration === Infinity &&
browser.IS_ANDROID && browser.IS_CHROME) {
if (this.el_.currentTime === 0) {
// Wait for the first `timeupdate` with currentTime > 0 - there may be
// several with 0
const checkProgress = () => {
if (this.el_.currentTime > 0) {
// Trigger durationchange for genuinely live video
if (this.el_.duration === Infinity) {
this.trigger('durationchange');
}
this.off('timeupdate', checkProgress);
}
};
this.on('timeupdate', checkProgress);
return NaN;
}
}
return this.el_.duration || NaN;
}
/**
* Get the current width of the HTML5 media element.
*
* @return {number}
* The width of the HTML5 media element.
*/
width() {
return this.el_.offsetWidth;
}
/**
* Get the current height of the HTML5 media element.
*
* @return {number}
* The heigth of the HTML5 media element.
*/
height() {
return this.el_.offsetHeight;
}
/**
* Proxy iOS `webkitbeginfullscreen` and `webkitendfullscreen` into
* `fullscreenchange` event.
*
* @private
* @fires fullscreenchange
* @listens webkitendfullscreen
* @listens webkitbeginfullscreen
* @listens webkitbeginfullscreen
*/
proxyWebkitFullscreen_() {
if (!('webkitDisplayingFullscreen' in this.el_)) {
return;
}
const endFn = function() {
this.trigger('fullscreenchange', { isFullscreen: false });
};
const beginFn = function() {
this.one('webkitendfullscreen', endFn);
this.trigger('fullscreenchange', { isFullscreen: true });
};
this.on('webkitbeginfullscreen', beginFn);
this.on('dispose', () => {
this.off('webkitbeginfullscreen', beginFn);
this.off('webkitendfullscreen', endFn);
});
}
/**
* Check if fullscreen is supported on the current playback device.
*
* @return {boolean}
* - True if fullscreen is supported.
* - False if fullscreen is not supported.
*/
supportsFullScreen() {
if (typeof this.el_.webkitEnterFullScreen === 'function') {
const userAgent = window.navigator && window.navigator.userAgent || '';
// Seems to be broken in Chromium/Chrome && Safari in Leopard
if ((/Android/).test(userAgent) || !(/Chrome|Mac OS X 10.5/).test(userAgent)) {
return true;
}
}
return false;
}
/**
* Request that the `HTML5` Tech enter fullscreen.
*/
enterFullScreen() {
const video = this.el_;
if (video.paused && video.networkState <= video.HAVE_METADATA) {
// attempt to prime the video element for programmatic access
// this isn't necessary on the desktop but shouldn't hurt
this.el_.play();
// playing and pausing synchronously during the transition to fullscreen
// can get iOS ~6.1 devices into a play/pause loop
this.setTimeout(function() {
video.pause();
video.webkitEnterFullScreen();
}, 0);
} else {
video.webkitEnterFullScreen();
}
}
/**
* Request that the `HTML5` Tech exit fullscreen.
*/
exitFullScreen() {
this.el_.webkitExitFullScreen();
}
/**
* A getter/setter for the `Html5` Tech's source object.
* > Note: Please use {@link Html5#setSource}
*
* @param {Tech~SourceObject} [src]
* The source object you want to set on the `HTML5` techs element.
*
* @return {Tech~SourceObject|undefined}
* - The current source object when a source is not passed in.
* - undefined when setting
*
* @deprecated Since version 5.
*/
src(src) {
if (src === undefined) {
return this.el_.src;
}
// Setting src through `src` instead of `setSrc` will be deprecated
this.setSrc(src);
}
/**
* Reset the tech by removing all sources and then calling
* {@link Html5.resetMediaElement}.
*/
reset() {
Html5.resetMediaElement(this.el_);
}
/**
* Get the current source on the `HTML5` Tech. Falls back to returning the source from
* the HTML5 media element.
*
* @return {Tech~SourceObject}
* The current source object from the HTML5 tech. With a fallback to the
* elements source.
*/
currentSrc() {
if (this.currentSource_) {
return this.currentSource_.src;
}
return this.el_.currentSrc;
}
/**
* Set controls attribute for the HTML5 media Element.
*
* @param {string} val
* Value to set the controls attribute to
*/
setControls(val) {
this.el_.controls = !!val;
}
/**
* Create and returns a remote {@link TextTrack} object.
*
* @param {string} kind
* `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
*
* @param {string} [label]
* Label to identify the text track
*
* @param {string} [language]
* Two letter language abbreviation
*
* @return {TextTrack}
* The TextTrack that gets created.
*/
addTextTrack(kind, label, language) {
if (!this.featuresNativeTextTracks) {
return super.addTextTrack(kind, label, language);
}
return this.el_.addTextTrack(kind, label, language);
}
/**
* Creates either native TextTrack or an emulated TextTrack depending
* on the value of `featuresNativeTextTracks`
*
* @param {Object} options
* The object should contain the options to intialize the TextTrack with.
*
* @param {string} [options.kind]
* `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
*
* @param {string} [options.label].
* Label to identify the text track
*
* @param {string} [options.language]
* Two letter language abbreviation.
*
* @param {boolean} [options.default]
* Default this track to on.
*
* @param {string} [options.id]
* The internal id to assign this track.
*
* @param {string} [options.src]
* A source url for the track.
*
* @return {HTMLTrackElement}
* The track element that gets created.
*/
createRemoteTextTrack(options) {
if (!this.featuresNativeTextTracks) {
return super.createRemoteTextTrack(options);
}
const htmlTrackElement = document.createElement('track');
if (options.kind) {
htmlTrackElement.kind = options.kind;
}
if (options.label) {
htmlTrackElement.label = options.label;
}
if (options.language || options.srclang) {
htmlTrackElement.srclang = options.language || options.srclang;
}
if (options.default) {
htmlTrackElement.default = options.default;
}
if (options.id) {
htmlTrackElement.id = options.id;
}
if (options.src) {
htmlTrackElement.src = options.src;
}
return htmlTrackElement;
}
/**
* Creates a remote text track object and returns an html track element.
*
* @param {Object} options The object should contain values for
* kind, language, label, and src (location of the WebVTT file)
* @param {Boolean} [manualCleanup=true] if set to false, the TextTrack will be
* automatically removed from the video element whenever the source changes
* @return {HTMLTrackElement} An Html Track Element.
* This can be an emulated {@link HTMLTrackElement} or a native one.
* @deprecated The default value of the "manualCleanup" parameter will default
* to "false" in upcoming versions of Video.js
*/
addRemoteTextTrack(options, manualCleanup) {
const htmlTrackElement = super.addRemoteTextTrack(options, manualCleanup);
if (this.featuresNativeTextTracks) {
this.el().appendChild(htmlTrackElement);
}
return htmlTrackElement;
}
/**
* Remove remote `TextTrack` from `TextTrackList` object
*
* @param {TextTrack} track
* `TextTrack` object to remove
*/
removeRemoteTextTrack(track) {
super.removeRemoteTextTrack(track);
if (this.featuresNativeTextTracks) {
const tracks = this.$$('track');
let i = tracks.length;
while (i--) {
if (track === tracks[i] || track === tracks[i].track) {
this.el().removeChild(tracks[i]);
}
}
}
}
}
/* HTML5 Support Testing ---------------------------------------------------- */
if (Dom.isReal()) {
/**
* Element for testing browser HTML5 media capabilities
*
* @type {Element}
* @constant
* @private
*/
Html5.TEST_VID = document.createElement('video');
const track = document.createElement('track');
track.kind = 'captions';
track.srclang = 'en';
track.label = 'English';
Html5.TEST_VID.appendChild(track);
}
/**
* Check if HTML5 media is supported by this browser/device.
*
* @return {boolean}
* - True if HTML5 media is supported.
* - False if HTML5 media is not supported.
*/
Html5.isSupported = function() {
// IE9 with no Media Player is a LIAR! (#984)
try {
Html5.TEST_VID.volume = 0.5;
} catch (e) {
return false;
}
return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType);
};
/**
* Check if the volume can be changed in this browser/device.
* Volume cannot be changed in a lot of mobile devices.
* Specifically, it can't be changed from 1 on iOS.
*
* @return {boolean}
* - True if volume can be controlled
* - False otherwise
*/
Html5.canControlVolume = function() {
// IE will error if Windows Media Player not installed #3315
try {
const volume = Html5.TEST_VID.volume;
Html5.TEST_VID.volume = (volume / 2) + 0.1;
return volume !== Html5.TEST_VID.volume;
} catch (e) {
return false;
}
};
/**
* Check if the playback rate can be changed in this browser/device.
*
* @return {boolean}
* - True if playback rate can be controlled
* - False otherwise
*/
Html5.canControlPlaybackRate = function() {
// Playback rate API is implemented in Android Chrome, but doesn't do anything
// https://github.com/videojs/video.js/issues/3180
if (browser.IS_ANDROID && browser.IS_CHROME) {
return false;
}
// IE will error if Windows Media Player not installed #3315
try {
const playbackRate = Html5.TEST_VID.playbackRate;
Html5.TEST_VID.playbackRate = (playbackRate / 2) + 0.1;
return playbackRate !== Html5.TEST_VID.playbackRate;
} catch (e) {
return false;
}
};
/**
* Check to see if native `TextTrack`s are supported by this browser/device.
*
* @return {boolean}
* - True if native `TextTrack`s are supported.
* - False otherwise
*/
Html5.supportsNativeTextTracks = function() {
return browser.IS_ANY_SAFARI;
};
/**
* Check to see if native `VideoTrack`s are supported by this browser/device
*
* @return {boolean}
* - True if native `VideoTrack`s are supported.
* - False otherwise
*/
Html5.supportsNativeVideoTracks = function() {
return !!(Html5.TEST_VID && Html5.TEST_VID.videoTracks);
};
/**
* Check to see if native `AudioTrack`s are supported by this browser/device
*
* @return {boolean}
* - True if native `AudioTrack`s are supported.
* - False otherwise
*/
Html5.supportsNativeAudioTracks = function() {
return !!(Html5.TEST_VID && Html5.TEST_VID.audioTracks);
};
/**
* An array of events available on the Html5 tech.
*
* @private
* @type {Array}
*/
Html5.Events = [
'loadstart',
'suspend',
'abort',
'error',
'emptied',
'stalled',
'loadedmetadata',
'loadeddata',
'canplay',
'canplaythrough',
'playing',
'waiting',
'seeking',
'seeked',
'ended',
'durationchange',
'timeupdate',
'progress',
'play',
'pause',
'ratechange',
'volumechange'
];
/**
* Boolean indicating whether the `Tech` supports volume control.
*
* @type {boolean}
* @default {@link Html5.canControlVolume}
*/
Html5.prototype.featuresVolumeControl = Html5.canControlVolume();
/**
* Boolean indicating whether the `Tech` supports changing the speed at which the media
* plays. Examples:
* - Set player to play 2x (twice) as fast
* - Set player to play 0.5x (half) as fast
*
* @type {boolean}
* @default {@link Html5.canControlPlaybackRate}
*/
Html5.prototype.featuresPlaybackRate = Html5.canControlPlaybackRate();
/**
* Boolean indicating whether the `HTML5` tech currently supports the media element
* moving in the DOM. iOS breaks if you move the media element, so this is set this to
* false there. Everywhere else this should be true.
*
* @type {boolean}
* @default
*/
Html5.prototype.movingMediaElementInDOM = !browser.IS_IOS;
// TODO: Previous comment: No longer appears to be used. Can probably be removed.
// Is this true?
/**
* Boolean indicating whether the `HTML5` tech currently supports automatic media resize
* when going into fullscreen.
*
* @type {boolean}
* @default
*/
Html5.prototype.featuresFullscreenResize = true;
/**
* Boolean indicating whether the `HTML5` tech currently supports the progress event.
* If this is false, manual `progress` events will be triggred instead.
*
* @type {boolean}
* @default
*/
Html5.prototype.featuresProgressEvents = true;
/**
* Boolean indicating whether the `HTML5` tech currently supports the timeupdate event.
* If this is false, manual `timeupdate` events will be triggred instead.
*
* @default
*/
Html5.prototype.featuresTimeupdateEvents = true;
/**
* Boolean indicating whether the `HTML5` tech currently supports native `TextTrack`s.
*
* @type {boolean}
* @default {@link Html5.supportsNativeTextTracks}
*/
Html5.prototype.featuresNativeTextTracks = Html5.supportsNativeTextTracks();
/**
* Boolean indicating whether the `HTML5` tech currently supports native `VideoTrack`s.
*
* @type {boolean}
* @default {@link Html5.supportsNativeVideoTracks}
*/
Html5.prototype.featuresNativeVideoTracks = Html5.supportsNativeVideoTracks();
/**
* Boolean indicating whether the `HTML5` tech currently supports native `AudioTrack`s.
*
* @type {boolean}
* @default {@link Html5.supportsNativeAudioTracks}
*/
Html5.prototype.featuresNativeAudioTracks = Html5.supportsNativeAudioTracks();
// HTML5 Feature detection and Device Fixes --------------------------------- //
const canPlayType = Html5.TEST_VID && Html5.TEST_VID.constructor.prototype.canPlayType;
const mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
const mp4RE = /^video\/mp4/i;
Html5.patchCanPlayType = function() {
// Android 4.0 and above can play HLS to some extent but it reports being unable to do so
if (browser.ANDROID_VERSION >= 4.0 && !browser.IS_FIREFOX) {
Html5.TEST_VID.constructor.prototype.canPlayType = function(type) {
if (type && mpegurlRE.test(type)) {
return 'maybe';
}
return canPlayType.call(this, type);
};
// Override Android 2.2 and less canPlayType method which is broken
} else if (browser.IS_OLD_ANDROID) {
Html5.TEST_VID.constructor.prototype.canPlayType = function(type) {
if (type && mp4RE.test(type)) {
return 'maybe';
}
return canPlayType.call(this, type);
};
}
};
Html5.unpatchCanPlayType = function() {
const r = Html5.TEST_VID.constructor.prototype.canPlayType;
Html5.TEST_VID.constructor.prototype.canPlayType = canPlayType;
return r;
};
// by default, patch the media element
Html5.patchCanPlayType();
Html5.disposeMediaElement = function(el) {
if (!el) {
return;
}
if (el.parentNode) {
el.parentNode.removeChild(el);
}
// remove any child track or source nodes to prevent their loading
while (el.hasChildNodes()) {
el.removeChild(el.firstChild);
}
// remove any src reference. not setting `src=''` because that causes a warning
// in firefox
el.removeAttribute('src');
// force the media element to update its loading state by calling load()
// however IE on Windows 7N has a bug that throws an error so need a try/catch (#793)
if (typeof el.load === 'function') {
// wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
(function() {
try {
el.load();
} catch (e) {
// not supported
}
}());
}
};
Html5.resetMediaElement = function(el) {
if (!el) {
return;
}
const sources = el.querySelectorAll('source');
let i = sources.length;
while (i--) {
el.removeChild(sources[i]);
}
// remove any src reference.
// not setting `src=''` because that throws an error
el.removeAttribute('src');
if (typeof el.load === 'function') {
// wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
(function() {
try {
el.load();
} catch (e) {
// satisfy linter
}
}());
}
};
/* Native HTML5 element property wrapping ----------------------------------- */
// Wrap native properties with a getter
[
/**
* Get the value of `paused` from the media element. `paused` indicates whether the media element
* is currently paused or not.
*
* @method Html5#paused
* @return {boolean}
* The value of `paused` from the media element.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-paused}
*/
'paused',
/**
* Get the value of `currentTime` from the media element. `currentTime` indicates
* the current second that the media is at in playback.
*
* @method Html5#currentTime
* @return {number}
* The value of `currentTime` from the media element.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-currenttime}
*/
'currentTime',
/**
* Get the value of `buffered` from the media element. `buffered` is a `TimeRange`
* object that represents the parts of the media that are already downloaded and
* available for playback.
*
* @method Html5#buffered
* @return {TimeRange}
* The value of `buffered` from the media element.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-buffered}
*/
'buffered',
/**
* Get the value of `volume` from the media element. `volume` indicates
* the current playback volume of audio for a media. `volume` will be a value from 0
* (silent) to 1 (loudest and default).
*
* @method Html5#volume
* @return {number}
* The value of `volume` from the media element. Value will be between 0-1.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
*/
'volume',
/**
* Get the value of `muted` from the media element. `muted` indicates
* that the volume for the media should be set to silent. This does not actually change
* the `volume` attribute.
*
* @method Html5#muted
* @return {boolean}
* - True if the value of `volume` should be ignored and the audio set to silent.
* - False if the value of `volume` should be used.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
*/
'muted',
/**
* Get the value of `poster` from the media element. `poster` indicates
* that the url of an image file that can/will be shown when no media data is available.
*
* @method Html5#poster
* @return {string}
* The value of `poster` from the media element. Value will be a url to an
* image.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster}
*/
'poster',
/**
* Get the value of `preload` from the media element. `preload` indicates
* what should download before the media is interacted with. It can have the following
* values:
* - none: nothing should be downloaded
* - metadata: poster and the first few frames of the media may be downloaded to get
* media dimensions and other metadata
* - auto: allow the media and metadata for the media to be downloaded before
* interaction
*
* @method Html5#preload
* @return {string}
* The value of `preload` from the media element. Will be 'none', 'metadata',
* or 'auto'.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
*/
'preload',
/**
* Get the value of `autoplay` from the media element. `autoplay` indicates
* that the media should start to play as soon as the page is ready.
*
* @method Html5#autoplay
* @return {boolean}
* - The value of `autoplay` from the media element.
* - True indicates that the media should start as soon as the page loads.
* - False indicates that the media should not start as soon as the page loads.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
*/
'autoplay',
/**
* Get the value of `controls` from the media element. `controls` indicates
* whether the native media controls should be shown or hidden.
*
* @method Html5#controls
* @return {boolean}
* - The value of `controls` from the media element.
* - True indicates that native controls should be showing.
* - False indicates that native controls should be hidden.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-controls}
*/
'controls',
/**
* Get the value of `loop` from the media element. `loop` indicates
* that the media should return to the start of the media and continue playing once
* it reaches the end.
*
* @method Html5#loop
* @return {boolean}
* - The value of `loop` from the media element.
* - True indicates that playback should seek back to start once
* the end of a media is reached.
* - False indicates that playback should not loop back to the start when the
* end of the media is reached.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
*/
'loop',
/**
* Get the value of the `error` from the media element. `error` indicates any
* MediaError that may have occured during playback. If error returns null there is no
* current error.
*
* @method Html5#error
* @return {MediaError|null}
* The value of `error` from the media element. Will be `MediaError` if there
* is a current error and null otherwise.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-error}
*/
'error',
/**
* Get the value of `seeking` from the media element. `seeking` indicates whether the
* media is currently seeking to a new position or not.
*
* @method Html5#seeking
* @return {boolean}
* - The value of `seeking` from the media element.
* - True indicates that the media is currently seeking to a new position.
* - Flase indicates that the media is not seeking to a new position at this time.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking}
*/
'seeking',
/**
* Get the value of `seekable` from the media element. `seekable` returns a
* `TimeRange` object indicating ranges of time that can currently be `seeked` to.
*
* @method Html5#seekable
* @return {TimeRange}
* The value of `seekable` from the media element. A `TimeRange` object
* indicating the current ranges of time that can be seeked to.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seekable}
*/
'seekable',
/**
* Get the value of `ended` from the media element. `ended` indicates whether
* the media has reached the end or not.
*
* @method Html5#ended
* @return {boolean}
* - The value of `ended` from the media element.
* - True indicates that the media has ended.
* - False indicates that the media has not ended.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-ended}
*/
'ended',
/**
* Get the value of `defaultMuted` from the media element. `defaultMuted` indicates
* whether the media should start muted or not. Only changes the default state of the
* media. `muted` and `defaultMuted` can have different values. `muted` indicates the
* current state.
*
* @method Html5#defaultMuted
* @return {boolean}
* - The value of `defaultMuted` from the media element.
* - True indicates that the media should start muted.
* - False indicates that the media should not start muted
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
*/
'defaultMuted',
/**
* Get the value of `playbackRate` from the media element. `playbackRate` indicates
* the rate at which the media is currently playing back. Examples:
* - if playbackRate is set to 2, media will play twice as fast.
* - if playbackRate is set to 0.5, media will play half as fast.
*
* @method Html5#playbackRate
* @return {number}
* The value of `playbackRate` from the media element. A number indicating
* the current playback speed of the media, where 1 is normal speed.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
*/
'playbackRate',
/**
* Get the value of `played` from the media element. `played` returns a `TimeRange`
* object representing points in the media timeline that have been played.
*
* @method Html5#played
* @return {TimeRange}
* The value of `played` from the media element. A `TimeRange` object indicating
* the ranges of time that have been played.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-played}
*/
'played',
/**
* Get the value of `networkState` from the media element. `networkState` indicates
* the current network state. It returns an enumeration from the following list:
* - 0: NETWORK_EMPTY
* - 1: NEWORK_IDLE
* - 2: NETWORK_LOADING
* - 3: NETWORK_NO_SOURCE
*
* @method Html5#networkState
* @return {number}
* The value of `networkState` from the media element. This will be a number
* from the list in the description.
*
* @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate}
*/
'networkState',
/**
* Get the value of `readyState` from the media element. `readyState` indicates
* the current state of the media element. It returns an enumeration from the
* following list:
* - 0: HAVE_NOTHING
* - 1: HAVE_METADATA
* - 2: HAVE_CURRENT_DATA
* - 3: HAVE_FUTURE_DATA
* - 4: HAVE_ENOUGH_DATA
*
* @method Html5#readyState
* @return {number}
* The value of `readyState` from the media element. This will be a number
* from the list in the description.
*
* @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states}
*/
'readyState',
/**
* Get the value of `videoWidth` from the video element. `videoWidth` indicates
* the current width of the video in css pixels.
*
* @method Html5#videoWidth
* @return {number}
* The value of `videoWidth` from the video element. This will be a number
* in css pixels.
*
* @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
*/
'videoWidth',
/**
* Get the value of `videoHeight` from the video element. `videoHeigth` indicates
* the current height of the video in css pixels.
*
* @method Html5#videoHeight
* @return {number}
* The value of `videoHeight` from the video element. This will be a number
* in css pixels.
*
* @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
*/
'videoHeight'
].forEach(function(prop) {
Html5.prototype[prop] = function() {
return this.el_[prop];
};
});
// Wrap native properties with a setter in this format:
// set + toTitleCase(name)
[
/**
* Set the value of `volume` on the media element. `volume` indicates the current
* audio level as a percentage in decimal form. This means that 1 is 100%, 0.5 is 50%, and
* so on.
*
* @method Html5#setVolume
* @param {number} percentAsDecimal
* The volume percent as a decimal. Valid range is from 0-1.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
*/
'volume',
/**
* Set the value of `muted` on the media element. `muted` indicates the current
* audio level should be silent.
*
* @method Html5#setMuted
* @param {boolean} muted
* - True if the audio should be set to silent
* - False otherwise
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
*/
'muted',
/**
* Set the value of `src` on the media element. `src` indicates the current
* {@link Tech~SourceObject} for the media.
*
* @method Html5#setSrc
* @param {Tech~SourceObject} src
* The source object to set as the current source.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-src}
*/
'src',
/**
* Set the value of `poster` on the media element. `poster` is the url to
* an image file that can/will be shown when no media data is available.
*
* @method Html5#setPoster
* @param {string} poster
* The url to an image that should be used as the `poster` for the media
* element.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-poster}
*/
'poster',
/**
* Set the value of `preload` on the media element. `preload` indicates
* what should download before the media is interacted with. It can have the following
* values:
* - none: nothing should be downloaded
* - metadata: poster and the first few frames of the media may be downloaded to get
* media dimensions and other metadata
* - auto: allow the media and metadata for the media to be downloaded before
* interaction
*
* @method Html5#setPreload
* @param {string} preload
* The value of `preload` to set on the media element. Must be 'none', 'metadata',
* or 'auto'.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
*/
'preload',
/**
* Set the value of `autoplay` on the media element. `autoplay` indicates
* that the media should start to play as soon as the page is ready.
*
* @method Html5#setAutoplay
* @param {boolean} autoplay
* - True indicates that the media should start as soon as the page loads.
* - False indicates that the media should not start as soon as the page loads.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
*/
'autoplay',
/**
* Set the value of `loop` on the media element. `loop` indicates
* that the media should return to the start of the media and continue playing once
* it reaches the end.
*
* @method Html5#setLoop
* @param {boolean} loop
* - True indicates that playback should seek back to start once
* the end of a media is reached.
* - False indicates that playback should not loop back to the start when the
* end of the media is reached.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
*/
'loop',
/**
* Set the value of `playbackRate` on the media element. `playbackRate` indicates
* the rate at which the media should play back. Examples:
* - if playbackRate is set to 2, media will play twice as fast.
* - if playbackRate is set to 0.5, media will play half as fast.
*
* @method Html5#setPlaybackRate
* @return {number}
* The value of `playbackRate` from the media element. A number indicating
* the current playback speed of the media, where 1 is normal speed.
*
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
*/
'playbackRate'
].forEach(function(prop) {
Html5.prototype['set' + toTitleCase(prop)] = function(v) {
this.el_[prop] = v;
};
});
// wrap native functions with a function
[
/**
* A wrapper around the media elements `pause` function. This will call the `HTML5`
* media elements `pause` function.
*
* @method Html5#pause
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-pause}
*/
'pause',
/**
* A wrapper around the media elements `load` function. This will call the `HTML5`s
* media element `load` function.
*
* @method Html5#load
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load}
*/
'load'
].forEach(function(prop) {
Html5.prototype[prop] = function() {
return this.el_[prop]();
};
});
Tech.withSourceHandlers(Html5);
/**
* Native source handler for Html5, simply passes the source to the media element.
*
* @proprety {Tech~SourceObject} source
* The source object
*
* @proprety {Html5} tech
* The instance of the HTML5 tech.
*/
Html5.nativeSourceHandler = {};
/**
* Check if the media element can play the given mime type.
*
* @param {string} type
* The mimetype to check
*
* @return {string}
* 'probably', 'maybe', or '' (empty string)
*/
Html5.nativeSourceHandler.canPlayType = function(type) {
// IE9 on Windows 7 without MediaPlayer throws an error here
// https://github.com/videojs/video.js/issues/519
try {
return Html5.TEST_VID.canPlayType(type);
} catch (e) {
return '';
}
};
/**
* Check if the media element can handle a source natively.
*
* @param {Tech~SourceObject} source
* The source object
*
* @param {Object} [options]
* Options to be passed to the tech.
*
* @return {string}
* 'probably', 'maybe', or '' (empty string).
*/
Html5.nativeSourceHandler.canHandleSource = function(source, options) {
// If a type was provided we should rely on that
if (source.type) {
return Html5.nativeSourceHandler.canPlayType(source.type);
// If no type, fall back to checking 'video/[EXTENSION]'
} else if (source.src) {
const ext = Url.getFileExtension(source.src);
return Html5.nativeSourceHandler.canPlayType(`video/${ext}`);
}
return '';
};
/**
* Pass the source to the native media element.
*
* @param {Tech~SourceObject} source
* The source object
*
* @param {Html5} tech
* The instance of the Html5 tech
*
* @param {Object} [options]
* The options to pass to the source
*/
Html5.nativeSourceHandler.handleSource = function(source, tech, options) {
tech.setSrc(source.src);
};
/**
* A noop for the native dispose function, as cleanup is not needed.
*/
Html5.nativeSourceHandler.dispose = function() {};
// Register the native source handler
Html5.registerSourceHandler(Html5.nativeSourceHandler);
Component.registerComponent('Html5', Html5);
Tech.registerTech('Html5', Html5);
export default Html5;