Skip to content

Commit

Permalink
Make @startNumber implementation consistent.
Browse files Browse the repository at this point in the history
* For SegmentTemplate with @duration, treat @startNumber as the number
  of segments absent from the start of the Period.
* For SegmentList, fix off-by-one error when computing segment start times.
* Add more @startNumber documentation and unit tests.

Issue #192

Change-Id: I0b7950cc6cda0a2cbd345e13bd769a50f83d4982
  • Loading branch information
Timothy Drews authored and Gerrit Code Review committed Nov 5, 2015
1 parent 6263e1f commit 29f8022
Show file tree
Hide file tree
Showing 9 changed files with 609 additions and 183 deletions.
20 changes: 18 additions & 2 deletions lib/dash/duration_segment_index_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ goog.provide('shaka.dash.DurationSegmentIndexSource');

goog.require('shaka.asserts');
goog.require('shaka.dash.DynamicLiveSegmentIndex');
goog.require('shaka.dash.MpdUtils');
goog.require('shaka.features');
goog.require('shaka.log');
goog.require('shaka.media.ISegmentIndexSource');
Expand Down Expand Up @@ -129,9 +130,24 @@ shaka.dash.DurationSegmentIndexSource.prototype.createStaticSegmentIndex_ =
var scaledSegmentDuration =
segmentTemplate.segmentDuration / segmentTemplate.timescale;

var numSegments = Math.ceil(this.period_.duration / scaledSegmentDuration);
// SegmentTemplate with @duration implicitly lists all segments in the
// Period. So, @startNumber effectively indicates the number of segments
// which are absent from the start of the Period.
// (See section 5.3.9.5.3 of the DASH spec.)
var numSegmentsInPeriod =
Math.ceil(this.period_.duration / scaledSegmentDuration);
if (segmentTemplate.startNumber > numSegmentsInPeriod) {
shaka.log.error(
'The first segment starts after the end of the Period',
'(because @startNumber > 1). @startNumber seems to be incorrect.');
return null;
}

var references = shaka.dash.MpdUtils.generateSegmentReferences(
this.networkCallback_, this.representation_, 1, numSegments);
this.networkCallback_,
this.representation_,
segmentTemplate.startNumber,
numSegmentsInPeriod - (segmentTemplate.startNumber - 1));
if (!references) {
return null;
}
Expand Down
42 changes: 35 additions & 7 deletions lib/dash/dynamic_live_segment_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ goog.provide('shaka.dash.DynamicLiveSegmentIndex');

goog.require('shaka.asserts');
goog.require('shaka.dash.LiveSegmentIndex');
goog.require('shaka.dash.MpdUtils');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.util.ArrayUtils');
Expand Down Expand Up @@ -148,7 +149,7 @@ shaka.dash.DynamicLiveSegmentIndex.computeAvailableSegmentRange_ =
shaka.asserts.assert(representation.segmentTemplate.timescale > 0);

if (mpd.availabilityStartTime > manifestCreationTime) {
shaka.log.warning('The stream is not available yet!', period);
shaka.log.warning('The stream is not available yet!', representation);
return null;
}

Expand Down Expand Up @@ -191,7 +192,7 @@ shaka.dash.DynamicLiveSegmentIndex.computeAvailableSegmentRange_ =
var currentPresentationTime =
manifestCreationTime - (mpd.availabilityStartTime + period.start);
if (currentPresentationTime < 0) {
shaka.log.warning('The Period is not available yet!', period);
shaka.log.warning('The Period is not available yet!', representation);
return null;
}

Expand All @@ -214,6 +215,8 @@ shaka.dash.DynamicLiveSegmentIndex.computeAvailableSegmentRange_ =
(2 * scaledSegmentDuration) -
timeShiftBufferDepth;
if (earliestAvailableTimestamp < 0) {
shaka.log.v1(
'The earliest available segment is not available yet.', representation);
earliestAvailableTimestamp = 0;
}

