diff --git a/lib/player.js b/lib/player.js index feae32f239..9bb8b181a1 100644 --- a/lib/player.js +++ b/lib/player.js @@ -1508,7 +1508,7 @@ shaka.Player.prototype.onLoad_ = async function(has, wants) { // Stats are for a single playback/load session. Stats must be initialized // before we allow calls to |updateStateHistory|. - this.stats_ = new shaka.util.Stats(wants.startTimeOfLoad); + this.stats_ = new shaka.util.Stats(); const updateStateHistory = () => this.updateStateHistory_(); this.eventManager_.listen(mediaElement, 'playing', updateStateHistory); @@ -1618,7 +1618,10 @@ shaka.Player.prototype.onLoad_ = async function(has, wants) { // Wait for the 'loadeddata' event to measure load() latency. this.eventManager_.listenOnce(mediaElement, 'loadeddata', () => { - this.stats_.markEndOfLoad(); + const now = Date.now() / 1000; + const delta = now - wants.startTimeOfLoad; + + this.stats_.setLoadLatency(delta); }); }; @@ -1660,7 +1663,7 @@ shaka.Player.prototype.onSrcEquals_ = function(has, wants) { this.assetUri_ = has.uri; // Stats are for a single playback/load session. - this.stats_ = new shaka.util.Stats(wants.startTimeOfLoad); + this.stats_ = new shaka.util.Stats(); this.playhead_ = new shaka.media.SrcEqualsPlayhead(has.mediaElement); @@ -1671,21 +1674,38 @@ shaka.Player.prototype.onSrcEquals_ = function(has, wants) { // We need to start the buffer management code near the end because it will // set the initial buffering state and that depends on other components being // initialized. - this.startBufferManagement_(this.config_.streaming.rebufferingGoal); + // + // With src= we are not in-charge of the buffering logic, so we must respect + // what the browser wants to do. Playing 4K MP4 Sintel on Chrome, the media + // element appears to limit itself to 2.5 seconds. For now, lets put our + // rebuffer to 2 seconds. + // + // TODO: If 2 seconds appears to be a poor choice, explore different options + // for what we can do to detect/track buffering with src=. + this.startBufferManagement_(/* rebuffer= */ 2); // Add all media element listeners. const updateStateHistory = () => this.updateStateHistory_(); this.eventManager_.listen(has.mediaElement, 'playing', updateStateHistory); this.eventManager_.listen(has.mediaElement, 'pause', updateStateHistory); this.eventManager_.listen(has.mediaElement, 'ended', updateStateHistory); + + // Wait for the 'loadeddata' event to measure load() latency. this.eventManager_.listenOnce(has.mediaElement, 'loadeddata', () => { - this.stats_.markEndOfLoad(); + const now = Date.now() / 1000; + const delta = now - wants.startTimeOfLoad; + + this.stats_.setLoadLatency(delta); }); // By setting |src| we are done "loading" with src=. We don't need to set the // current time because |playhead| will do that for us. has.mediaElement.src = has.uri; + // Set the load mode last so that we know that all our components are + // initialized. + this.loadMode_ = shaka.Player.LoadMode.SRC_EQUALS; + return shaka.util.AbortableOperation.completed(undefined); }; @@ -2233,9 +2253,10 @@ shaka.Player.prototype.getNetworkingEngine = function() { * @export */ shaka.Player.prototype.getAssetUri = function() { - return this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE ? - this.assetUri_ : - null; + const loaded = this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE || + this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS; + + return loaded ? this.assetUri_ : null; }; @@ -2295,6 +2316,9 @@ shaka.Player.prototype.isInProgress = function() { * @export */ shaka.Player.prototype.isAudioOnly = function() { + // TODO: When the audio tracks api on HTMLMediaElement is generally supported + // we should be able to check if content is audio only. + if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { return false; } @@ -2322,6 +2346,13 @@ shaka.Player.prototype.isAudioOnly = function() { * @export */ shaka.Player.prototype.seekRange = function() { + // If we have loaded content with src=, we assume that the whole presentation + // is seekable. For mp4, this is always the case. For HLS, this may not always + // be the case, but there is no way for us to get this information. + if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) { + return {'start': 0, 'end': this.video_.duration}; + } + if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { return {'start': 0, 'end': 0}; } @@ -2392,7 +2423,10 @@ shaka.Player.prototype.getExpiration = function() { shaka.Player.prototype.isBuffering = function() { const State = shaka.media.BufferingObserver.State; - return this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE ? + const loaded = this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE || + this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS; + + return loaded ? this.bufferObserver_.getState() == State.STARVING : false; }; @@ -2410,9 +2444,10 @@ shaka.Player.prototype.isBuffering = function() { * @export */ shaka.Player.prototype.getPlaybackRate = function() { - return this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE ? - this.playhead_.getPlaybackRate() : - 0; + const loaded = this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE || + this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS; + + return loaded ? this.playhead_.getPlaybackRate() : 0; }; @@ -2470,6 +2505,9 @@ shaka.Player.prototype.cancelTrickPlay = function() { * @export */ shaka.Player.prototype.getVariantTracks = function() { + // TODO: Once we get consistent behaviour from the tracks api across browsers + // we should be able to provide variant track information for src=. + if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { return []; } @@ -2502,6 +2540,9 @@ shaka.Player.prototype.getVariantTracks = function() { * @export */ shaka.Player.prototype.getTextTracks = function() { + // TODO: Once we get consistent behaviour from the tracks api across browsers + // we should be able to provide variant track information for src=. + if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { return []; } @@ -2828,6 +2869,8 @@ shaka.Player.prototype.selectTextLanguage = function(language, role) { * @export */ shaka.Player.prototype.isTextTrackVisible = function() { + // Right now we don't support text with src=. So the only time that text could + // be visible is when we have loaded content with media source. if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { return false; } @@ -2994,6 +3037,11 @@ shaka.Player.prototype.getBufferedInfo = function() { text: [], }; + if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) { + const getBufferedInfo = shaka.media.TimeRangesUtils.getBufferedInfo; + info.total = getBufferedInfo(this.video_.buffered); + } + if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) { this.mediaSourceEngine_.getBufferedInfo(info); } @@ -3014,7 +3062,10 @@ shaka.Player.prototype.getStats = function() { // never fail. // // TODO: Should we include the bandwidth information with the empty blob? - if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { + const loaded = this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE || + this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS; + + if (!loaded) { return shaka.util.Stats.getEmptyBlob(); } @@ -3031,23 +3082,25 @@ shaka.Player.prototype.getStats = function() { Number(info.totalVideoFrames)); } - // Event through we are load, it is still possible that we don't have a - // presentation variant yet because we set the load mode before we select the - // first variant to stream. - const variant = this.getPresentationVariant_(); + if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) { + // Event through we are loaded, it is still possible that we don't have a + // presentation variant yet because we set the load mode before we select + // the first variant to stream. + const variant = this.getPresentationVariant_(); - if (variant) { - this.stats_.setVariantBandwidth(variant.bandwidth); - } + if (variant) { + this.stats_.setVariantBandwidth(variant.bandwidth); + } - if (variant && variant.video) { - this.stats_.setResolution( - /* width= */ variant.video.width || NaN, - /* height= */ variant.video.height || NaN); - } + if (variant && variant.video) { + this.stats_.setResolution( + /* width= */ variant.video.width || NaN, + /* height= */ variant.video.height || NaN); + } - const estimate = this.abrManager_.getBandwidthEstimate(); - this.stats_.setBandwidthEstimate(estimate); + const estimate = this.abrManager_.getBandwidthEstimate(); + this.stats_.setBandwidthEstimate(estimate); + } return this.stats_.getBlob(); }; @@ -3071,6 +3124,10 @@ shaka.Player.prototype.getStats = function() { shaka.Player.prototype.addTextTrack = async function( uri, language, kind, mime, codec, label) { // TODO: Add an actual error for this. + if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) { + shaka.log.error('Cannot add text when loaded with src='); + return Promise.reject(); + } if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) { shaka.log.error( 'Must call load() and wait for it to resolve before adding text ' + diff --git a/lib/util/stats.js b/lib/util/stats.js index 9acd691579..a3f48b5794 100644 --- a/lib/util/stats.js +++ b/lib/util/stats.js @@ -28,10 +28,7 @@ goog.require('shaka.util.SwitchHistory'); * @final */ shaka.util.Stats = class { - /** - * @param {number} startOfLoadSeconds - */ - constructor(startOfLoadSeconds) { + constructor() { /** @private {number} */ this.width_ = NaN; /** @private {number} */ @@ -42,13 +39,8 @@ shaka.util.Stats = class { /** @private {number} */ this.totalDecodedFrames_ = NaN; - // We want to track how much time elapses between when the player says it - // starts loading content and when it says its done. The meaning of "start" - // and "finish" is defined within the scope of the player, not here. - /** @private {number} */ - this.loadStartedSeconds_ = startOfLoadSeconds; /** @private {number} */ - this.loadFinishedSeconds_ = NaN; + this.loadLatencySeconds_ = NaN; /** @private {number} */ this.variantBandwidth_ = NaN; @@ -86,11 +78,13 @@ shaka.util.Stats = class { } /** - * Mark the end of the load process. This should only be called after calling - * |markStartOfLoad|. + * Record the time it took between the user signalling "I want to play this" + * to "I am now seeing this". + * + * @param {number} seconds */ - markEndOfLoad() { - this.loadFinishedSeconds_ = Date.now() / 1000; + setLoadLatency(seconds) { + this.loadLatencySeconds_ = seconds; } /** @@ -128,11 +122,6 @@ shaka.util.Stats = class { * @return {shaka.extern.Stats} */ getBlob() { - // Make sure that the start and end times make sense. If not, use NaN. - const loadLatency = (this.loadFinishedSeconds_ > this.loadStartedSeconds_) ? - (this.loadFinishedSeconds_ - this.loadStartedSeconds_) : - (NaN); - return { width: this.width_, height: this.height_, @@ -140,7 +129,7 @@ shaka.util.Stats = class { decodedFrames: this.totalDecodedFrames_, droppedFrames: this.totalDroppedFrames_, estimatedBandwidth: this.bandwidthEstimate_, - loadLatency: loadLatency, + loadLatency: this.loadLatencySeconds_, playTime: this.stateHistory_.getTimeSpentIn('playing'), pauseTime: this.stateHistory_.getTimeSpentIn('paused'), bufferingTime: this.stateHistory_.getTimeSpentIn('buffering'),