/** * @file tech.js */ import Component from '../component'; import * as Fn from '../utils/fn.js'; import log from '../utils/log.js'; import { createTimeRange } from '../utils/time.js'; import { bufferedPercent } from '../utils/buffer.js'; import MediaError from '../media-error.js'; import window from 'global/window'; import document from 'global/document'; import {isPlain, merge} from '../utils/obj'; import * as TRACK_TYPES from '../tracks/track-types'; import {toTitleCase, toLowerCase} from '../utils/str.js'; import vtt from 'videojs-vtt.js'; import * as Guid from '../utils/guid.js'; /** * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string * that just contains the src url alone. * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};` * `var SourceString = 'http://example.com/some-video.mp4';` * * @typedef {Object|string} SourceObject * * @property {string} src * The url to the source * * @property {string} type * The mime type of the source */ /** * A function used by {@link Tech} to create a new {@link TextTrack}. * * @private * * @param {Tech} self * An instance of the Tech class. * * @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 * * @param {Object} [options={}] * An object with additional text track options * * @return {TextTrack} * The text track that was created. */ function createTrackHelper(self, kind, label, language, options = {}) { const tracks = self.textTracks(); options.kind = kind; if (label) { options.label = label; } if (language) { options.language = language; } options.tech = self; const track = new TRACK_TYPES.ALL.text.TrackClass(options); tracks.addTrack(track); return track; } /** * This is the base class for media playback technology controllers, such as * {@link HTML5} * * @extends Component */ class Tech extends Component { /** * Create an instance of this Tech. * * @param {Object} [options] * The key/value store of player options. * * @param {Function} [ready] * Callback function to call when the `HTML5` Tech is ready. */ constructor(options = {}, ready = function() {}) { // we don't want the tech to report user activity automatically. // This is done manually in addControlsListeners options.reportTouchActivity = false; super(null, options, ready); this.onDurationChange_ = (e) => this.onDurationChange(e); this.trackProgress_ = (e) => this.trackProgress(e); this.trackCurrentTime_ = (e) => this.trackCurrentTime(e); this.stopTrackingCurrentTime_ = (e) => this.stopTrackingCurrentTime(e); this.disposeSourceHandler_ = (e) => this.disposeSourceHandler(e); this.queuedHanders_ = new Set(); // keep track of whether the current source has played at all to // implement a very limited played() this.hasStarted_ = false; this.on('playing', function() { this.hasStarted_ = true; }); this.on('loadstart', function() { this.hasStarted_ = false; }); TRACK_TYPES.ALL.names.forEach((name) => { const props = TRACK_TYPES.ALL[name]; if (options && options[props.getterName]) { this[props.privateName] = options[props.getterName]; } }); // Manually track progress in cases where the browser/tech doesn't report it. if (!this.featuresProgressEvents) { this.manualProgressOn(); } // Manually track timeupdates in cases where the browser/tech doesn't report it. if (!this.featuresTimeupdateEvents) { this.manualTimeUpdatesOn(); } ['Text', 'Audio', 'Video'].forEach((track) => { if (options[`native${track}Tracks`] === false) { this[`featuresNative${track}Tracks`] = false; } }); if (options.nativeCaptions === false || options.nativeTextTracks === false) { this.featuresNativeTextTracks = false; } else if (options.nativeCaptions === true || options.nativeTextTracks === true) { this.featuresNativeTextTracks = true; } if (!this.featuresNativeTextTracks) { this.emulateTextTracks(); } this.preloadTextTracks = options.preloadTextTracks !== false; this.autoRemoteTextTracks_ = new TRACK_TYPES.ALL.text.ListClass(); this.initTrackListeners(); // Turn on component tap events only if not using native controls if (!options.nativeControlsForTouch) { this.emitTapEvents(); } if (this.constructor) { this.name_ = this.constructor.name || 'Unknown Tech'; } } /** * A special function to trigger source set in a way that will allow player * to re-trigger if the player or tech are not ready yet. * * @fires Tech#sourceset * @param {string} src The source string at the time of the source changing. */ triggerSourceset(src) { if (!this.isReady_) { // on initial ready we have to trigger source set // 1ms after ready so that player can watch for it. this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1)); } /** * Fired when the source is set on the tech causing the media element * to reload. * * @see {@link Player#event:sourceset} * @event Tech#sourceset * @type {Event} */ this.trigger({ src, type: 'sourceset' }); } /* Fallbacks for unsupported event types ================================================================================ */ /** * Polyfill the `progress` event for browsers that don't support it natively. * * @see {@link Tech#trackProgress} */ manualProgressOn() { this.on('durationchange', this.onDurationChange_); this.manualProgress = true; // Trigger progress watching when a source begins loading this.one('ready', this.trackProgress_); } /** * Turn off the polyfill for `progress` events that was created in * {@link Tech#manualProgressOn} */ manualProgressOff() { this.manualProgress = false; this.stopTrackingProgress(); this.off('durationchange', this.onDurationChange_); } /** * This is used to trigger a `progress` event when the buffered percent changes. It * sets an interval function that will be called every 500 milliseconds to check if the * buffer end percent has changed. * * > This function is called by {@link Tech#manualProgressOn} * * @param {Event} event * The `ready` event that caused this to run. * * @listens Tech#ready * @fires Tech#progress */ trackProgress(event) { this.stopTrackingProgress(); this.progressInterval = this.setInterval(Fn.bind_(this, function() { // Don't trigger unless buffered amount is greater than last time const numBufferedPercent = this.bufferedPercent(); if (this.bufferedPercent_ !== numBufferedPercent) { /** * See {@link Player#progress} * * @event Tech#progress * @type {Event} */ this.trigger('progress'); } this.bufferedPercent_ = numBufferedPercent; if (numBufferedPercent === 1) { this.stopTrackingProgress(); } }), 500); } /** * Update our internal duration on a `durationchange` event by calling * {@link Tech#duration}. * * @param {Event} event * The `durationchange` event that caused this to run. * * @listens Tech#durationchange */ onDurationChange(event) { this.duration_ = this.duration(); } /** * Get and create a `TimeRange` object for buffering. * * @return { import('../utils/time').TimeRange } * The time range object that was created. */ buffered() { return createTimeRange(0, 0); } /** * Get the percentage of the current video that is currently buffered. * * @return {number} * A number from 0 to 1 that represents the decimal percentage of the * video that is buffered. * */ bufferedPercent() { return bufferedPercent(this.buffered(), this.duration_); } /** * Turn off the polyfill for `progress` events that was created in * {@link Tech#manualProgressOn} * Stop manually tracking progress events by clearing the interval that was set in * {@link Tech#trackProgress}. */ stopTrackingProgress() { this.clearInterval(this.progressInterval); } /** * Polyfill the `timeupdate` event for browsers that don't support it. * * @see {@link Tech#trackCurrentTime} */ manualTimeUpdatesOn() { this.manualTimeUpdates = true; this.on('play', this.trackCurrentTime_); this.on('pause', this.stopTrackingCurrentTime_); } /** * Turn off the polyfill for `timeupdate` events that was created in * {@link Tech#manualTimeUpdatesOn} */ manualTimeUpdatesOff() { this.manualTimeUpdates = false; this.stopTrackingCurrentTime(); this.off('play', this.trackCurrentTime_); this.off('pause', this.stopTrackingCurrentTime_); } /** * Sets up an interval function to track current time and trigger `timeupdate` every * 250 milliseconds. * * @listens Tech#play * @triggers Tech#timeupdate */ trackCurrentTime() { if (this.currentTimeInterval) { this.stopTrackingCurrentTime(); } this.currentTimeInterval = this.setInterval(function() { /** * Triggered at an interval of 250ms to indicated that time is passing in the video. * * @event Tech#timeupdate * @type {Event} */ this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 }, 250); } /** * Stop the interval function created in {@link Tech#trackCurrentTime} so that the * `timeupdate` event is no longer triggered. * * @listens {Tech#pause} */ stopTrackingCurrentTime() { this.clearInterval(this.currentTimeInterval); // #1002 - if the video ends right before the next timeupdate would happen, // the progress bar won't make it all the way to the end this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); } /** * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList}, * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech. * * @fires Component#dispose */ dispose() { // clear out all tracks because we can't reuse them between techs this.clearTracks(TRACK_TYPES.NORMAL.names); // Turn off any manual progress or timeupdate tracking if (this.manualProgress) { this.manualProgressOff(); } if (this.manualTimeUpdates) { this.manualTimeUpdatesOff(); } super.dispose(); } /** * Clear out a single `TrackList` or an array of `TrackLists` given their names. * * > Note: Techs without source handlers should call this between sources for `video` * & `audio` tracks. You don't want to use them between tracks! * * @param {string[]|string} types * TrackList names to clear, valid names are `video`, `audio`, and * `text`. */ clearTracks(types) { types = [].concat(types); // clear out all tracks because we can't reuse them between techs types.forEach((type) => { const list = this[`${type}Tracks`]() || []; let i = list.length; while (i--) { const track = list[i]; if (type === 'text') { this.removeRemoteTextTrack(track); } list.removeTrack(track); } }); } /** * Remove any TextTracks added via addRemoteTextTrack that are * flagged for automatic garbage collection */ cleanupAutoTextTracks() { const list = this.autoRemoteTextTracks_ || []; let i = list.length; while (i--) { const track = list[i]; this.removeRemoteTextTrack(track); } } /** * Reset the tech, which will removes all sources and reset the internal readyState. * * @abstract */ reset() {} /** * Get the value of `crossOrigin` from the tech. * * @abstract * * @see {Html5#crossOrigin} */ crossOrigin() {} /** * Set the value of `crossOrigin` on the tech. * * @abstract * * @param {string} crossOrigin the crossOrigin value * @see {Html5#setCrossOrigin} */ setCrossOrigin() {} /** * Get or set an error on the Tech. * * @param {MediaError} [err] * Error to set on the Tech * * @return {MediaError|null} * The current error object on the tech, or null if there isn't one. */ error(err) { if (err !== undefined) { this.error_ = new MediaError(err); this.trigger('error'); } return this.error_; } /** * Returns the `TimeRange`s that have been played through for the current source. * * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`. * It only checks whether the source has played at all or not. * * @return { import('../utils/time').TimeRange } * - A single time range if this video has played * - An empty set of ranges if not. */ played() { if (this.hasStarted_) { return createTimeRange(0, 0); } return createTimeRange(); } /** * Start playback * * @abstract * * @see {Html5#play} */ play() {} /** * Set whether we are scrubbing or not * * @abstract * @param {boolean} _isScrubbing * - true for we are currently scrubbing * - false for we are no longer scrubbing * * @see {Html5#setScrubbing} */ setScrubbing(_isScrubbing) {} /** * Get whether we are scrubbing or not * * @abstract * * @see {Html5#scrubbing} */ scrubbing() {} /** * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was * previously called. * * @param {number} _seconds * Set the current time of the media to this. * @fires Tech#timeupdate */ setCurrentTime(_seconds) { // improve the accuracy of manual timeupdates if (this.manualTimeUpdates) { /** * A manual `timeupdate` event. * * @event Tech#timeupdate * @type {Event} */ this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); } } /** * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and * {@link TextTrackList} events. * * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`. * * @fires Tech#audiotrackchange * @fires Tech#videotrackchange * @fires Tech#texttrackchange */ initTrackListeners() { /** * Triggered when tracks are added or removed on the Tech {@link AudioTrackList} * * @event Tech#audiotrackchange * @type {Event} */ /** * Triggered when tracks are added or removed on the Tech {@link VideoTrackList} * * @event Tech#videotrackchange * @type {Event} */ /** * Triggered when tracks are added or removed on the Tech {@link TextTrackList} * * @event Tech#texttrackchange * @type {Event} */ TRACK_TYPES.NORMAL.names.forEach((name) => { const props = TRACK_TYPES.NORMAL[name]; const trackListChanges = () => { this.trigger(`${name}trackchange`); }; const tracks = this[props.getterName](); tracks.addEventListener('removetrack', trackListChanges); tracks.addEventListener('addtrack', trackListChanges); this.on('dispose', () => { tracks.removeEventListener('removetrack', trackListChanges); tracks.removeEventListener('addtrack', trackListChanges); }); }); } /** * Emulate TextTracks using vtt.js if necessary * * @fires Tech#vttjsloaded * @fires Tech#vttjserror */ addWebVttScript_() { if (window.WebVTT) { return; } // Initially, Tech.el_ is a child of a dummy-div wait until the Component system // signals that the Tech is ready at which point Tech.el_ is part of the DOM // before inserting the WebVTT script if (document.body.contains(this.el())) { // load via require if available and vtt.js script location was not passed in // as an option. novtt builds will turn the above require call into an empty object // which will cause this if check to always fail. if (!this.options_['vtt.js'] && isPlain(vtt) && Object.keys(vtt).length > 0) { this.trigger('vttjsloaded'); return; } // load vtt.js via the script location option or the cdn of no location was // passed in const script = document.createElement('script'); script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js'; script.onload = () => { /** * Fired when vtt.js is loaded. * * @event Tech#vttjsloaded * @type {Event} */ this.trigger('vttjsloaded'); }; script.onerror = () => { /** * Fired when vtt.js was not loaded due to an error * * @event Tech#vttjsloaded * @type {Event} */ this.trigger('vttjserror'); }; this.on('dispose', () => { script.onload = null; script.onerror = null; }); // but have not loaded yet and we set it to true before the inject so that // we don't overwrite the injected window.WebVTT if it loads right away window.WebVTT = true; this.el().parentNode.appendChild(script); } else { this.ready(this.addWebVttScript_); } } /** * Emulate texttracks * */ emulateTextTracks() { const tracks = this.textTracks(); const remoteTracks = this.remoteTextTracks(); const handleAddTrack = (e) => tracks.addTrack(e.track); const handleRemoveTrack = (e) => tracks.removeTrack(e.track); remoteTracks.on('addtrack', handleAddTrack); remoteTracks.on('removetrack', handleRemoveTrack); this.addWebVttScript_(); const updateDisplay = () => this.trigger('texttrackchange'); const textTracksChanges = () => { updateDisplay(); for (let i = 0; i < tracks.length; i++) { const track = tracks[i]; track.removeEventListener('cuechange', updateDisplay); if (track.mode === 'showing') { track.addEventListener('cuechange', updateDisplay); } } }; textTracksChanges(); tracks.addEventListener('change', textTracksChanges); tracks.addEventListener('addtrack', textTracksChanges); tracks.addEventListener('removetrack', textTracksChanges); this.on('dispose', function() { remoteTracks.off('addtrack', handleAddTrack); remoteTracks.off('removetrack', handleRemoveTrack); tracks.removeEventListener('change', textTracksChanges); tracks.removeEventListener('addtrack', textTracksChanges); tracks.removeEventListener('removetrack', textTracksChanges); for (let i = 0; i < tracks.length; i++) { const track = tracks[i]; track.removeEventListener('cuechange', updateDisplay); } }); } /** * 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 (!kind) { throw new Error('TextTrack kind is required but was not provided'); } return createTrackHelper(this, kind, label, language); } /** * Create an emulated TextTrack for use by addRemoteTextTrack * * This is intended to be overridden by classes that inherit from * Tech in order to create native or custom TextTracks. * * @param {Object} options * The object should contain the options to initialize 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. * * @return {HTMLTrackElement} * The track element that gets created. */ createRemoteTextTrack(options) { const track = merge(options, { tech: this }); return new TRACK_TYPES.REMOTE.remoteTextEl.TrackClass(track); } /** * Creates a remote text track object and returns an html track element. * * > Note: This can be an emulated {@link HTMLTrackElement} or a native one. * * @param {Object} options * See {@link Tech#createRemoteTextTrack} for more detailed properties. * * @param {boolean} [manualCleanup=false] * - When false: the TextTrack will be automatically removed from the video * element whenever the source changes * - When True: The TextTrack will have to be cleaned up manually * * @return {HTMLTrackElement} * An Html Track Element. * */ addRemoteTextTrack(options = {}, manualCleanup) { const htmlTrackElement = this.createRemoteTextTrack(options); if (typeof manualCleanup !== 'boolean') { manualCleanup = false; } // store HTMLTrackElement and TextTrack to remote list this.remoteTextTrackEls().addTrackElement_(htmlTrackElement); this.remoteTextTracks().addTrack(htmlTrackElement.track); if (manualCleanup === false) { // create the TextTrackList if it doesn't exist this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track)); } return htmlTrackElement; } /** * Remove a remote text track from the remote `TextTrackList`. * * @param {TextTrack} track * `TextTrack` to remove from the `TextTrackList` */ removeRemoteTextTrack(track) { const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track); // remove HTMLTrackElement and TextTrack from remote list this.remoteTextTrackEls().removeTrackElement_(trackElement); this.remoteTextTracks().removeTrack(track); this.autoRemoteTextTracks_.removeTrack(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} * An object with supported media playback quality metrics * * @abstract */ getVideoPlaybackQuality() { return {}; } /** * Attempt to 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. * * @see [Spec]{@link https://wicg.github.io/picture-in-picture} * * @return {Promise|undefined} * A promise with a Picture-in-Picture window if the browser supports * Promises (or one was passed in as an option). It returns undefined * otherwise. * * @abstract */ requestPictureInPicture() { return Promise.reject(); } /** * A method to check for the value of the 'disablePictureInPicture' <video> property. * Defaults to true, as it should be considered disabled if the tech does not support pip * * @abstract */ disablePictureInPicture() { return true; } /** * A method to set or unset the 'disablePictureInPicture' <video> property. * * @abstract */ setDisablePictureInPicture() {} /** * A fallback implementation of requestVideoFrameCallback using requestAnimationFrame * * @param {function} cb * @return {number} request id */ requestVideoFrameCallback(cb) { const id = Guid.newGUID(); if (!this.isReady_ || this.paused()) { this.queuedHanders_.add(id); this.one('playing', () => { if (this.queuedHanders_.has(id)) { this.queuedHanders_.delete(id); cb(); } }); } else { this.requestNamedAnimationFrame(id, cb); } return id; } /** * A fallback implementation of cancelVideoFrameCallback * * @param {number} id id of callback to be cancelled */ cancelVideoFrameCallback(id) { if (this.queuedHanders_.has(id)) { this.queuedHanders_.delete(id); } else { this.cancelNamedAnimationFrame(id); } } /** * A method to set a poster from a `Tech`. * * @abstract */ setPoster() {} /** * A method to check for the presence of the 'playsinline' <video> attribute. * * @abstract */ playsinline() {} /** * A method to set or unset the 'playsinline' <video> attribute. * * @abstract */ setPlaysinline() {} /** * Attempt to force override of native audio tracks. * * @param {boolean} override - If set to true native audio will be overridden, * otherwise native audio will potentially be used. * * @abstract */ overrideNativeAudioTracks(override) {} /** * Attempt to force override of native video tracks. * * @param {boolean} override - If set to true native video will be overridden, * otherwise native video will potentially be used. * * @abstract */ overrideNativeVideoTracks(override) {} /** * Check if the tech can support the given mime-type. * * The base tech does not support any type, but source handlers might * overwrite this. * * @param {string} _type * The mimetype to check for support * * @return {string} * 'probably', 'maybe', or empty string * * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType} * * @abstract */ canPlayType(_type) { return ''; } /** * Check if the type is supported by this tech. * * The base tech does not support any type, but source handlers might * overwrite this. * * @param {string} _type * The media type to check * @return {string} Returns the native video element's response */ static canPlayType(_type) { return ''; } /** * Check if the tech can support the given source * * @param {Object} srcObj * The source object * @param {Object} options * The options passed to the tech * @return {string} 'probably', 'maybe', or '' (empty string) */ static canPlaySource(srcObj, options) { return Tech.canPlayType(srcObj.type); } /* * Return whether the argument is a Tech or not. * Can be passed either a Class like `Html5` or a instance like `player.tech_` * * @param {Object} component * The item to check * * @return {boolean} * Whether it is a tech or not * - True if it is a tech * - False if it is not */ static isTech(component) { return component.prototype instanceof Tech || component instanceof Tech || component === Tech; } /** * Registers a `Tech` into a shared list for videojs. * * @param {string} name * Name of the `Tech` to register. * * @param {Object} tech * The `Tech` class to register. */ static registerTech(name, tech) { if (!Tech.techs_) { Tech.techs_ = {}; } if (!Tech.isTech(tech)) { throw new Error(`Tech ${name} must be a Tech`); } if (!Tech.canPlayType) { throw new Error('Techs must have a static canPlayType method on them'); } if (!Tech.canPlaySource) { throw new Error('Techs must have a static canPlaySource method on them'); } name = toTitleCase(name); Tech.techs_[name] = tech; Tech.techs_[toLowerCase(name)] = tech; if (name !== 'Tech') { // camel case the techName for use in techOrder Tech.defaultTechOrder_.push(name); } return tech; } /** * Get a `Tech` from the shared list by name. * * @param {string} name * `camelCase` or `TitleCase` name of the Tech to get * * @return {Tech|undefined} * The `Tech` or undefined if there was no tech with the name requested. */ static getTech(name) { if (!name) { return; } if (Tech.techs_ && Tech.techs_[name]) { return Tech.techs_[name]; } name = toTitleCase(name); if (window && window.videojs && window.videojs[name]) { log.warn(`The ${name} tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)`); return window.videojs[name]; } } } /** * Get the {@link VideoTrackList} * * @returns {VideoTrackList} * @method Tech.prototype.videoTracks */ /** * Get the {@link AudioTrackList} * * @returns {AudioTrackList} * @method Tech.prototype.audioTracks */ /** * Get the {@link TextTrackList} * * @returns {TextTrackList} * @method Tech.prototype.textTracks */ /** * Get the remote element {@link TextTrackList} * * @returns {TextTrackList} * @method Tech.prototype.remoteTextTracks */ /** * Get the remote element {@link HtmlTrackElementList} * * @returns {HtmlTrackElementList} * @method Tech.prototype.remoteTextTrackEls */ TRACK_TYPES.ALL.names.forEach(function(name) { const props = TRACK_TYPES.ALL[name]; Tech.prototype[props.getterName] = function() { this[props.privateName] = this[props.privateName] || new props.ListClass(); return this[props.privateName]; }; }); /** * List of associated text tracks * * @type {TextTrackList} * @private * @property Tech#textTracks_ */ /** * List of associated audio tracks. * * @type {AudioTrackList} * @private * @property Tech#audioTracks_ */ /** * List of associated video tracks. * * @type {VideoTrackList} * @private * @property Tech#videoTracks_ */ /** * Boolean indicating whether the `Tech` supports volume control. * * @type {boolean} * @default */ Tech.prototype.featuresVolumeControl = true; /** * Boolean indicating whether the `Tech` supports muting volume. * * @type {boolean} * @default */ Tech.prototype.featuresMuteControl = true; /** * Boolean indicating whether the `Tech` supports fullscreen resize control. * Resizing plugins using request fullscreen reloads the plugin * * @type {boolean} * @default */ Tech.prototype.featuresFullscreenResize = false; /** * Boolean indicating whether the `Tech` supports changing the speed at which the video * plays. Examples: * - Set player to play 2x (twice) as fast * - Set player to play 0.5x (half) as fast * * @type {boolean} * @default */ Tech.prototype.featuresPlaybackRate = false; /** * Boolean indicating whether the `Tech` supports the `progress` event. * This will be used to determine if {@link Tech#manualProgressOn} should be called. * * @type {boolean} * @default */ Tech.prototype.featuresProgressEvents = false; /** * Boolean indicating whether the `Tech` supports the `sourceset` event. * * A tech should set this to `true` and then use {@link Tech#triggerSourceset} * to trigger a {@link Tech#event:sourceset} at the earliest time after getting * a new source. * * @type {boolean} * @default */ Tech.prototype.featuresSourceset = false; /** * Boolean indicating whether the `Tech` supports the `timeupdate` event. * This will be used to determine if {@link Tech#manualTimeUpdates} should be called. * * @type {boolean} * @default */ Tech.prototype.featuresTimeupdateEvents = false; /** * Boolean indicating whether the `Tech` supports the native `TextTrack`s. * This will help us integrate with native `TextTrack`s if the browser supports them. * * @type {boolean} * @default */ Tech.prototype.featuresNativeTextTracks = false; /** * Boolean indicating whether the `Tech` supports `requestVideoFrameCallback`. * * @type {boolean} * @default */ Tech.prototype.featuresVideoFrameCallback = false; /** * A functional mixin for techs that want to use the Source Handler pattern. * Source handlers are scripts for handling specific formats. * The source handler pattern is used for adaptive formats (HLS, DASH) that * manually load video data and feed it into a Source Buffer (Media Source Extensions) * Example: `Tech.withSourceHandlers.call(MyTech);` * * @param {Tech} _Tech * The tech to add source handler functions to. * * @mixes Tech~SourceHandlerAdditions */ Tech.withSourceHandlers = function(_Tech) { /** * Register a source handler * * @param {Function} handler * The source handler class * * @param {number} [index] * Register it at the following index */ _Tech.registerSourceHandler = function(handler, index) { let handlers = _Tech.sourceHandlers; if (!handlers) { handlers = _Tech.sourceHandlers = []; } if (index === undefined) { // add to the end of the list index = handlers.length; } handlers.splice(index, 0, handler); }; /** * Check if the tech can support the given type. Also checks the * Techs sourceHandlers. * * @param {string} type * The mimetype to check. * * @return {string} * 'probably', 'maybe', or '' (empty string) */ _Tech.canPlayType = function(type) { const handlers = _Tech.sourceHandlers || []; let can; for (let i = 0; i < handlers.length; i++) { can = handlers[i].canPlayType(type); if (can) { return can; } } return ''; }; /** * Returns the first source handler that supports the source. * * TODO: Answer question: should 'probably' be prioritized over 'maybe' * * @param {SourceObject} source * The source object * * @param {Object} options * The options passed to the tech * * @return {SourceHandler|null} * The first source handler that supports the source or null if * no SourceHandler supports the source */ _Tech.selectSourceHandler = function(source, options) { const handlers = _Tech.sourceHandlers || []; let can; for (let i = 0; i < handlers.length; i++) { can = handlers[i].canHandleSource(source, options); if (can) { return handlers[i]; } } return null; }; /** * Check if the tech can support the given source. * * @param {SourceObject} srcObj * The source object * * @param {Object} options * The options passed to the tech * * @return {string} * 'probably', 'maybe', or '' (empty string) */ _Tech.canPlaySource = function(srcObj, options) { const sh = _Tech.selectSourceHandler(srcObj, options); if (sh) { return sh.canHandleSource(srcObj, options); } return ''; }; /** * When using a source handler, prefer its implementation of * any function normally provided by the tech. */ const deferrable = [ 'seekable', 'seeking', 'duration' ]; /** * A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable * function if it exists, with a fallback to the Techs seekable function. * * @method _Tech.seekable */ /** * A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration * function if it exists, otherwise it will fallback to the techs duration function. * * @method _Tech.duration */ deferrable.forEach(function(fnName) { const originalFn = this[fnName]; if (typeof originalFn !== 'function') { return; } this[fnName] = function() { if (this.sourceHandler_ && this.sourceHandler_[fnName]) { return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments); } return originalFn.apply(this, arguments); }; }, _Tech.prototype); /** * Create a function for setting the source using a source object * and source handlers. * Should never be called unless a source handler was found. * * @param {SourceObject} source * A source object with src and type keys */ _Tech.prototype.setSource = function(source) { let sh = _Tech.selectSourceHandler(source, this.options_); if (!sh) { // Fall back to a native source handler when unsupported sources are // deliberately set if (_Tech.nativeSourceHandler) { sh = _Tech.nativeSourceHandler; } else { log.error('No source handler found for the current source.'); } } // Dispose any existing source handler this.disposeSourceHandler(); this.off('dispose', this.disposeSourceHandler_); if (sh !== _Tech.nativeSourceHandler) { this.currentSource_ = source; } this.sourceHandler_ = sh.handleSource(source, this, this.options_); this.one('dispose', this.disposeSourceHandler_); }; /** * Clean up any existing SourceHandlers and listeners when the Tech is disposed. * * @listens Tech#dispose */ _Tech.prototype.disposeSourceHandler = function() { // if we have a source and get another one // then we are loading something new // than clear all of our current tracks if (this.currentSource_) { this.clearTracks(['audio', 'video']); this.currentSource_ = null; } // always clean up auto-text tracks this.cleanupAutoTextTracks(); if (this.sourceHandler_) { if (this.sourceHandler_.dispose) { this.sourceHandler_.dispose(); } this.sourceHandler_ = null; } }; }; // The base Tech class needs to be registered as a Component. It is the only // Tech that can be registered as a Component. Component.registerComponent('Tech', Tech); Tech.registerTech('Tech', Tech); /** * A list of techs that should be added to techOrder on Players * * @private */ Tech.defaultTechOrder_ = []; export default Tech;