Expand All @@ -233,7 +236,8 @@ shaka.dash.DynamicLiveSegmentIndex.computeAvailableSegmentRange_ =
var latestAvailableTimestamp = currentPresentationTime -
scaledSegmentDuration;
if (latestAvailableTimestamp < 0) {
shaka.log.warning('The first segment is not available yet!', period);
shaka.log.error(
'The current segment is not available yet!', representation);
return null;
}

Expand All @@ -248,10 +252,10 @@ shaka.dash.DynamicLiveSegmentIndex.computeAvailableSegmentRange_ =
var bestAvailableTimestamp = latestAvailableSegmentStartTime -
suggestedPresentationDelay;
if (bestAvailableTimestamp < 0) {
shaka.log.warning('The first segment may not be available yet.');
shaka.log.v1('bestAvailableTimestamp < 0');
bestAvailableTimestamp = 0;
// Don't return; taking into account @suggestedPresentationDelay is only a
// reccomendation. The first segment /might/ be available.
// recommendation. The first segment /might/ be available.
}

var bestAvailableSegmentStartTime =
Expand All @@ -263,25 +267,49 @@ shaka.dash.DynamicLiveSegmentIndex.computeAvailableSegmentRange_ =
var currentSegmentStartTime;
if (bestAvailableSegmentStartTime >= earliestAvailableSegmentStartTime) {
currentSegmentStartTime = bestAvailableSegmentStartTime;
shaka.log.v1('The best available segment is still available!');
shaka.log.debug('The best available segment is still available.');
} else {
// @suggestedPresentationDelay is large compared to @timeShiftBufferDepth,
// so we can't start as far back as we'd like.
currentSegmentStartTime = earliestAvailableSegmentStartTime;
shaka.log.v1('The best available segment is no longer available.');
shaka.log.debug('The best available segment is no longer available.');
}

// Now compute the segment numbers.
//
// SegmentTemplate with @duration implicitly lists all segments in the
// Period. So, @startNumber effectively indicates the number of segments
// which are absent from the start of the Period.
// (See section 5.3.9.5.3 of the DASH spec.)
var earliestSegmentNumber =
(earliestAvailableSegmentStartTime / scaledSegmentDuration) + 1;
shaka.asserts.assert(
earliestSegmentNumber == Math.round(earliestSegmentNumber),
'earliestSegmentNumber should be an integer.');
if (earliestSegmentNumber < segmentTemplate.startNumber) {
shaka.log.warning(
'The earliest available segment may not be available yet',
'(because @startNumber > 1).',
'@startNumber may be incorrect.',
representation);
// Don't return; the manifest is probably incorrect but the content may
// still be playable.
}

var currentSegmentNumber =
(currentSegmentStartTime / scaledSegmentDuration) + 1;
shaka.asserts.assert(
currentSegmentNumber == Math.round(currentSegmentNumber),
'currentSegmentNumber should be an integer.');
if (currentSegmentNumber < segmentTemplate.startNumber) {
shaka.log.warning(
'The current segment may not be available yet',
'(because @startNumber > 1).',
'@startNumber may be incorrect.',
representation);
// Don't return; the manifest is probably incorrect but the content may
// still be playable.
}

shaka.log.v1('earliestSegmentNumber', earliestSegmentNumber);
shaka.log.v1('currentSegmentNumber', currentSegmentNumber);
Expand Down
3 changes: 2 additions & 1 deletion lib/dash/list_segment_index_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ goog.provide('shaka.dash.ListSegmentIndexSource');

