Skip to content

Commit

Permalink
Tolerate drift in DASH live streams
Browse files Browse the repository at this point in the history
This calculates the DASH live edge from explicit segment descriptions
when we have them.  The net effect is that availabilityStartTime will
only be used with SegmentTemplate+duration, in which we have no
explicit segment times.

With this, a DASH live stream experiencing encoder drift can still be
played so long as we know the segment times.  This makes playback of
DASH live streams work more like HLS live streams.

Please note that DASH live streams using SegmentTemplate+duration may
still suffer from encoder drift.

This change also:
  - Avoids DASH clock sync when availabilityStartTime is not used,
    which should reduce startup latency
  - Removes the simulation of a presentation start time for HLS live
  - Renames some variables and improves comments for clarity
  - Fixes some brittle tests that made bad assumptions or instrumented
    the wrong methods
  - Adds new tests that show the new behavior in PresentationTimeline

Closes #999

Change-Id: I21d7f3ccc81c9d9e218857a9b41882a7609ca36a
  • Loading branch information
joeyparrish committed Aug 7, 2018
1 parent 0726543 commit 7a1c662
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 97 deletions.
37 changes: 17 additions & 20 deletions lib/dash/dash_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ shaka.dash.DashParser.prototype.parseManifest_ =
* @private
*/
shaka.dash.DashParser.prototype.processManifest_ =
function(mpd, finalManifestUri) {
async function(mpd, finalManifestUri) {
const Functional = shaka.util.Functional;
const XmlUtils = shaka.util.XmlUtils;
const ManifestParserUtils = shaka.util.ManifestParserUtils;
Expand Down Expand Up @@ -571,31 +571,28 @@ shaka.dash.DashParser.prototype.processManifest_ =
presentationTimeline.notifyMaxSegmentDuration(maxSegmentDuration || 1);
if (goog.DEBUG) presentationTimeline.assertIsValid();

if (this.manifest_) {
// This is a manifest update, so we're done.
return Promise.resolve();
}

// This is the first manifest parse, so we cannot return until we calculate
// the clock offset.
let timingElements = XmlUtils.findChildren(mpd, 'UTCTiming');

return this.parseUtcTiming_(
baseUris, timingElements, isLive).then(function(offset) {
// Detect calls to stop().
if (!this.playerInterface_) {
return;
}

presentationTimeline.setClockOffset(offset);

// These steps are not done on manifest update.
if (!this.manifest_) {
this.manifest_ = {
presentationTimeline: presentationTimeline,
periods: periods,
offlineSessionIds: [],
minBufferTime: minBufferTime || 0,
};
}.bind(this));

// We only need to do clock sync when we're using presentation start time.
// This condition also excludes VOD streams.
if (presentationTimeline.usingPresentationStartTime()) {
let timingElements = XmlUtils.findChildren(mpd, 'UTCTiming');
const offset =
await this.parseUtcTiming_(baseUris, timingElements, isLive);
// Detect calls to stop().
if (!this.playerInterface_) {
return;
}
presentationTimeline.setClockOffset(offset);
}
}
};


Expand Down
62 changes: 39 additions & 23 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,23 +326,17 @@ shaka.hls.HlsParser.prototype.parseManifest_ = function(data, uri) {
}
}

// This assert is our own sanity check.
goog.asserts.assert(this.presentationTimeline_ == null,
'Presentation timeline created early!');
this.createPresentationTimeline_(maxLastTimestamp);

