diff --git a/externs/shaka/player.js b/externs/shaka/player.js index e76bdac6ed..e56f2d851e 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -468,9 +468,58 @@ shakaExtern.HlsManifestConfiguration; shakaExtern.ManifestConfiguration; +/** + * @typedef {{ + * adjustOffset: boolean, + * adjustWindowEnd: boolean, + * bFrameCount: number + * }} + * + * @description + * This configuration block can be used to account for a bug in chrome that + * uses the DTS instead of the PTS for timestamp comparisons in the MSE. + * https://bugs.chromium.org/p/chromium/issues/detail?id=398130 + * + * There are two problems that arise from this bug: + * + * 1. The presentationTimeOffset (PTO) will be set too far in the future since + * the DTS falls before the PTS. This will lead to a negative starting value + * for the presentation time, which, in Chrome throws error 3014. This + * will only happen if the base_media_decode_time in your tftd is set + * (correctly) to the DTS. ffmpeg, for example, will set it to the PTS by + * default, which would mask the aforementioned bug. + * 2. Periods will get incorrect durations, leading to gaps in the timeline. + * This happens because the Chrome MSE expects appendWindowEnd to be in DTS + * time, and thus it filters frames after the last DTS, which will always + * be count(bframes) before the last PTS. + * + * The bframe count is typically an encoder configuration option, and can be + * held constant within fragments. Given the number of bframes, and the + * framerate, shaka can adjust the windowEnd, and the timestampOffset to + * account for Chrome's bug. To address (1) above, set adjustOffset to true, + * and to address (2), set adjustWindowEnd to true. You must provide an accurate + * bFrameCount for this to work correctly. + * + * Shaka will use the frameRate provided by the manifest to calculate frame + * durations. + * + * @property {boolean} adjustOffset + * If true, adjust the timestampOffset on the MSE to account for bframes. + * Use this if your base_media_decode time is set to the DTS. + * @property {boolean} adjustWindowEnd + * If true, adjust the windowEnd on the MSE to accounbt for bframes. + * Use this to correct your period durations. + * @property {number} bFrameCount + * The number of bframes in each segment. + * @exportDoc + */ +shakaExtern.BFrameAdjustment; + + /** * @typedef {{ * retryParameters: shakaExtern.RetryParameters, + * bFrameAdjustment: shakaExtern.BFrameAdjustment, * rebufferingGoal: number, * bufferingGoal: number, * bufferBehind: number, @@ -483,6 +532,8 @@ shakaExtern.ManifestConfiguration; * @description * The StreamingEngine's configuration options. * + * @property {shakaExtern.BFrameAdjustment} bFrameAdjustment + * Configuration to address Chrome PTS/DTS bug * @property {shakaExtern.RetryParameters} retryParameters * Retry parameters for segment requests. * @property {number} rebufferingGoal diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index 4284fe4fb1..b09c44d673 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -41,12 +41,15 @@ goog.require('shaka.util.PublicPromise'); * @param {MediaSource} mediaSource The MediaSource, which must be in the * 'open' state. * @param {TextTrack} textTrack The TextTrack to use for subtitles/captions. + * @param {shakaExtern.BFrameAdjustment} bFrameAdjustment Configuration options + * for adjusting the timestampOffset and windowEnd to account for bframes. * * @struct * @constructor * @implements {shaka.util.IDestroyable} */ -shaka.media.MediaSourceEngine = function(video, mediaSource, textTrack) { +shaka.media.MediaSourceEngine = + function(video, mediaSource, textTrack, bFrameAdjustment) { goog.asserts.assert(mediaSource.readyState == 'open', 'The MediaSource should be in the \'open\' state.'); @@ -59,10 +62,16 @@ shaka.media.MediaSourceEngine = function(video, mediaSource, textTrack) { /** @private {TextTrack} */ this.textTrack_ = textTrack; + /** @private {shakaExtern.BFrameAdjustment} */ + this.bFrameAdjustment_ = bFrameAdjustment; + /** @private {!Object.} */ this.sourceBuffers_ = {}; + /** @private {?number} */ + this.videoFrameRate_ = null; + /** @private {shaka.media.TextEngine} */ this.textEngine_ = null; @@ -457,10 +466,11 @@ shaka.media.MediaSourceEngine.prototype.flush = function(contentType) { * value does not affect segments which have already been inserted. * @param {?number} appendWindowEnd The timestamp to set the append window end * to. Media beyond this value will be truncated. + * @param {?number} frameRate The framerate; only provided by video tracks * @return {!Promise} */ shaka.media.MediaSourceEngine.prototype.setStreamProperties = function( - contentType, timestampOffset, appendWindowEnd) { + contentType, timestampOffset, appendWindowEnd, frameRate) { var ContentType = shaka.util.ManifestParserUtils.ContentType; if (contentType == ContentType.TEXT) { this.textEngine_.setTimestampOffset(timestampOffset); @@ -469,6 +479,10 @@ shaka.media.MediaSourceEngine.prototype.setStreamProperties = function( return Promise.resolve(); } + if (contentType == ContentType.VIDEO) { + this.videoFrameRate_ = frameRate; + } + if (appendWindowEnd == null) appendWindowEnd = Infinity; @@ -634,7 +648,17 @@ shaka.media.MediaSourceEngine.prototype.flush_ = function(contentType) { */ shaka.media.MediaSourceEngine.prototype.setTimestampOffset_ = function(contentType, timestampOffset) { - this.sourceBuffers_[contentType].timestampOffset = timestampOffset; + var ContentType = shaka.util.ManifestParserUtils.ContentType; + var fudge = 0; + + if (contentType == ContentType.VIDEO && + this.bFrameAdjustment_.adjustOffset) { + goog.asserts.assert(this.videoFrameRate_, + 'Must provide frameRate for offset bframe adjustment'); + fudge = this.bFrameAdjustment_.bFrameCount / this.videoFrameRate_; + } + + this.sourceBuffers_[contentType].timestampOffset = timestampOffset + fudge; // Fake 'updateend' event to resolve the operation. this.onUpdateEnd_(contentType); @@ -649,7 +673,16 @@ shaka.media.MediaSourceEngine.prototype.setTimestampOffset_ = */ shaka.media.MediaSourceEngine.prototype.setAppendWindowEnd_ = function(contentType, appendWindowEnd) { - var fudge = 1 / 25; // one frame, assuming a low framerate + var ContentType = shaka.util.ManifestParserUtils.ContentType; + var fudge = 1 / 25; // one frame, assuming a low framerate + if (contentType == ContentType.VIDEO && + this.bFrameAdjustment_.adjustWindowEnd) { + goog.asserts.assert(this.videoFrameRate_, + 'Must provide frameRate for windowEnd bframe adjustment'); + // windowEnd is exclusive, so we need one extra frame on the end + fudge = (this.bFrameAdjustment_.bFrameCount + 1) / this.videoFrameRate_; + } + this.sourceBuffers_[contentType].appendWindowEnd = appendWindowEnd + fudge; // Fake 'updateend' event to resolve the operation. diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index 45cc59b014..1403585de0 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -1385,7 +1385,8 @@ shaka.media.StreamingEngine.prototype.initSourceBuffer_ = function( shaka.log.v1(logPrefix, 'setting append window end to ' + appendWindowEnd); var setStreamProperties = this.playerInterface_.mediaSourceEngine.setStreamProperties( - mediaState.type, timestampOffset, appendWindowEnd); + mediaState.type, timestampOffset, appendWindowEnd, + mediaState.stream.frameRate || null); if (!mediaState.stream.initSegmentReference) { // The Stream is self initializing. diff --git a/lib/player.js b/lib/player.js index c0df892d66..37de1f8422 100644 --- a/lib/player.js +++ b/lib/player.js @@ -698,7 +698,8 @@ shaka.Player.prototype.createMediaSource = function() { */ shaka.Player.prototype.createMediaSourceEngine = function() { return new shaka.media.MediaSourceEngine( - this.video_, this.mediaSource_, this.textTrack_); + this.video_, this.mediaSource_, this.textTrack_, + this.config_.streaming.bFrameAdjustment); }; @@ -1725,7 +1726,13 @@ shaka.Player.prototype.defaultConfig_ = function() { bufferingGoal: 10, bufferBehind: 30, ignoreTextStreamFailures: false, + useRelativeCueTimestamps: false, startAtSegmentBoundary: false, + bFrameAdjustment: { + adjustOffset: true, + adjustWindowEnd: true, + bFrameCount: 2 + }, smallGapLimit: 0.5, jumpLargeGaps: false },