UNPKG

shaka-player

Version:
1,640 lines (1,374 loc) 88.9 kB
/** * @license * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ goog.provide('shaka.Player'); goog.require('goog.asserts'); goog.require('shaka.abr.SimpleAbrManager'); goog.require('shaka.log'); goog.require('shaka.media.DrmEngine'); goog.require('shaka.media.ManifestParser'); goog.require('shaka.media.MediaSourceEngine'); goog.require('shaka.media.Playhead'); goog.require('shaka.media.PlayheadObserver'); goog.require('shaka.media.SegmentReference'); goog.require('shaka.media.StreamingEngine'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.text.SimpleTextDisplayer'); goog.require('shaka.util.CancelableChain'); goog.require('shaka.util.ConfigUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.EventManager'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.FakeEventTarget'); goog.require('shaka.util.Functional'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MapUtils'); goog.require('shaka.util.PublicPromise'); goog.require('shaka.util.StreamUtils'); /** * Construct a Player. * * @param {!HTMLMediaElement} video Any existing TextTracks attached to this * element that were not created by Shaka will be disabled. A new TextTrack * may be created to display captions or subtitles. * @param {function(shaka.Player)=} opt_dependencyInjector Optional callback * which is called to inject mocks into the Player. Used for testing. * * @constructor * @struct * @implements {shaka.util.IDestroyable} * @extends {shaka.util.FakeEventTarget} * @export */ shaka.Player = function(video, opt_dependencyInjector) { shaka.util.FakeEventTarget.call(this); /** @private {boolean} */ this.destroyed_ = false; /** @private {HTMLMediaElement} */ this.video_ = video; /** * Only holds the visibility setting until a textDisplayer_ is created. * @private {boolean} */ this.textVisibility_ = false; /** @private {shakaExtern.TextDisplayer} */ this.textDisplayer_ = null; /** @private {shaka.util.EventManager} */ this.eventManager_ = new shaka.util.EventManager(); /** @private {shaka.net.NetworkingEngine} */ this.networkingEngine_ = null; /** @private {shaka.media.DrmEngine} */ this.drmEngine_ = null; /** @private {MediaSource} */ this.mediaSource_ = null; /** @private {shaka.media.MediaSourceEngine} */ this.mediaSourceEngine_ = null; /** @private {Promise} */ this.mediaSourceOpen_ = null; /** @private {shaka.media.Playhead} */ this.playhead_ = null; /** @private {shaka.media.PlayheadObserver} */ this.playheadObserver_ = null; /** @private {shaka.media.StreamingEngine} */ this.streamingEngine_ = null; /** @private {shakaExtern.ManifestParser} */ this.parser_ = null; /** @private {?shakaExtern.Manifest} */ this.manifest_ = null; /** @private {?string} */ this.manifestUri_ = null; /** @private {shakaExtern.AbrManager} */ this.abrManager_ = null; /** * Contains an ID for use with creating streams. The manifest parser should * start with small IDs, so this starts with a large one. * @private {number} */ this.nextExternalStreamId_ = 1e9; /** @private {!Array.<number>} */ this.loadingTextStreamIds_ = []; /** @private {boolean} */ this.buffering_ = false; /** @private {boolean} */ this.switchingPeriods_ = true; /** @private {shaka.util.CancelableChain} */ this.loadChain_ = null; /** @private {Promise} */ this.unloadChain_ = null; /** @private {?shakaExtern.Variant} */ this.deferredVariant_ = null; /** @private {boolean} */ this.deferredVariantClearBuffer_ = false; /** @private {?shakaExtern.Stream} */ this.deferredTextStream_ = null; /** @private {!Array.<shakaExtern.TimelineRegionInfo>} */ this.pendingTimelineRegions_ = []; /** * A map of Period number to a map of content type to stream id. * @private {!Object.<number, !Object.<string, number>>} */ this.activeStreamsByPeriod_ = {}; /** @private {?shakaExtern.PlayerConfiguration} */ this.config_ = this.defaultConfig_(); /** @private {{width: number, height: number}} */ this.maxHwRes_ = { width: Infinity, height: Infinity }; /** @private {shakaExtern.Stats} */ this.stats_ = this.getCleanStats_(); /** @private {number} */ this.lastTimeStatsUpdateTimestamp_ = 0; /** @private {string} */ this.currentAudioLanguage_ = this.config_.preferredAudioLanguage; /** @private {string} */ this.currentTextLanguage_ = this.config_.preferredTextLanguage; /** @private {string} */ this.currentVariantRole_ = ''; /** @private {string} */ this.currentTextRole_ = ''; /** * Deprecated. To be removed in v2.3. * @private {boolean} */ this.infiniteRetriesForLiveStreams_ = true; if (opt_dependencyInjector) opt_dependencyInjector(this); this.networkingEngine_ = this.createNetworkingEngine(); this.initialize_(); }; goog.inherits(shaka.Player, shaka.util.FakeEventTarget); /** * @return {!Promise} * @private */ shaka.Player.prototype.cancelLoadChain_ = function() { if (!this.loadChain_) return Promise.resolve(); var stopParser = Promise.resolve(); if (this.parser_) { // Stop the parser manually, to ensure that any network calls it may be // making are stopped in a timely fashion. // This happens in parallel with cancelling the load chain. // Otherwise, destroying will wait for any failing network calls to run // out of retries. stopParser = this.parser_.stop(); this.parser_ = null; } var cancelChain = this.loadChain_.cancel(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.PLAYER, shaka.util.Error.Code.LOAD_INTERRUPTED)); return Promise.all([stopParser, cancelChain]); }; /** * After destruction, a Player object cannot be used again. * * @override * @export */ shaka.Player.prototype.destroy = function() { this.destroyed_ = true; return this.cancelLoadChain_().then(function() { var p = Promise.all([ // We need to destroy the current fields as well as waiting for an // existing unload to complete. It is fine to call destroyStreaming_ if // there is an unload since it resets the fields immediately. this.unloadChain_, this.destroyStreaming_(), this.eventManager_ ? this.eventManager_.destroy() : null, this.networkingEngine_ ? this.networkingEngine_.destroy() : null ]); this.video_ = null; this.textVisibility_ = false; this.eventManager_ = null; this.abrManager_ = null; this.networkingEngine_ = null; this.config_ = null; return p; }.bind(this)); }; /** * @define {string} A version number taken from git at compile time. */ goog.define('GIT_VERSION', 'v2.2.9-debug'); /** * @const {string} * @export */ shaka.Player.version = GIT_VERSION; /** * @event shaka.Player.ErrorEvent * @description Fired when a playback error occurs. * @property {string} type * 'error' * @property {!shaka.util.Error} detail * An object which contains details on the error. The error's 'category' and * 'code' properties will identify the specific error that occurred. In an * uncompiled build, you can also use the 'message' and 'stack' properties * to debug. * @exportDoc */ /** * @event shaka.Player.EmsgEvent * @description Fired when a non-typical emsg is found in a segment. * @property {string} type * 'emsg' * @property {shakaExtern.EmsgInfo} detail * An object which contains the content of the emsg box. * @exportDoc */ /** * @event shaka.Player.DrmSessionUpdateEvent * @description Fired when the CDM has accepted the license response. * @property {string} type * 'drmsessionupdate' * @exportDoc */ /** * @event shaka.Player.TimelineRegionAdded * @description Fired when a media timeline region is added. * @property {string} type * 'timelineregionadded' * @property {shakaExtern.TimelineRegionInfo} detail * An object which contains a description of the region. * @exportDoc */ /** * @event shaka.Player.TimelineRegionEnter * @description Fired when the playhead enters a timeline region. * @property {string} type * 'timelineregionenter' * @property {shakaExtern.TimelineRegionInfo} detail * An object which contains a description of the region. * @exportDoc */ /** * @event shaka.Player.TimelineRegionExit * @description Fired when the playhead exits a timeline region. * @property {string} type * 'timelineregionexit' * @property {shakaExtern.TimelineregionInfo} detail * An object which contains a description of the region. * @exportDoc */ /** * @event shaka.Player.BufferingEvent * @description Fired when the player's buffering state changes. * @property {string} type * 'buffering' * @property {boolean} buffering * True when the Player enters the buffering state. * False when the Player leaves the buffering state. * @exportDoc */ /** * @event shaka.Player.LoadingEvent * @description Fired when the player begins loading. * Used by the Cast receiver to determine idle state. * @property {string} type * 'loading' * @exportDoc */ /** * @event shaka.Player.UnloadingEvent * @description Fired when the player unloads or fails to load. * Used by the Cast receiver to determine idle state. * @property {string} type * 'unloading' * @exportDoc */ /** * @event shaka.Player.TextTrackVisibilityEvent * @description Fired when text track visibility changes. * @property {string} type * 'texttrackvisibility' * @exportDoc */ /** * @event shaka.Player.TracksChangedEvent * @description Fired when the list of tracks changes. For example, this will * happen when changing periods or when track restrictions change. * @property {string} type * 'trackschanged' * @exportDoc */ /** * @event shaka.Player.AdaptationEvent * @description Fired when an automatic adaptation causes the active tracks * to change. Does not fire when the application calls selectVariantTrack() * selectTextTrack(), selectAudioLanguage() or selectTextLanguage(). * @property {string} type * 'adaptation' * @exportDoc */ /** * @event shaka.Player.ExpirationUpdatedEvent * @description Fired when there is a change in the expiration times of an * EME session. * @property {string} type * 'expirationupdated' * @exportDoc */ /** * @event shaka.Player.LargeGapEvent * @description Fired when the playhead enters a large gap. If * |config.streaming.jumpLargeGaps| is set, the default action of this event * is to jump the gap; this can be prevented by calling preventDefault() on * the event object. * @property {string} type * 'largegap' * @property {number} currentTime * The current time of the playhead. * @property {number} gapSize * The size of the gap, in seconds. * @exportDoc */ /** * @event shaka.Player.StreamingEvent * @description Fired after the manifest has been parsed and track information * is available, but before streams have been chosen and before any segments * have been fetched. You may use this event to configure the player based on * information found in the manifest. * @property {string} type * 'streaming' * @exportDoc */ /** @private {!Object.<string, function():*>} */ shaka.Player.supportPlugins_ = {}; /** * Registers a plugin callback that will be called with support(). The * callback will return the value that will be stored in the return value from * support(). * * @param {string} name * @param {function():*} callback * @export */ shaka.Player.registerSupportPlugin = function(name, callback) { shaka.Player.supportPlugins_[name] = callback; }; /** * Return whether the browser provides basic support. If this returns false, * Shaka Player cannot be used at all. In this case, do not construct a Player * instance and do not use the library. * * @return {boolean} * @export */ shaka.Player.isBrowserSupported = function() { // Basic features needed for the library to be usable. var basic = !!window.Promise && !!window.Uint8Array && !!Array.prototype.forEach; return basic && shaka.media.MediaSourceEngine.isBrowserSupported() && shaka.media.DrmEngine.isBrowserSupported(); }; /** * Probes the browser to determine what features are supported. This makes a * number of requests to EME/MSE/etc which may result in user prompts. This * should only be used for diagnostics. * * NOTE: This may show a request to the user for permission. * * @see https://goo.gl/ovYLvl * @return {!Promise.<shakaExtern.SupportType>} * @export */ shaka.Player.probeSupport = function() { goog.asserts.assert(shaka.Player.isBrowserSupported(), 'Must have basic support'); return shaka.media.DrmEngine.probeSupport().then(function(drm) { var manifest = shaka.media.ManifestParser.probeSupport(); var media = shaka.media.MediaSourceEngine.probeSupport(); var ret = { manifest: manifest, media: media, drm: drm }; var plugins = shaka.Player.supportPlugins_; for (var name in plugins) { ret[name] = plugins[name](); } return ret; }); }; /** * Load a manifest. * * @param {string} manifestUri * @param {number=} opt_startTime Optional start time, in seconds, to begin * playback. Defaults to 0 for VOD and to the live edge for live. * @param {shakaExtern.ManifestParser.Factory=} opt_manifestParserFactory * Optional manifest parser factory to override auto-detection or use an * unregistered parser. * @return {!Promise} Resolved when the manifest has been loaded and playback * has begun; rejected when an error occurs or the call was interrupted by * destroy(), unload() or another call to load(). * @export */ shaka.Player.prototype.load = function(manifestUri, opt_startTime, opt_manifestParserFactory) { var unloadPromise = this.unload(); var loadChain = new shaka.util.CancelableChain(); this.loadChain_ = loadChain; this.dispatchEvent(new shaka.util.FakeEvent('loading')); var startTime = Date.now(); return loadChain.then(function() { return unloadPromise; }).then(function() { // Not tracked in stats because it should be insignificant. // Logged in case it is not. shaka.log.debug('Unload latency:', (Date.now() - startTime) / 1000); this.stats_ = this.getCleanStats_(); this.eventManager_.listen(this.video_, 'playing', this.updateState_.bind(this)); this.eventManager_.listen(this.video_, 'pause', this.updateState_.bind(this)); this.eventManager_.listen(this.video_, 'ended', this.updateState_.bind(this)); var abrManagerFactory = this.config_.abrFactory; this.abrManager_ = new abrManagerFactory(); this.configureAbrManager_(); this.textDisplayer_ = new this.config_.textDisplayFactory(); this.textDisplayer_.setTextVisibility(this.textVisibility_); goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed'); return shaka.media.ManifestParser.getFactory( manifestUri, this.networkingEngine_, this.config_.manifest.retryParameters, opt_manifestParserFactory); }.bind(this)).then(function(factory) { this.parser_ = new factory(); this.parser_.configure(this.config_.manifest); goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed'); var playerInterface = { networkingEngine: this.networkingEngine_, filterNewPeriod: this.filterNewPeriod_.bind(this), filterAllPeriods: this.filterAllPeriods_.bind(this), onTimelineRegionAdded: this.onTimelineRegionAdded_.bind(this), onEvent: this.onEvent_.bind(this), onError: this.onError_.bind(this) }; if (this.parser_.start.length > 2) { goog.asserts.assert(false, 'Old ManifestParser interface is deprecated'); shaka.log.alwaysWarn( 'The ManifestParser interface has changed. Please upgrade your ' + 'plugin to accept the PlayerInterface structure. See the ' + 'ManifestParser documentation for details.'); // Use a string index here so the compiler doesn't complain about the // incorrect arguments. return this.parser_['start']( manifestUri, this.networkingEngine_, playerInterface.filterNewPeriod, playerInterface.onError, playerInterface.onEvent); } return this.parser_.start(manifestUri, playerInterface); }.bind(this)).then(function(manifest) { // When there is a variant with video and audio, filter out all // variants which lack one or the other. // This is to avoid problems where we choose audio-only variants because // they have lower bandwidth, when there are variants with video available. var hasAVVariant = manifest.periods.some(function(period) { return period.variants.some(function(variant) { return variant.video && variant.audio; }); }); if (hasAVVariant) { shaka.log.debug('Found variant with audio and video content, ' + 'so filtering all periods.'); manifest.periods.forEach(function(period) { period.variants = period.variants.filter(function(variant) { return variant.video && variant.audio; }); }); } if (manifest.periods.length == 0) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.NO_PERIODS); } this.manifest_ = manifest; this.manifestUri_ = manifestUri; this.drmEngine_ = this.createDrmEngine(); this.drmEngine_.configure(this.config_.drm); return this.drmEngine_.init(manifest, false /* isOffline */); }.bind(this)).then(function() { // Re-filter the manifest after DRM has been initialized. this.filterAllPeriods_(this.manifest_.periods); this.lastTimeStatsUpdateTimestamp_ = Date.now() / 1000; // Copy preferred languages from the config again, in case the config was // changed between construction and playback. this.currentAudioLanguage_ = this.config_.preferredAudioLanguage; this.currentTextLanguage_ = this.config_.preferredTextLanguage; var fullDuration = this.manifest_.presentationTimeline.getDuration(); var playRangeEnd = this.config_.playRangeEnd; var playRangeStart = this.config_.playRangeStart; if (playRangeStart > 0) { if (this.isLive()) { shaka.log.warning('PlayerConfiguration.playRangeStart has been ' + 'configured for live content. Ignoring the setting.'); } else { this.manifest_.presentationTimeline.setAvailabilityStart( playRangeStart); } } // If the playback has been configured to end before the end of the // presentation, update the duration unless it's live content. if (playRangeEnd < fullDuration) { if (this.isLive()) { shaka.log.warning('PlayerConfiguration.playRangeEnd has been ' + 'configured for live content. Ignoring the setting.'); } else { this.manifest_.presentationTimeline.setDuration(playRangeEnd); } } // Wait for MediaSource to open before continuing. return Promise.all([ this.drmEngine_.attach(this.video_), this.mediaSourceOpen_ ]); }.bind(this)).then(function() { if (this.abrManager_['chooseStreams']) { shaka.log.alwaysWarn('AbrManager API has changed. ' + 'The SwitchCallback signature has changed to accept a variant ' + 'instead of a map. Please update your AbrManager. ' + 'The old API will be removed in v2.3.'); this.abrManager_['init'](this.switchV21_.bind(this)); } else { this.abrManager_.init(this.switch_.bind(this)); } // MediaSource is open, so create the Playhead, MediaSourceEngine, and // StreamingEngine. this.playhead_ = this.createPlayhead(opt_startTime); this.playheadObserver_ = this.createPlayheadObserver(); this.mediaSourceEngine_ = this.createMediaSourceEngine(); this.streamingEngine_ = this.createStreamingEngine(); this.streamingEngine_.configure(this.config_.streaming); var event = new shaka.util.FakeEvent('streaming'); this.dispatchEvent(event); // If the content is multi-codec and the browser can play more than one of // them, choose codecs now before we initialize streaming. this.chooseCodecsAndFilterManifest_(); return this.streamingEngine_.init(); }.bind(this)).then(function() { if (this.config_.streaming.startAtSegmentBoundary) { var time = this.adjustStartTime_(this.playhead_.getTime()); this.playhead_.setStartTime(time); } // Re-filter the manifest after streams have been chosen. this.manifest_.periods.forEach(this.filterNewPeriod_.bind(this)); // Dispatch a 'trackschanged' event now that all initial filtering is done. this.onTracksChanged_(); // Since the first streams just became active, send an adaptation event. this.onAdaptation_(); // Now that we've filtered out variants that aren't compatible with the // active one, update abr manager with filtered variants for the current // period. var currentPeriod = this.streamingEngine_.getCurrentPeriod(); var variants = shaka.util.StreamUtils.filterVariantsByLanguageAndRole( currentPeriod, this.currentAudioLanguage_, this.currentVariantRole_); this.abrManager_.setVariants(variants); var hasPrimary = currentPeriod.variants.some(function(variant) { return variant.primary; }); if (!this.currentAudioLanguage_ && !hasPrimary) { shaka.log.warning('No preferred audio language set. We will choose an ' + 'arbitrary language initially'); } this.pendingTimelineRegions_.forEach( this.playheadObserver_.addTimelineRegion.bind(this.playheadObserver_)); this.pendingTimelineRegions_ = []; // Wait for the 'loadeddata' event to measure load() latency. this.eventManager_.listenOnce(this.video_, 'loadeddata', function() { // Compute latency in seconds (Date.now() gives ms): var latency = (Date.now() - startTime) / 1000; this.stats_.loadLatency = latency; shaka.log.debug('Load latency:', latency); }.bind(this)); this.loadChain_ = null; }.bind(this)).finalize().catch(function(error) { goog.asserts.assert(error instanceof shaka.util.Error, 'Wrong error type!'); shaka.log.debug('load() failed:', error, error ? error.message : null, error ? error.stack : null); // If we haven't started another load, clear the loadChain_ member. if (this.loadChain_ == loadChain) { this.loadChain_ = null; this.dispatchEvent(new shaka.util.FakeEvent('unloading')); } return Promise.reject(error); }.bind(this)); }; /** * In case of multiple usable codecs, choose one based on lowest average * bandwidth and filter out the rest. * @private */ shaka.Player.prototype.chooseCodecsAndFilterManifest_ = function() { function variantCodecs(variant) { // Only consider the base of the codec string. For example, these should // both be considered the same codec: avc1.42c01e, avc1.4d401f var baseVideoCodec = variant.video ? variant.video.codecs.split('.')[0] : ''; var baseAudioCodec = variant.audio ? variant.audio.codecs.split('.')[0] : ''; return baseVideoCodec + '-' + baseAudioCodec; } // Organize variants into buckets by codecs. var variantsByCodecs = {}; this.manifest_.periods.forEach(function(period) { period.variants.forEach(function(variant) { var codecs = variantCodecs(variant); if (!(codecs in variantsByCodecs)) { variantsByCodecs[codecs] = []; } variantsByCodecs[codecs].push(variant); }); }); // Compute the average bandwidth for each group of variants. // Choose the lowest-bandwidth codecs. var bestCodecs = null; var lowestAverageBandwidth = Infinity; shaka.util.MapUtils.forEach(variantsByCodecs, function(codecs, variants) { var sum = 0; var num = 0; variants.forEach(function(variant) { sum += variant.bandwidth || 0; ++num; }); var averageBandwidth = sum / num; shaka.log.debug('codecs', codecs, 'avg bandwidth', averageBandwidth); if (averageBandwidth < lowestAverageBandwidth) { bestCodecs = codecs; lowestAverageBandwidth = averageBandwidth; } }); goog.asserts.assert(bestCodecs != null, 'Should have chosen codecs!'); goog.asserts.assert(!isNaN(lowestAverageBandwidth), 'Bandwidth should be a number!'); // Filter out any variants that don't match, forcing AbrManager to choose from // the most efficient variants possible. this.manifest_.periods.forEach(function(period) { period.variants = period.variants.filter(function(variant) { var codecs = variantCodecs(variant); if (codecs == bestCodecs) return true; shaka.log.debug('Dropping Variant (better codec available)', variant); return false; }); }); }; /** * Creates a new instance of DrmEngine. This can be replaced by tests to * create fake instances instead. * * @return {!shaka.media.DrmEngine} */ shaka.Player.prototype.createDrmEngine = function() { goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed'); var playerInterface = { netEngine: this.networkingEngine_, onError: this.onError_.bind(this), onKeyStatus: this.onKeyStatus_.bind(this), onExpirationUpdated: this.onExpirationUpdated_.bind(this), onEvent: this.onEvent_.bind(this) }; return new shaka.media.DrmEngine(playerInterface); }; /** * Creates a new instance of NetworkingEngine. This can be replaced by tests * to create fake instances instead. * * @return {!shaka.net.NetworkingEngine} */ shaka.Player.prototype.createNetworkingEngine = function() { return new shaka.net.NetworkingEngine(this.onSegmentDownloaded_.bind(this)); }; /** * Creates a new instance of Playhead. This can be replaced by tests to create * fake instances instead. * * @param {number=} opt_startTime * @return {!shaka.media.Playhead} */ shaka.Player.prototype.createPlayhead = function(opt_startTime) { goog.asserts.assert(this.manifest_, 'Must have manifest'); var startTime = opt_startTime == undefined ? null : opt_startTime; return new shaka.media.Playhead( this.video_, this.manifest_, this.config_.streaming, startTime, this.onSeek_.bind(this), this.onEvent_.bind(this)); }; /** * Creates a new instance of PlayheadOvserver. This can be replaced by tests to * create fake instances instead. * * @return {!shaka.media.PlayheadObserver} */ shaka.Player.prototype.createPlayheadObserver = function() { goog.asserts.assert(this.manifest_, 'Must have manifest'); return new shaka.media.PlayheadObserver( this.video_, this.mediaSource_, this.manifest_, this.config_.streaming, this.onBuffering_.bind(this), this.onEvent_.bind(this), this.onChangePeriod_.bind(this)); }; /** * Create and open MediaSource. Potentially slow. * * @return {!Promise} */ shaka.Player.prototype.createMediaSource = function() { this.mediaSource_ = new MediaSource(); var ret = new shaka.util.PublicPromise(); this.eventManager_.listen(this.mediaSource_, 'sourceopen', ret.resolve); this.video_.src = window.URL.createObjectURL(this.mediaSource_); return ret; }; /** * Creates a new instance of MediaSourceEngine. This can be replaced by tests * to create fake instances instead. * * @return {!shaka.media.MediaSourceEngine} */ shaka.Player.prototype.createMediaSourceEngine = function() { return new shaka.media.MediaSourceEngine( this.video_, this.mediaSource_, this.textDisplayer_); }; /** * Creates a new instance of StreamingEngine. This can be replaced by tests * to create fake instances instead. * * @return {!shaka.media.StreamingEngine} */ shaka.Player.prototype.createStreamingEngine = function() { goog.asserts.assert( this.playhead_ && this.playheadObserver_ && this.mediaSourceEngine_ && this.manifest_, 'Must not be destroyed'); var playerInterface = { playhead: this.playhead_, mediaSourceEngine: this.mediaSourceEngine_, netEngine: this.networkingEngine_, onChooseStreams: this.onChooseStreams_.bind(this), onCanSwitch: this.canSwitch_.bind(this), onError: this.onError_.bind(this), onEvent: this.onEvent_.bind(this), onManifestUpdate: this.onManifestUpdate_.bind(this), onSegmentAppended: this.onSegmentAppended_.bind(this), filterNewPeriod: this.filterNewPeriod_.bind(this), filterAllPeriods: this.filterAllPeriods_.bind(this) }; return new shaka.media.StreamingEngine(this.manifest_, playerInterface); }; /** * Configure the Player instance. * * The config object passed in need not be complete. It will be merged with * the existing Player configuration. * * Config keys and types will be checked. If any problems with the config * object are found, errors will be reported through logs. * * @param {!Object} config This should follow the form of * {@link shakaExtern.PlayerConfiguration}, but you may omit any field you do * not wish to change. * @export */ shaka.Player.prototype.configure = function(config) { goog.asserts.assert(this.config_, 'Config must not be null!'); // Backward compatibility for AbrManager injection. To be removed in v2.3. if (config['abr'] && config['abr']['manager']) { shaka.log.alwaysWarn('AbrManager configuration has changed. ' + 'Please use abrFactory instead of abr.manager. ' + 'The old API will be removed in v2.3.'); var managerInstance = config['abr']['manager']; var backwardCompatibilityFactory = function() { return managerInstance; }; delete config['abr']['manager']; config['abrFactory'] = backwardCompatibilityFactory; } // Backward compatibility for streaming config. To be removed in v2.3. if (config['streaming'] && config['streaming']['infiniteRetriesForLiveStreams'] != null) { shaka.log.alwaysWarn('Streaming configuration has changed. ' + 'Please use streaming.failureCallback instead of ' + 'streaming.infiniteRetriesForLiveStreams. ' + 'The old API will be removed in v2.3.'); this.infiniteRetriesForLiveStreams_ = !!config['streaming']['infiniteRetriesForLiveStreams']; delete config['streaming']['infiniteRetriesForLiveStreams']; } shaka.util.ConfigUtils.mergeConfigObjects( this.config_, config, this.defaultConfig_(), this.configOverrides_(), ''); this.applyConfig_(); }; /** * Apply config changes. * @private */ shaka.Player.prototype.applyConfig_ = function() { if (this.parser_) { this.parser_.configure(this.config_.manifest); } if (this.drmEngine_) { this.drmEngine_.configure(this.config_.drm); } if (this.streamingEngine_) { this.streamingEngine_.configure(this.config_.streaming); // Need to apply the restrictions to every period. try { // this.filterNewPeriod_() may throw. this.manifest_.periods.forEach(this.filterNewPeriod_.bind(this)); } catch (error) { this.onError_(error); } // If the stream we are playing is restricted, we need to switch. var activeStreams = this.streamingEngine_.getActiveStreams(); var ContentType = shaka.util.ManifestParserUtils.ContentType; var activeAudio = activeStreams[ContentType.AUDIO]; var activeVideo = activeStreams[ContentType.VIDEO]; var period = this.streamingEngine_.getCurrentPeriod(); var activeVariant = shaka.util.StreamUtils.getVariantByStreams( activeAudio, activeVideo, period.variants); if (!activeVariant || !activeVariant.allowedByApplication || !activeVariant.allowedByKeySystem) { shaka.log.debug('Choosing new streams after changing configuration'); this.chooseStreamsAndSwitch_(period); } } if (this.abrManager_) { this.configureAbrManager_(); // Simply enable/disable ABR with each call, since multiple calls to these // methods have no effect. if (this.config_.abr.enabled && !this.switchingPeriods_) { this.abrManager_.enable(); } else { this.abrManager_.disable(); } } }; /** * Backward compatibility for AbrManager configuration. To be removed in v2.3. * @private */ shaka.Player.prototype.configureAbrManager_ = function() { if (this.abrManager_.configure) { this.abrManager_.configure(this.config_.abr); } else { shaka.log.alwaysWarn('AbrManager API has changed. ' + 'AbrManager.setDefaultEstimate() and ' + 'AbrManager.setRestrictions() are deprecated. ' + 'AbrManager.configure() is used instead. ' + 'Please upgrade to the new API. ' + 'The old API will be removed in v2.3.'); this.abrManager_['setDefaultEstimate']( this.config_.abr.defaultBandwidthEstimate); this.abrManager_['setRestrictions'](this.config_.abr.restrictions); } }; /** * Return a copy of the current configuration. Modifications of the returned * value will not affect the Player's active configuration. You must call * player.configure() to make changes. * * @return {shakaExtern.PlayerConfiguration} * @export */ shaka.Player.prototype.getConfiguration = function() { goog.asserts.assert(this.config_, 'Config must not be null!'); var ret = this.defaultConfig_(); shaka.util.ConfigUtils.mergeConfigObjects( ret, this.config_, this.defaultConfig_(), this.configOverrides_(), ''); return ret; }; /** * Reset configuration to default. * @export */ shaka.Player.prototype.resetConfiguration = function() { // Don't call mergeConfigObjects_(), since that would not reset open-ended // dictionaries like drm.servers. this.config_ = this.defaultConfig_(); this.applyConfig_(); }; /** * @return {HTMLMediaElement} A reference to the HTML Media Element passed * in during initialization. * @export */ shaka.Player.prototype.getMediaElement = function() { return this.video_; }; /** * @return {shaka.net.NetworkingEngine} A reference to the Player's networking * engine. Applications may use this to make requests through Shaka's * networking plugins. * @export */ shaka.Player.prototype.getNetworkingEngine = function() { return this.networkingEngine_; }; /** * @return {?string} If a manifest is loaded, returns the manifest URI given in * the last call to load(). Otherwise, returns null. * @export */ shaka.Player.prototype.getManifestUri = function() { return this.manifestUri_; }; /** * @return {boolean} True if the current stream is live. False otherwise. * @export */ shaka.Player.prototype.isLive = function() { return this.manifest_ ? this.manifest_.presentationTimeline.isLive() : false; }; /** * @return {boolean} True if the current stream is in-progress VOD. * False otherwise. * @export */ shaka.Player.prototype.isInProgress = function() { return this.manifest_ ? this.manifest_.presentationTimeline.isInProgress() : false; }; /** * @return {boolean} True for audio-only content. False otherwise. * @export */ shaka.Player.prototype.isAudioOnly = function() { if (!this.manifest_ || !this.manifest_.periods.length) return false; var variants = this.manifest_.periods[0].variants; if (!variants.length) return false; // Note that if there are some audio-only variants and some audio-video // variants, the audio-only variants are removed during filtering. // Therefore if the first variant has no video, that's sufficient to say it // is audio-only content. return !variants[0].video; }; /** * Get the seekable range for the current stream. * @return {{start: number, end: number}} * @export */ shaka.Player.prototype.seekRange = function() { var start = 0; var end = 0; if (this.manifest_) { var timeline = this.manifest_.presentationTimeline; start = timeline.getSegmentAvailabilityStart(); end = timeline.getSeekRangeEnd(); } return {'start': start, 'end': end}; }; /** * Get the key system currently being used by EME. This returns the empty * string if not using EME. * * @return {string} * @export */ shaka.Player.prototype.keySystem = function() { return this.drmEngine_ ? this.drmEngine_.keySystem() : ''; }; /** * Get the DrmInfo used to initialize EME. This returns null when not using * EME. * * @return {?shakaExtern.DrmInfo} * @export */ shaka.Player.prototype.drmInfo = function() { return this.drmEngine_ ? this.drmEngine_.getDrmInfo() : null; }; /** * The next known expiration time for any EME session. If the sessions never * expire, or there are no EME sessions, this returns Infinity. * * @return {number} * @export */ shaka.Player.prototype.getExpiration = function() { return this.drmEngine_ ? this.drmEngine_.getExpiration() : Infinity; }; /** * @return {boolean} True if the Player is in a buffering state. * @export */ shaka.Player.prototype.isBuffering = function() { return this.buffering_; }; /** * Unload the current manifest and make the Player available for re-use. * * @return {!Promise} Resolved when streaming has stopped and the previous * content, if any, has been unloaded. * @export */ shaka.Player.prototype.unload = function() { if (this.destroyed_) return Promise.resolve(); this.dispatchEvent(new shaka.util.FakeEvent('unloading')); return this.cancelLoadChain_().then(function() { // If there is an existing unload operation, use that. if (!this.unloadChain_) { this.unloadChain_ = this.resetStreaming_().then(function() { this.unloadChain_ = null; }.bind(this)); } return this.unloadChain_; }.bind(this)); }; /** * Gets the current effective playback rate. If using trick play, it will * return the current trick play rate; otherwise, it will return the video * playback rate. * @return {number} * @export */ shaka.Player.prototype.getPlaybackRate = function() { return this.playhead_ ? this.playhead_.getPlaybackRate() : 0; }; /** * Skip through the content without playing. Simulated using repeated seeks. * * Trick play will be canceled automatically if the playhead hits the beginning * or end of the seekable range for the content. * * @param {number} rate The playback rate to simulate. For example, a rate of * 2.5 would result in 2.5 seconds of content being skipped every second. * To trick-play backward, use a negative rate. * @export */ shaka.Player.prototype.trickPlay = function(rate) { shaka.log.debug('Trick play rate', rate); if (this.playhead_) this.playhead_.setPlaybackRate(rate); if (this.streamingEngine_) this.streamingEngine_.setTrickPlay(rate != 1); }; /** * Cancel trick-play. * @export */ shaka.Player.prototype.cancelTrickPlay = function() { shaka.log.debug('Trick play canceled'); if (this.playhead_) this.playhead_.setPlaybackRate(1); if (this.streamingEngine_) this.streamingEngine_.setTrickPlay(false); }; /** * Return a list of variant tracks available for the current * Period. If there are multiple Periods, then you must seek to the Period * before being able to switch. * * @return {!Array.<shakaExtern.Track>} * @export */ shaka.Player.prototype.getVariantTracks = function() { if (!this.manifest_ || !this.playhead_) return []; this.assertCorrectActiveStreams_(); var ContentType = shaka.util.ManifestParserUtils.ContentType; var currentPeriod = shaka.util.StreamUtils.findPeriodContainingTime( this.manifest_, this.playhead_.getTime()); var activeStreams = this.activeStreamsByPeriod_[currentPeriod] || {}; return shaka.util.StreamUtils.getVariantTracks( this.manifest_.periods[currentPeriod], activeStreams[ContentType.AUDIO], activeStreams[ContentType.VIDEO]); }; /** * Return a list of text tracks available for the current * Period. If there are multiple Periods, then you must seek to the Period * before being able to switch. * * @return {!Array.<shakaExtern.Track>} * @export */ shaka.Player.prototype.getTextTracks = function() { if (!this.manifest_ || !this.playhead_) return []; this.assertCorrectActiveStreams_(); var ContentType = shaka.util.ManifestParserUtils.ContentType; var currentPeriod = shaka.util.StreamUtils.findPeriodContainingTime( this.manifest_, this.playhead_.getTime()); var activeStreams = this.activeStreamsByPeriod_[currentPeriod] || {}; return shaka.util.StreamUtils .getTextTracks( this.manifest_.periods[currentPeriod], activeStreams[ContentType.TEXT]) .filter(function(track) { // Don't show any tracks that are being loaded still. return this.loadingTextStreamIds_.indexOf(track.id) < 0; }.bind(this)); }; /** * Select a specific text track. Note that AdaptationEvents are not * fired for manual track selections. * * @param {shakaExtern.Track} track * @export */ shaka.Player.prototype.selectTextTrack = function(track) { if (!this.streamingEngine_) return; var StreamUtils = shaka.util.StreamUtils; var period = this.streamingEngine_.getCurrentPeriod(); var stream = StreamUtils.findTextStreamForTrack(period, track); if (!stream) { shaka.log.error('Unable to find the track with id "' + track.id + '"; did we change Periods?'); return; } // Add entries to the history. this.addTextStreamToSwitchHistory_(stream, /* fromAdaptation */ false); this.switchTextStream_(stream); }; /** * Select a specific track. Note that AdaptationEvents are not fired for manual * track selections. * * @param {shakaExtern.Track} track * @param {boolean=} opt_clearBuffer * @export */ shaka.Player.prototype.selectVariantTrack = function(track, opt_clearBuffer) { if (!this.streamingEngine_) return; if (this.config_.abr.enabled) { shaka.log.alwaysWarn('Changing tracks while abr manager is enabled will ' + 'likely result in the selected track being ' + 'overriden. Consider disabling abr before calling ' + 'selectVariantTrack().'); } var StreamUtils = shaka.util.StreamUtils; var period = this.streamingEngine_.getCurrentPeriod(); var variant = StreamUtils.findVariantForTrack(period, track); if (!variant) { shaka.log.error('Unable to locate track with id "' + track.id + '".'); return; } // Double check that the track is allowed to be played. // The track list should only contain playable variants, // but if restrictions change and selectVariantTrack() // is called before the track list is updated, we could // get a now-restricted variant. var variantIsPlayable = StreamUtils.isPlayable(variant); if (!variantIsPlayable) { shaka.log.error('Unable to switch to track with id "' + track.id + '" because it is restricted.'); return; } // Add entries to the history. this.addVariantToSwitchHistory_(variant, /* fromAdaptation */ false); this.switchVariant_(variant, opt_clearBuffer); }; /** * Return a list of audio languages available for the current * Period. * * @return {!Array.<string>} * @export */ shaka.Player.prototype.getAudioLanguages = function() { if (!this.streamingEngine_) { return []; } var StreamUtils = shaka.util.StreamUtils; var period = this.streamingEngine_.getCurrentPeriod(); var variants = StreamUtils.getPlayableVariants(period.variants); return variants.map(function(variant) { return variant.language; }).filter(shaka.util.Functional.isNotDuplicate); }; /** * Return a list of text languages available for the current * Period. * * @return {!Array.<string>} * @export */ shaka.Player.prototype.getTextLanguages = function() { if (!this.streamingEngine_) { return []; } var period = this.streamingEngine_.getCurrentPeriod(); return period.textStreams.map(function(stream) { return stream.language; }).filter(shaka.util.Functional.isNotDuplicate); }; /** * Sets currentAudioLanguage to the selected language and chooses * new variant in that language if need be. * * @param {!string} language * @param {string=} opt_role * @export */ shaka.Player.prototype.selectAudioLanguage = function(language, opt_role) { if (!this.streamingEngine_) return; var period = this.streamingEngine_.getCurrentPeriod(); this.currentAudioLanguage_ = language; this.currentVariantRole_ = opt_role || ''; this.chooseStreamsAndSwitch_(period); }; /** * Sets currentTextLanguage to the selected language and chooses * new text stream in that language if need be. * * @param {!string} language * @param {string=} opt_role * @export */ shaka.Player.prototype.selectTextLanguage = function(language, opt_role) { if (!this.streamingEngine_) return; var period = this.streamingEngine_.getCurrentPeriod(); this.currentTextLanguage_ = language; this.currentTextRole_ = opt_role || ''; this.chooseStreamsAndSwitch_(period); }; /** * @return {boolean} True if the current text track is visible. * @export */ shaka.Player.prototype.isTextTrackVisible = function() { if (this.textDisplayer_) { return this.textDisplayer_.isTextVisible(); } else { return this.textVisibility_; } }; /** * Set the visibility of the current text track, if any. * * @param {boolean} on * @export */ shaka.Player.prototype.setTextTrackVisibility = function(on) { if (this.textDisplayer_) { this.textDisplayer_.setTextVisibility(on); } this.textVisibility_ = on; this.onTextTrackVisibility_(); }; /** * Returns current playhead time as a Date. * * @return {Date} * @export */ shaka.Player.prototype.getPlayheadTimeAsDate = function() { if (!this.manifest_) return null; goog.asserts.assert(this.isLive(), 'getPlayheadTimeAsDate should be called on a live stream!'); var time = this.manifest_.presentationTimeline.getPresentationStartTime() * 1000 + this.video_.currentTime * 1000; return new Date(time); }; /** * Returns the presentation start time as a Date. * * @return {Date} * @export */ shaka.Player.prototype.getPresentationStartTimeAsDate = function() { if (!this.manifest_) return null; goog.asserts.assert(this.isLive(), 'getPresentationStartTimeAsDate should be called on a live stream!'); var time = this.manifest_.presentationTimeline.getPresentationStartTime() * 1000; return new Date(time); }; /** * Return the information about the current buffered ranges. * * @return {shakaExtern.BufferedInfo} * @export */ shaka.Player.prototype.getBufferedInfo = function() { if (!this.mediaSourceEngine_) { return { total: [], audio: [], video: [], text: [] }; } return this.mediaSourceEngine_.getBufferedInfo(); }; /** * Return playback and adaptation stats. * * @return {shakaExtern.Stats} * @export */ shaka.Player.prototype.getStats = function() { var ContentType = shaka.util.ManifestParserUtils.ContentType; this.updateTimeStats_(); this.updateState_(); var video = null; var variant = null; var videoElem = /** @type {!HTMLVideoElement} */ (this.video_); var videoInfo = videoElem && videoElem.getVideoPlaybackQuality ? videoElem.getVideoPlaybackQuality() : {}; if (this.playhead_ && this.manifest_) { var periodIdx = shaka.util.StreamUtils.findPeriodContainingTime( this.manifest_, this.playhead_.getTime()); var period = this.manifest_.periods[periodIdx]; var activeStreams = this.activeStreamsByPeriod_[periodIdx]; if (activeStreams) { variant = shaka.util.StreamUtils.getVariantByStreamIds( activeStreams[ContentType.AUDIO], activeStreams[ContentType.VIDEO], period.variants); video = variant.video || {}; } } if (!video) video = {}; if (!variant) variant = {}; // Clone the internal object so our state cannot be tampered with. var cloneObject = shaka.util.ConfigUtils.cloneObject; return { // Not tracked in this.stats_: width: video.width || 0, height: video.height || 0, streamBandwidth: variant.bandwidth || 0, decodedFrames: Number(videoInfo.totalVideoFrames), droppedFrames: Number(videoInfo.droppedVideoFrames), estimatedBandwidth: this.abrManager_ ? this.abrManager_.getBandwidthEstimate() : NaN, loadLatency: this.stats_.loadLatency, playTime: this.stats_.playTime, bufferingTime: this.stats_.bufferingTime, // Deep-clone the objects as well as the arrays that contain them: switchHistory: cloneObject(this.stats_.switchHistory), stateHistory: cloneObject(this.stats_.stateHistory) }; }; /** * Adds the given text track to the current Period. Load() must resolve before * calling. The current Period or the presentation must have a duration. This * returns a Promise that will resolve when the track can be switched to and * will resolve with the track that was created. * * @param {string} uri * @param {string} language * @param {string} ki