if (this.isLive_()) {
// The HLS spec (RFC 8216) states in 6.3.3:
//
// "The client SHALL choose which Media Segment to play first ... the
// client SHOULD NOT choose a segment that starts less than three target
// durations from the end of the Playlist file. Doing so can trigger
// playback stalls."
//
// We accomplish this in our DASH-y model by setting a presentation delay
// of 3 segments. This will be the "live edge" of the presentation.
let threeSegmentDurations = this.maxTargetDuration_ * 3;
this.presentationTimeline_.setDelay(threeSegmentDurations);
// This assert satisfies the compiler that it is not null for the rest of
// the method.
goog.asserts.assert(this.presentationTimeline_,
'Presentation timeline not created!');

if (this.isLive_()) {
// The HLS spec (RFC 8216) states in 6.3.4:
// "the client MUST wait for at least the target duration before
// attempting to reload the Playlist file again"
Expand All @@ -356,10 +350,16 @@ shaka.hls.HlsParser.prototype.parseManifest_ = function(data, uri) {
const PresentationType = shaka.hls.HlsParser.PresentationType_;

if (this.presentationType_ == PresentationType.LIVE) {
let segmentAvailabilityDuration = threeSegmentDurations;
// This defaults to the presentation delay, which has the effect of
// making the live stream unseekable. This is consistent with Apple's
// HLS implementation.
let segmentAvailabilityDuration = this.presentationTimeline_.getDelay();

// The app can override that with a longer duration, to allow seeking.
if (!isNaN(this.config_.availabilityWindowOverride)) {
segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
}

this.presentationTimeline_.setSegmentAvailabilityDuration(
segmentAvailabilityDuration);
}
Expand Down Expand Up @@ -1107,20 +1107,36 @@ shaka.hls.HlsParser.prototype.determinePresentationType_ = function(playlist) {
*/
shaka.hls.HlsParser.prototype.createPresentationTimeline_ =
function(lastTimestamp) {
let presentationStartTime = null;
let delay = 0;

if (this.isLive_()) {
presentationStartTime = (Date.now() / 1000) - lastTimestamp;

// We should have a delay of at least 3 target durations.
delay = this.maxTargetDuration_ * 3;
// The live edge will be calculated from segments, so we don't need to set
// a presentation start time. We will assert later that this is working as
// expected.

// The HLS spec (RFC 8216) states in 6.3.3:
//
// "The client SHALL choose which Media Segment to play first ... the
// client SHOULD NOT choose a segment that starts less than three target
// durations from the end of the Playlist file. Doing so can trigger
// playback stalls."
//
// We accomplish this in our DASH-y model by setting a presentation delay
// of 3 segments. This will be the "live edge" of the presentation.
this.presentationTimeline_ = new shaka.media.PresentationTimeline(
/* presentationStartTime */ 0, /* delay */ this.maxTargetDuration_ * 3);
this.presentationTimeline_.setStatic(false);
} else {
this.presentationTimeline_ = new shaka.media.PresentationTimeline(
/* presentationStartTime */ null, /* delay */ 0);
this.presentationTimeline_.setStatic(true);
}

this.presentationTimeline_ = new shaka.media.PresentationTimeline(
presentationStartTime, delay);
this.presentationTimeline_.setStatic(!this.isLive_());
this.notifySegments_();

// This asserts that the live edge is being calculated from segment times.
// For VOD and event streams, this check should still pass.
goog.asserts.assert(
!this.presentationTimeline_.usingPresentationStartTime(),
'We should not be using the presentation start time in HLS!');
};


Expand Down
79 changes: 73 additions & 6 deletions lib/media/presentation_timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ shaka.media.PresentationTimeline = function(
/** @private {number} */
this.maxFirstSegmentStartTime_ = 0;

/**
* Max segment end time for segments we explicitly know about.
* This is null if we have no explicit descriptions of segments, such as in
* DASH when using SegmentTemplate w/ duration. When this is non-null, the
* presentation start time is calculated from the segment end times.
* @private {?number}
*/
this.maxSegmentEndTime_ = null;

/** @private {number} */
this.clockOffset_ = 0;

Expand Down Expand Up @@ -152,17 +161,30 @@ shaka.media.PresentationTimeline.prototype.setSegmentAvailabilityDuration =


/**
* Sets the presentation delay.
* Sets the presentation delay in seconds.
*
* @param {number} delay
* @export
*/
shaka.media.PresentationTimeline.prototype.setDelay = function(delay) {
// NOTE: This is no longer used internally, but is exported.
// So we cannot remove it without deprecating it and waiting one release
// cycle, or else we risk breaking custom manifest parsers.
goog.asserts.assert(delay >= 0, 'delay must be >= 0');
this.presentationDelay_ = delay;
};


/**
* Gets the presentation delay in seconds.
* @return {number}
* @export
*/
shaka.media.PresentationTimeline.prototype.getDelay = function() {
return this.presentationDelay_;
};


/**
* Gives PresentationTimeline a Stream's segments so it can size and position
* the segment availability window, and account for missing segment
Expand All @@ -188,6 +210,19 @@ shaka.media.PresentationTimeline.prototype.notifySegments = function(
function(max, r) { return Math.max(max, r.endTime - r.startTime); },
this.maxSegmentDuration_);

this.maxSegmentEndTime_ =
Math.max(this.maxSegmentEndTime_,
references[references.length - 1].endTime);

if (this.presentationStartTime_ != null) {
// Since we have explicit segment end times, calculate a presentation start
// based on them. This start time accounts for drift.
// Date.now() is in milliseconds, from which we compute "now" in seconds.
let now = (Date.now() + this.clockOffset_) / 1000.0;
this.presentationStartTime_ =
now - this.maxSegmentEndTime_ - this.maxSegmentDuration_;
}

shaka.log.v1('notifySegments:',
'maxSegmentDuration=' + this.maxSegmentDuration_);
};
Expand Down Expand Up @@ -302,22 +337,26 @@ shaka.media.PresentationTimeline.prototype.getSegmentAvailabilityEnd =
*/
shaka.media.PresentationTimeline.prototype.getSafeSeekRangeStart = function(
offset) {
// The start of the seek window, ignoring segment availability duration.
const seekStart =
// The earliest known segment time, ignoring segment availability duration.
const earliestSegmentTime =
Math.max(this.maxFirstSegmentStartTime_, this.userSeekStart_);
if (this.segmentAvailabilityDuration_ == Infinity) {
return seekStart;
return earliestSegmentTime;
}

// AKA the live edge for live streams.
const availabilityEnd = this.getSegmentAvailabilityEnd();

// The ideal availability start, not considering known segments.
const availabilityStart = availabilityEnd - this.segmentAvailabilityDuration_;

// Add the offset to the availability start to ensure that we don't fall
// outside the availability window while we buffer; we don't need to add the
// offset to seekStart since that won't change over time.
// offset to earliestSegmentTime since that won't change over time.
// Also see: https://github.com/google/shaka-player/issues/692
const desiredStart =
Math.min(availabilityStart + offset, this.getSeekRangeEnd());
return Math.max(seekStart, desiredStart);
return Math.max(earliestSegmentTime, desiredStart);
};


Expand Down Expand Up @@ -345,13 +384,41 @@ shaka.media.PresentationTimeline.prototype.getSeekRangeEnd = function() {
};


/**
* True if the presentation start time is being used to calculate the live edge.
* Using the presentation start time means that the stream may be subject to
* encoder drift. At runtime, we will avoid using the presentation start time
* whenever possible.
*
* @return {boolean}
* @export
*/
shaka.media.PresentationTimeline.prototype.usingPresentationStartTime =
function() {
// If it's VOD, IPR, or an HLS "event", we are not using the presentation
// start time.
if (this.presentationStartTime_ == null) {
return false;
}

// If we have explicit segment times, we're not using the presentation
// start time.
if (this.maxSegmentEndTime_ != null) {
return false;
}

return true;
};


/**
* @return {number} The current presentation time in seconds.
* @private
*/
shaka.media.PresentationTimeline.prototype.getLiveEdge_ = function() {
goog.asserts.assert(this.presentationStartTime_ != null,
'Cannot compute timeline live edge without start time');
// Date.now() is in milliseconds, from which we compute "now" in seconds.
let now = (Date.now() + this.clockOffset_) / 1000.0;
return Math.max(
0, now - this.maxSegmentDuration_ - this.presentationStartTime_);
Expand Down
32 changes: 18 additions & 14 deletions test/dash/dash_parser_live_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ describe('DashParser Live', function() {
it('evicts old references for single-period live stream', function(done) {
let template = [
'<MPD type="dynamic" minimumUpdatePeriod="PT%(updateTime)dS"',
' timeShiftBufferDepth="PT1S"',
' timeShiftBufferDepth="PT30S"',
' suggestedPresentationDelay="PT5S"',
' availabilityStartTime="1970-01-01T00:00:00Z">',
' <Period id="1">',
Expand Down Expand Up @@ -211,9 +211,12 @@ describe('DashParser Live', function() {
expect(stream.findSegmentPosition(0)).toBe(1);
ManifestParser.verifySegmentIndex(stream, basicRefs);

// 15 seconds for @timeShiftBufferDepth and the first segment
// duration.
Date.now = function() { return 2 * 15 * 1000; };
// The 30 second availability window is initially full in all cases
// (SegmentTemplate+Timeline, etc.) The first segment is always 10
// seconds long in all of these cases. So 11 seconds after the
// manifest was parsed, the first segment should have fallen out of
// the availability window.
Date.now = function() { return 11 * 1000; };
delayForUpdatePeriod();
// The first reference should have been evicted.
expect(stream.findSegmentPosition(0)).toBe(2);
Expand All @@ -225,7 +228,7 @@ describe('DashParser Live', function() {
it('evicts old references for multi-period live stream', function(done) {
let template = [
'<MPD type="dynamic" minimumUpdatePeriod="PT%(updateTime)dS"',
' timeShiftBufferDepth="PT1S"',
' timeShiftBufferDepth="PT30S"',
' suggestedPresentationDelay="PT5S"',
' availabilityStartTime="1970-01-01T00:00:00Z">',
' <Period id="1">',
Expand Down Expand Up @@ -266,16 +269,19 @@ describe('DashParser Live', function() {
ManifestParser.verifySegmentIndex(stream1, basicRefs);
ManifestParser.verifySegmentIndex(stream2, basicRefs);

// 15 seconds for @timeShiftBufferDepth and the first segment
// duration.
Date.now = function() { return 2 * 15 * 1000; };
// The 30 second availability window is initially full in all cases
// (SegmentTemplate+Timeline, etc.) The first segment is always 10
// seconds long in all of these cases. So 11 seconds after the
// manifest was parsed, the first segment should have fallen out of
// the availability window.
Date.now = function() { return 11 * 1000; };
delayForUpdatePeriod();
// The first reference should have been evicted.
ManifestParser.verifySegmentIndex(stream1, basicRefs.slice(1));
ManifestParser.verifySegmentIndex(stream2, basicRefs);

// Same as above, but 1 period length later
Date.now = function() { return (2 * 15 + pStart) * 1000; };
Date.now = function() { return (11 + pStart) * 1000; };
delayForUpdatePeriod();
ManifestParser.verifySegmentIndex(stream1, []);
ManifestParser.verifySegmentIndex(stream2, basicRefs.slice(1));
Expand Down Expand Up @@ -753,8 +759,7 @@ describe('DashParser Live', function() {
expect(manifest).toBeTruthy();
let timeline = manifest.presentationTimeline;
expect(timeline).toBeTruthy();
expect(timeline.getSegmentAvailabilityStart()).toBe(165);
expect(timeline.getSegmentAvailabilityEnd()).toBe(285);
expect(timeline.getMaxSegmentDuration()).toBe(15);
}).catch(fail).then(done);
PromiseMock.flush();
});
Expand Down Expand Up @@ -831,12 +836,11 @@ describe('DashParser Live', function() {
expect(timeline).toBeTruthy();

// NOTE: the largest segment is 8 seconds long in each test.
expect(timeline.getSegmentAvailabilityStart()).toBe(172);
expect(timeline.getSegmentAvailabilityEnd()).toBe(292);
expect(timeline.getMaxSegmentDuration()).toBe(8);
}).catch(fail).then(done);
PromiseMock.flush();
}
});
}); // describe('maxSegmentDuration')

describe('stop', function() {
const manifestRequestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
Expand Down
Loading

0 comments on commit 7a1c662

Please sign in to comment.