goog.require('shaka.asserts');
goog.require('shaka.dash.LiveSegmentIndex');
goog.require('shaka.dash.MpdUtils');
goog.require('shaka.features');
goog.require('shaka.log');
goog.require('shaka.media.ISegmentIndexSource');
Expand Down Expand Up @@ -110,7 +111,7 @@ shaka.dash.ListSegmentIndexSource.prototype.create = function() {
// Calculate an initial start time.
var lastEndTime = 0;
if (segmentList.segmentDuration && segmentList.startNumber) {
lastEndTime = segmentList.segmentDuration * segmentList.startNumber;
lastEndTime = (segmentList.startNumber - 1) * segmentList.segmentDuration;
} else if (timeline.length > 0) {
// Align the SegmentReferences to the Period's start
// (see shaka.dash.TimelineSegmentIndexSource.create).
Expand Down
64 changes: 58 additions & 6 deletions lib/dash/mpd_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -558,8 +558,14 @@ shaka.dash.mpd.SegmentList = function() {
this.segmentDuration = null;

/**
* The segment number origin. This value may be zero.
* @type {?number}
* The segment number (one-based) of the first segment specified in the
* SegmentList's corresponding Representation, relative to the start of the
* Representation's Period.
*
* Please see shaka.dash.mpd.SegmentTemplate.zeroBasedSegmentNumbers for
* additional information.
*
* @type {number}
*/
this.startNumber = 1;

Expand Down Expand Up @@ -645,11 +651,35 @@ shaka.dash.mpd.SegmentTemplate = function() {
this.segmentDuration = null;

/**
* The segment number origin. This value may be zero.
* @type {?number}
* The segment number (one-based) of the first segment specified in the
* SegmentTemplates's corresponding Representation, relative to the start of
* the Representation's Period.
* @type {number}
*/
this.startNumber = 1;

/**
* True if the manifest uses zero-based segment numbers; false if the
* manifest uses one-based segment numbers.
*
* The DASH spec. does not explicitly indicate if manifests should use
* zero-based segment numbers or one-based segment numbers; however, the
* spec. seems to prefer one-based segment numbers. We assume manifests use
* one-based segment numbers unless the manifest specifies startNumber == 0.
*
* So, manifests which use zero-based segment numbers and specify
* startNumber > 0 will cause the player to interpret the first segment
* listed in a Representation as the startNumber'th segment relative to the
* start of the Representation's Period, instead of the (startNumber - 1)'th
* segment. So, we encourage manifests to use one-based segment numbers.
*
* We only need this property so we can compute $Number$ for manifests that
* use zero-based segment numbers instead of one-based segment numbers.
*
* @type {boolean}
*/
this.zeroBasedSegmentNumbers = false;

/** @type {?string} */
this.mediaUrlTemplate = null;

Expand Down Expand Up @@ -677,6 +707,7 @@ shaka.dash.mpd.SegmentTemplate.prototype.clone = function() {
clone.presentationTimeOffset = this.presentationTimeOffset;
clone.segmentDuration = this.segmentDuration;
clone.startNumber = this.startNumber;
clone.zeroBasedSegmentNumbers = this.zeroBasedSegmentNumbers;
clone.mediaUrlTemplate = this.mediaUrlTemplate;
clone.indexUrlTemplate = this.indexUrlTemplate;
clone.initializationUrlTemplate = this.initializationUrlTemplate;
Expand Down Expand Up @@ -1319,8 +1350,18 @@ shaka.dash.mpd.SegmentList.prototype.parse = function(parent, elem) {
this.segmentDuration = mpd.parseAttr_(
elem, 'duration', mpd.parsePositiveInt_, this.segmentDuration);

this.startNumber = mpd.parseAttr_(
var startNumber = mpd.parseAttr_(
elem, 'startNumber', mpd.parseNonNegativeInt_, this.startNumber);
if (startNumber != null) {
if (startNumber > 0) {
this.startNumber = startNumber;
} else {
shaka.log.warning(
'SegmentList specifies @startNumber=0.',
'Please consider using one-based segment numbers.');
this.startNumber = 1;
}
}

// Parse simple children
this.initialization =
Expand Down Expand Up @@ -1374,8 +1415,19 @@ shaka.dash.mpd.SegmentTemplate.prototype.parse = function(parent, elem) {
this.segmentDuration = mpd.parseAttr_(
elem, 'duration', mpd.parsePositiveInt_, this.segmentDuration);

this.startNumber = mpd.parseAttr_(
var startNumber = mpd.parseAttr_(
elem, 'startNumber', mpd.parseNonNegativeInt_, this.startNumber);
if (startNumber != null) {
if (startNumber > 0) {
this.startNumber = startNumber;
} else {
shaka.log.warning(
'SegmentTemplate specifies @startNumber=0.',
'Please consider using one-based segment numbers.');
this.startNumber = 1;
this.zeroBasedSegmentNumbers = true;
}
}

this.mediaUrlTemplate = mpd.parseAttr_(
elem, 'media', mpd.parseString_, this.mediaUrlTemplate);
Expand Down
13 changes: 9 additions & 4 deletions lib/dash/mpd_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,15 @@ shaka.dash.MpdUtils.generateSegmentReferences = function(
var scaledStartTime = startTime / segmentTemplate.timescale;
var scaledEndTime = endTime / segmentTemplate.timescale;

// Compute the media URL template placeholder replacements. Note
// that |segmentReplacement| may be zero.
var segmentReplacement = (segmentNumber - 1) + segmentTemplate.startNumber;
var timeReplacement = (segmentNumber - 1) * segmentTemplate.segmentDuration;
// Compute the media URL template placeholder replacements.
//
// To compute $Number$ we must subtract 1 for manifests that use
// zero-based segment numbers, see
// shaka.dash.mpd.SegmentTemplate.zeroBasedSegmentNumbers.
var segmentReplacement = segmentTemplate.zeroBasedSegmentNumbers ?
segmentNumber - 1 :
segmentNumber;
var timeReplacement = startTime;

// Generate the media URL.
var mediaUrl = shaka.dash.MpdUtils.createFromTemplate(
Expand Down
3 changes: 3 additions & 0 deletions lib/dash/timeline_segment_index_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ shaka.dash.TimelineSegmentIndexSource.prototype.create = function() {

// Compute the media URL template placeholder replacements. Note
// that |segmentReplacement| may be zero.
//
// Note: i = k - 1, where k indicates the k'th segment listed in the MPD.
// (See section 5.3.9.5.3 of the DASH spec.)
var segmentReplacement = i + segmentTemplate.startNumber;
var timeReplacement = startTime;

Expand Down
39 changes: 39 additions & 0 deletions spec/mpd_processor_segment_list_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,45 @@ describe('MpdProcessor.SegmentList', function() {
shaka.player.Player.isTypeSupported = originalIsTypeSupported;
});

it('uses @startNumber correctly', function(done) {
sl.timescale = 9000;
sl.startNumber = 5;
sl.segmentDuration = 9000 * 10;

var segmentUrl1 = new shaka.dash.mpd.SegmentUrl();
segmentUrl1.mediaUrl = [new goog.Uri('http://example.com/video_5.mp4')];

var segmentUrl2 = new shaka.dash.mpd.SegmentUrl();
segmentUrl2.mediaUrl = [new goog.Uri('http://example.com/video_6.mp4')];

sl.segmentUrls.push(segmentUrl1);
sl.segmentUrls.push(segmentUrl2);

p.start = 0;

manifestInfo = processor.process(m);

var periodInfo = manifestInfo.periodInfos[0];
var si1 = periodInfo.streamSetInfos[0].streamInfos[0];

si1.segmentIndexSource.create().then(function(segmentIndex) {
var references = segmentIndex.references;
expect(references.length).toBe(2);

checkReference(
references[0],
'http://example.com/video_5.mp4',
40, 50);

checkReference(
references[1],
'http://example.com/video_6.mp4',
50, 60);

done();
});
});

it('allows no segment duration with one segment', function(done) {
sl.timescale = 9000;

Expand Down
Loading

0 comments on commit 29f8022

Please sign in to comment.