From f1c5a1c19126832184f43b0d08e9503a34b0dac0 Mon Sep 17 00:00:00 2001 From: Nick Crast Date: Fri, 28 Apr 2023 20:20:33 -0400 Subject: [PATCH] feat(dash): Improve DASH SegmentTemplate performance with on-demand segment references (#5061) This is a performance optimization intended to reduce the Video Start Time for DASH streams, both VOD and Live, by reducing the amount of processing done during manifest parse time. This is especially effective for long multi-period assets, assets with many variants, or on low end devices in general. I've provided some measurements showing the performance improvements at the conclusion of this PR. Currently, during manifest parse time for a Segment Template, Shaka will loop through the entire timeline and create segment references for each timeline entry. For a long asset, or an asset with many tracks, this is a significant amount of processing. I've created a new entity called the Timeline Segment Index that extends the SegmentIndex interface. The purpose of the Timeline Segment Index is to ingest a timeline during construction, and then use that information to build Segment References only on demand. This removes the need to parse the entire timeline and create all of the Segment References during parse time. The effects of this change aren't quite as apparent on desktop web browsers, given their speed and power. This improvement really shines on lower end TVs on assets with a long timeline. DASH parsing speed-ups in some cases can be as much as 40%. Co-authored-by: Joey Parrish --- lib/dash/mpd_utils.js | 2 + lib/dash/segment_template.js | 362 ++++++++++++++++-- lib/media/presentation_timeline.js | 42 ++ lib/media/segment_index.js | 41 +- .../dash/dash_parser_segment_template_unit.js | 280 ++++++++++++++ test/media/presentation_timeline_unit.js | 51 +++ 6 files changed, 745 insertions(+), 33 deletions(-) diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index 806485f672..1fd3ca9e92 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -554,6 +554,8 @@ shaka.dash.MpdUtils = class { * The start time of the range in representation timescale units. * @property {number} end * The end time (exclusive) of the range. + * + * @export */ shaka.dash.MpdUtils.TimeRange; diff --git a/lib/dash/segment_template.js b/lib/dash/segment_template.js index 80cb792156..63375a88ef 100644 --- a/lib/dash/segment_template.js +++ b/lib/dash/segment_template.js @@ -14,6 +14,7 @@ goog.require('shaka.media.InitSegmentReference'); goog.require('shaka.media.SegmentIndex'); goog.require('shaka.media.SegmentReference'); goog.require('shaka.util.Error'); +goog.require('shaka.util.IReleasable'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.ObjectUtils'); goog.requireType('shaka.dash.DashParser'); @@ -42,8 +43,11 @@ shaka.dash.SegmentTemplate = class { goog.asserts.assert(context.representation.segmentTemplate, 'Should only be called with SegmentTemplate'); const SegmentTemplate = shaka.dash.SegmentTemplate; + const TimelineSegmentIndex = shaka.dash.TimelineSegmentIndex; const initSegmentReference = SegmentTemplate.createInitSegment_(context); + + /** @type {shaka.dash.SegmentTemplate.SegmentTemplateInfo} */ const info = SegmentTemplate.parseSegmentTemplateInfo_(context); SegmentTemplate.checkSegmentTemplateInfo_(context, info); @@ -94,12 +98,11 @@ shaka.dash.SegmentTemplate = class { } } - const references = SegmentTemplate.createFromTimeline_( - shallowCopyOfContext, info, initSegmentReference); - const periodStart = context.periodInfo.start; - const periodEnd = context.periodInfo.duration ? - context.periodInfo.start + context.periodInfo.duration : Infinity; + const periodEnd = context.periodInfo.duration ? periodStart + + context.periodInfo.duration : Infinity; + + shaka.log.debug(`New manifest ${periodStart} - ${periodEnd}`); /* When to fit segments. All refactors should honor/update this table: * @@ -119,23 +122,32 @@ shaka.dash.SegmentTemplate = class { // most recent segment to the end of the presentation). const shouldFit = !(context.dynamic && context.periodInfo.isLastPeriod); - if (segmentIndex) { - if (shouldFit) { - // Fit the new references before merging them, so that the merge - // algorithm has a more accurate view of their start and end times. - const wrapper = new shaka.media.SegmentIndex(references); - wrapper.fit(periodStart, periodEnd, /* isNew= */ true); - } - - segmentIndex.mergeAndEvict(references, - context.presentationTimeline.getSegmentAvailabilityStart()); + if (!segmentIndex) { + shaka.log.debug(`Creating TSI with end ${periodEnd}`); + segmentIndex = new TimelineSegmentIndex( + info, + context.representation.id, + context.bandwidth, + context.representation.baseUris, + periodStart, + periodEnd, + initSegmentReference, + shouldFit, + ); } else { - segmentIndex = new shaka.media.SegmentIndex(references); + const tsi = /** @type {!TimelineSegmentIndex} */(segmentIndex); + tsi.appendTemplateInfo(info, periodStart, periodEnd, shouldFit); + + const availabilityStart = + context.presentationTimeline.getSegmentAvailabilityStart(); + tsi.evict(availabilityStart); } - context.presentationTimeline.notifySegments(references); - if (shouldFit) { - segmentIndex.fit(periodStart, periodEnd); + if (info.timeline) { + const timeline = info.timeline; + context.presentationTimeline.notifyTimeRange( + timeline, + periodStart); } if (stream && context.dynamic) { @@ -147,8 +159,10 @@ shaka.dash.SegmentTemplate = class { // If segmentIndex is deleted, or segmentIndex's references are // released by closeSegmentIndex(), we should set the value of // segmentIndex again. - if (!segmentIndex || segmentIndex.isEmpty()) { - segmentIndex.merge(references); + if (segmentIndex instanceof shaka.dash.TimelineSegmentIndex && + segmentIndex.isEmpty()) { + segmentIndex.appendTemplateInfo(info, periodStart, + periodEnd, shouldFit); } return Promise.resolve(segmentIndex); }, @@ -597,6 +611,311 @@ shaka.dash.SegmentTemplate = class { } }; + +/** + * A SegmentIndex that returns segments references on demand from + * a segment timeline. + * + * @extends shaka.media.SegmentIndex + * @implements {shaka.util.IReleasable} + * @implements {Iterable.} + * + * @private + * + */ +shaka.dash.TimelineSegmentIndex = class extends shaka.media.SegmentIndex { + /** + * + * @param {!shaka.dash.SegmentTemplate.SegmentTemplateInfo} templateInfo + * @param {?string} representationId + * @param {number} bandwidth + * @param {Array.} baseUris + * @param {number} periodStart + * @param {number} periodEnd + * @param {shaka.media.InitSegmentReference} initSegmentReference + * @param {boolean} shouldFit + * + */ + constructor(templateInfo, representationId, bandwidth, baseUris, + periodStart, periodEnd, initSegmentReference, shouldFit) { + super([]); + + /** @private {?shaka.dash.SegmentTemplate.SegmentTemplateInfo} */ + this.templateInfo_ = templateInfo; + /** @private {?string} */ + this.representationId_ = representationId; + /** @private {number} */ + this.bandwidth_ = bandwidth; + /** @private {Array.} */ + this.baseUris_ = baseUris; + /** @private {number} */ + this.periodStart_ = periodStart; + /** @private {number} */ + this.periodEnd_ = periodEnd; + /** @private {shaka.media.InitSegmentReference} */ + this.initSegmentReference_ = initSegmentReference; + + + if (shouldFit) { + this.fitTimeline(); + } + } + + /** + * @override + */ + getNumReferences() { + if (this.templateInfo_) { + return this.templateInfo_.timeline.length; + } else { + return 0; + } + } + + /** + * @override + */ + release() { + super.release(); + this.templateInfo_ = null; + // We cannot release other fields, as segment index can + // be recreated using only template info. + } + + + /** + * @override + */ + evict(time) { + if (!this.templateInfo_) { + return; + } + shaka.log.debug(`${this.representationId_} Evicting at ${time}`); + let numToEvict = 0; + const timeline = this.templateInfo_.timeline; + + for (let i = 0; i < timeline.length; i += 1) { + const range = timeline[i]; + const end = range.end + this.periodStart_; + const start = range.start + this.periodStart_; + + if (end <= time) { + shaka.log.debug(`Evicting ${start} - ${end}`); + numToEvict += 1; + } else { + break; + } + } + + if (numToEvict > 0) { + this.templateInfo_.timeline = timeline.slice(numToEvict); + if (this.references.length >= numToEvict) { + this.references = this.references.slice(numToEvict); + } + + this.numEvicted_ += numToEvict; + + if (this.getNumReferences() === 0) { + this.release(); + } + } + } + + /** + * Merge new template info + * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info + * @param {number} periodStart + * @param {number} periodEnd + * @param {boolean} shouldFit + */ + appendTemplateInfo(info, periodStart, periodEnd, shouldFit) { + if (!this.templateInfo_) { + this.templateInfo_ = info; + this.periodStart_ = periodStart; + this.periodEnd_ = periodEnd; + } else { + const currentTimeline = this.templateInfo_.timeline; + + // Append timeline + const lastCurrentEntry = currentTimeline[currentTimeline.length - 1]; + const newEntries = info.timeline.filter((entry) => { + return entry.start >= lastCurrentEntry.end; + }); + + if (newEntries.length > 0) { + shaka.log.debug(`Appending ${newEntries.length} entries`); + this.templateInfo_.timeline.push(...newEntries); + } + + if (this.periodEnd_ !== periodEnd) { + this.periodEnd_ = periodEnd; + } + } + + if (shouldFit) { + this.fitTimeline(); + } + } + + /** + * + * @param {number} time + */ + isBeforeFirstEntry(time) { + const hasTimeline = this.templateInfo_ && + this.templateInfo_.timeline && this.templateInfo_.timeline.length; + + if (hasTimeline) { + const timeline = this.templateInfo_.timeline; + return time < timeline[0].start + this.periodStart_; + } else { + return false; + } + } + + /** + * Fit timeline entries to period boundaries + */ + fitTimeline() { + if (this.getIsImmutable()) { + return; + } + const timeline = this.templateInfo_.timeline; + while (timeline.length) { + const lastTimePeriod = timeline[timeline.length - 1]; + if (lastTimePeriod.start >= this.periodEnd_) { + timeline.pop(); + } else { + break; + } + } + + this.evict(this.periodStart_); + + if (timeline.length === 0) { + return; + } + + if (this.periodEnd_ !== Infinity) { + // Adjust the last timeline entry to match the period end + const lastTimePeriod = timeline[timeline.length - 1]; + // NOTE: end should be relative to period start + lastTimePeriod.end = this.periodEnd_ - this.periodStart_; + } + } + + /** + * @override + */ + find(time) { + shaka.log.debug(`Find ${time}`); + + if (this.isBeforeFirstEntry(time)) { + return this.numEvicted_; + } + + if (!this.templateInfo_) { + return null; + } + + const timeline = this.templateInfo_.timeline; + + // Early exit if the time isn't within this period + if (time < this.periodStart_ || time > this.periodEnd_) { + return null; + } + + const lastIndex = timeline.length - 1; + + for (let i = 0; i < timeline.length; i++) { + const range = timeline[i]; + const start = range.start + this.periodStart_; + // A rounding error can cause /time/ to equal e.endTime or fall in between + // the references by a fraction of a second. To account for this, we use + // the start of the next segment as /end/, unless this is the last + // reference, in which case we use the period end as the /end/ + let end = range.end + this.periodStart_; + + if (i < lastIndex) { + end = timeline[i + 1].start + this.periodStart_; + } + + if ((time >= start) && (time < end)) { + return i + this.numEvicted_; + } + } + + return null; + } + + /** + * @override + */ + get(position) { + const correctedPosition = position - this.numEvicted_; + if (correctedPosition < 0 || + correctedPosition >= this.getNumReferences() || !this.templateInfo_) { + return null; + } + + let ref = this.references[correctedPosition]; + + if (!ref) { + const range = this.templateInfo_.timeline[correctedPosition]; + const segmentReplacement = position + this.templateInfo_.startNumber; + const timeReplacement = this.templateInfo_ + .unscaledPresentationTimeOffset + range.unscaledStart; + + const createUrisCb = () => { + return shaka.dash.TimelineSegmentIndex + .createUris_( + this.templateInfo_.mediaTemplate, + this.representationId_, + segmentReplacement, + this.bandwidth_, + timeReplacement, + this.baseUris_, + ); + }; + + const timestampOffset = this.periodStart_ - + this.templateInfo_.scaledPresentationTimeOffset; + + ref = new shaka.media.SegmentReference( + this.periodStart_ + range.start, + this.periodStart_ + range.end, + createUrisCb, + /* startByte= */ 0, + /* endByte= */ null, + this.initSegmentReference_, + timestampOffset, + this.periodStart_, + this.periodEnd_); + this.references[correctedPosition] = ref; + } + + return ref; + } + + /** + * Fill in a specific template with values to get the segment uris + * + * @return {!Array.} + * @private + */ + static createUris_(mediaTemplate, repId, segmentReplacement, + bandwidth, timeReplacement, baseUris) { + const mediaUri = shaka.dash.MpdUtils.fillUriTemplate( + mediaTemplate, repId, + segmentReplacement, bandwidth || null, timeReplacement); + return shaka.util.ManifestParserUtils + .resolveUris(baseUris, [mediaUri]) + .map((g) => { + return g.toString(); + }); + } +}; + /** * @typedef {{ * timescale: number, @@ -608,7 +927,6 @@ shaka.dash.SegmentTemplate = class { * mediaTemplate: ?string, * indexTemplate: ?string * }} - * @private * * @description * Contains information about a SegmentTemplate. diff --git a/lib/media/presentation_timeline.js b/lib/media/presentation_timeline.js index 64021ff244..8d09fcf74e 100644 --- a/lib/media/presentation_timeline.js +++ b/lib/media/presentation_timeline.js @@ -7,6 +7,7 @@ goog.provide('shaka.media.PresentationTimeline'); goog.require('goog.asserts'); +goog.require('shaka.dash.MpdUtils'); goog.require('shaka.log'); goog.require('shaka.media.SegmentReference'); @@ -219,6 +220,46 @@ shaka.media.PresentationTimeline = class { return this.presentationDelay_; } + /** + * Gives PresentationTimeline a Stream's timeline so it can size and position + * the segment availability window, and account for missing segment + * information. + * + * @param {!Array.} timeline + * @param {number} startOffset + * @export + */ + notifyTimeRange(timeline, startOffset) { + if (timeline.length == 0) { + return; + } + + const firstStartTime = timeline[0].start + startOffset; + const lastEndTime = timeline[timeline.length - 1].end + startOffset; + + this.notifyMinSegmentStartTime(firstStartTime); + + this.maxSegmentDuration_ = timeline.reduce( + (max, r) => { return Math.max(max, r.end - r.start); }, + this.maxSegmentDuration_); + + this.maxSegmentEndTime_ = + Math.max(this.maxSegmentEndTime_, lastEndTime); + + if (this.presentationStartTime_ != null && this.autoCorrectDrift_ && + !this.startTimeLocked_) { + // 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. + const now = (Date.now() + this.clockOffset_) / 1000.0; + this.presentationStartTime_ = + now - this.maxSegmentEndTime_ - this.maxSegmentDuration_; + } + + shaka.log.v1('notifySegments:', + 'maxSegmentDuration=' + this.maxSegmentDuration_); + } + /** * Gives PresentationTimeline an array of segments so it can size and position @@ -573,3 +614,4 @@ shaka.media.PresentationTimeline = class { } } }; + diff --git a/lib/media/segment_index.js b/lib/media/segment_index.js index 544d9bd7b9..9959aac50d 100644 --- a/lib/media/segment_index.js +++ b/lib/media/segment_index.js @@ -44,12 +44,29 @@ shaka.media.SegmentIndex = class { * * @protected {number} */ - this.numEvicted = 0; + this.numEvicted_ = 0; /** @private {boolean} */ this.immutable_ = false; } + /** + * Get immutability + * + * @return {boolean} + */ + getIsImmutable() { + return this.immutable_; + } + + /** + * Get number of references + * @protected + * @return {number} + */ + getNumReferences() { + return this.references.length; + } /** * @override @@ -140,11 +157,11 @@ shaka.media.SegmentIndex = class { this.references[i + 1].startTime : r.endTime; // Note that a segment ends immediately before the end time. if ((time >= start) && (time < end)) { - return i + this.numEvicted; + return i + this.numEvicted_; } } if (this.references.length && time < this.references[0].startTime) { - return this.numEvicted; + return this.numEvicted_; } return null; @@ -164,7 +181,7 @@ shaka.media.SegmentIndex = class { return null; } - const index = position - this.numEvicted; + const index = position - this.numEvicted_; if (index < 0 || index >= this.references.length) { return null; } @@ -290,7 +307,7 @@ shaka.media.SegmentIndex = class { const diff = oldSize - newSize; // Tracking the number of evicted refs will keep their "positions" stable // for the caller. - this.numEvicted += diff; + this.numEvicted_ += diff; } @@ -332,7 +349,7 @@ shaka.media.SegmentIndex = class { if (firstReference.endTime <= windowStart) { this.references.shift(); if (!isNew) { - this.numEvicted++; + this.numEvicted_++; } } else { break; @@ -448,7 +465,7 @@ shaka.media.SegmentIndex = class { * @return {boolean} */ isEmpty() { - return this.references.length == 0; + return this.getNumReferences() == 0; } /** @@ -629,7 +646,7 @@ shaka.media.MetaSegmentIndex = class extends shaka.media.SegmentIndex { */ appendSegmentIndex(segmentIndex) { goog.asserts.assert( - this.indexes_.length == 0 || segmentIndex.numEvicted == 0, + this.indexes_.length == 0 || segmentIndex.numEvicted_ == 0, 'Should not append a new segment index with already-evicted segments'); this.indexes_.push(segmentIndex); } @@ -673,7 +690,8 @@ shaka.media.MetaSegmentIndex = class extends shaka.media.SegmentIndex { return position + numPassedInEarlierIndexes; } - numPassedInEarlierIndexes += index.numEvicted + index.references.length; + numPassedInEarlierIndexes += index.numEvicted_ + + index.references.length; } return null; @@ -689,7 +707,7 @@ shaka.media.MetaSegmentIndex = class extends shaka.media.SegmentIndex { for (const index of this.indexes_) { goog.asserts.assert( - !sawSegments || index.numEvicted == 0, + !sawSegments || index.numEvicted_ == 0, 'Should not see evicted segments after available segments'); const reference = index.get(position - numPassedInEarlierIndexes); @@ -697,7 +715,8 @@ shaka.media.MetaSegmentIndex = class extends shaka.media.SegmentIndex { return reference; } - numPassedInEarlierIndexes += index.numEvicted + index.references.length; + numPassedInEarlierIndexes += index.numEvicted_ + + index.references.length; sawSegments = sawSegments || index.references.length != 0; } diff --git a/test/dash/dash_parser_segment_template_unit.js b/test/dash/dash_parser_segment_template_unit.js index b582b5bd1d..843149d19d 100644 --- a/test/dash/dash_parser_segment_template_unit.js +++ b/test/dash/dash_parser_segment_template_unit.js @@ -578,4 +578,284 @@ describe('DashParser SegmentTemplate', () => { await Dash.testFails(source, error); }); }); + + describe('TimelineSegmentIndex', () => { + describe('find', () => { + it('finds the correct references', async () => { + const info = makeTemplateInfo(makeRanges(0, 2.0, 10)); + const infoClone = shaka.util.ObjectUtils.cloneObject(info); + const index = await makeTimelineSegmentIndex(infoClone, + /* delayPeriodEnd= */ true, + /* shouldFit= */ true); + + const pos1 = index.find(1.0); + expect(pos1).toBe(0); + const pos2 = index.find(2.0); + expect(pos2).toBe(1); + + // After the end of the last reference but before the end of the period + // should return index of the last reference + const lastRef = info.timeline[info.timeline.length - 1]; + const pos3 = index.find(lastRef.end + 0.5); + expect(pos3).toBe(info.timeline.length - 1); + + const pos4 = index.find(123.45); + expect(pos4).toBeNull(); + }); + + it('finds correct position if time is in gap', async () => { + const ranges = [ + { + start: 0, + end: 2, + unscaledStart: 0, + }, + { + start: 3, + end: 5, + unscaledStart: 3 * 90000, + }, + ]; + const info = makeTemplateInfo(ranges); + const index = await makeTimelineSegmentIndex(info); + const pos = index.find(2.5); + expect(pos).toBe(0); + }); + + it('finds correct position if time === first start time', async () => { + const info = makeTemplateInfo(makeRanges(0, 2.0, 10)); + const index = await makeTimelineSegmentIndex(info); + + const pos = index.find(0); + expect(pos).toBe(0); + }); + + it('finds correct position if time === first end time', async () => { + const ranges = [ + { + start: 0, + end: 2, + unscaledStart: 0, + }, + { + start: 2.1, + end: 5, + unscaledStart: 3 * 90000, + }, + ]; + const info = makeTemplateInfo(ranges); + const index = await makeTimelineSegmentIndex(info); + + const pos = index.find(2.0); + expect(pos).toBe(0); + }); + + it('finds correct position if time === second start time', async () => { + const ranges = [ + { + start: 0, + end: 2, + unscaledStart: 0, + }, + { + start: 2.1, + end: 5, + unscaledStart: 3 * 90000, + }, + ]; + const info = makeTemplateInfo(ranges); + const index = await makeTimelineSegmentIndex(info); + + const pos = index.find(2.1); + expect(pos).toBe(1); + }); + + + it('returns null if time === last end time', async () => { + const info = makeTemplateInfo(makeRanges(0, 2.0, 2)); + const index = await makeTimelineSegmentIndex(info, false); + + const pos = index.find(4.0); + expect(pos).toBeNull(); + }); + + it('returns null if time > last end time', async () => { + const info = makeTemplateInfo(makeRanges(0, 2.0, 2)); + const index = await makeTimelineSegmentIndex(info, false); + + const pos = index.find(6.0); + expect(pos).toBeNull(); + }); + }); + describe('get', () => { + it('creates a segment reference for a given position', async () => { + const info = makeTemplateInfo(makeRanges(0, 2.0, 10)); + const index = await makeTimelineSegmentIndex(info); + const pos = index.find(2.0); + goog.asserts.assert(pos != null, 'Null position!'); + const ref = index.get(pos); + expect(ref).toEqual(jasmine.objectContaining({ + 'startTime': 2, + 'endTime': 4, + 'trueEndTime': 4, + 'startByte': 0, + 'endByte': null, + 'timestampOffset': 0, + 'appendWindowStart': 0, + 'appendWindowEnd': 21, + 'partialReferences': [], + 'tilesLayout': '', + 'tileDuration': null, + })); + }); + + it('returns null if a position is unknown', async () => { + const info = makeTemplateInfo(makeRanges(0, 2.0, 10)); + const index = await makeTimelineSegmentIndex(info); + const ref = index.get(12345); + expect(ref).toBeNull(); + }); + + it('returns null if a position < 0', async () => { + const info = makeTemplateInfo(makeRanges(0, 2.0, 10)); + const index = await makeTimelineSegmentIndex(info); + const ref = index.get(-12); + expect(ref).toBeNull(); + }); + }); + + describe('appendTemplateInfo', () => { + it('appends new timeline to existing', async () => { + const initialRanges = makeRanges(0, 2.0, 10); + const info = makeTemplateInfo(initialRanges); + const index = await makeTimelineSegmentIndex(info, false); + + const newStart = initialRanges[initialRanges.length - 1].end; + expect(index.find(newStart)).toBeNull(); + + const newRanges = makeRanges(newStart, 2.0, 10); + const newTemplateInfo = makeTemplateInfo(newRanges); + + const newEnd = newRanges[newRanges.length - 1].end; + index.appendTemplateInfo(newTemplateInfo, newEnd); + expect(index.find(newStart)).toBe(10); + expect(index.find(newEnd - 1.0)).toBe(19); + }); + }); + + describe('evict', () => { + it('evicts old entries and maintains position', async () => { + const initialRanges = makeRanges(0, 2.0, 10); + const info = makeTemplateInfo(initialRanges); + const index = await makeTimelineSegmentIndex(info, false); + + index.evict(4.0); + expect(index.find(2.0)).toBe(2); + expect(index.find(6.0)).toBe(3); + }); + }); + }); + + /** + * + * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info + * @param {boolean} delayPeriodEnd + * @param {boolean} shouldFit + * @return {?} + */ + async function makeTimelineSegmentIndex(info, delayPeriodEnd = true, + shouldFit = false) { + // Period end may be a bit after the last timeline entry + let periodEnd = info.timeline[info.timeline.length - 1].end; + if (delayPeriodEnd) { + periodEnd += 1.0; + } + + const dummySource = Dash.makeSimpleManifestText([ + '', + ' ', + ' ', + ' ', + '', + ], /* duration= */ 45); + + fakeNetEngine.setResponseText('dummy://foo', dummySource); + const manifest = await parser.start('dummy://foo', playerInterface); + + expect(manifest.variants.length).toBe(1); + + const stream = manifest.variants[0].video; + expect(stream).toBeTruthy(); + await stream.createSegmentIndex(); + + /** @type {?} */ + const index = stream.segmentIndex; + index.release(); + index.appendTemplateInfo(info, info.timeline[0].start, + periodEnd, shouldFit); + + return index; + } }); + +/** + * Creates a URI string. + * + * @param {number} x + * @return {string} + */ +function uri(x) { + return 'http://example.com/video_' + x + '.m4s'; +} + +/** + * + * @return {shaka.media.InitSegmentReference} + */ +function makeInitSegmentReference() { + return new shaka.media.InitSegmentReference(() => [], 0, null); +} + +/** + * Create a list of continuous time ranges + * @param {number} start + * @param {number} duration + * @param {number} num + * @return {Array} + */ +function makeRanges(start, duration, num) { + const ranges = []; + let currentPos = start; + for (let i = 0; i < num; i += 1) { + ranges.push({ + start: currentPos, + end: currentPos + duration, + unscaledStart: currentPos * 90000, + }); + currentPos += duration; + } + return ranges; +} + +/** + * Creates a real SegmentReference. This is distinct from the fake ones used + * in ManifestParser tests because it can be on the left-hand side of an + * expect(). You can't expect jasmine.any(Number) to equal + * jasmine.any(Number). :-( + * + * @param {Array} timeline + * @return {shaka.dash.SegmentTemplate.SegmentTemplateInfo} + */ +function makeTemplateInfo(timeline) { + return { + 'segmentDuration': null, + 'timescale': 90000, + 'startNumber': 1, + 'scaledPresentationTimeOffset': 0, + 'unscaledPresentationTimeOffset': 0, + 'timeline': timeline, + 'mediaTemplate': 'master_540_2997_$Number%09d$.cmfv', + 'indexTemplate': null, + }; +} diff --git a/test/media/presentation_timeline_unit.js b/test/media/presentation_timeline_unit.js index 03d2bf94fa..0d91c2eee1 100644 --- a/test/media/presentation_timeline_unit.js +++ b/test/media/presentation_timeline_unit.js @@ -114,6 +114,57 @@ describe('PresentationTimeline', () => { return timeline; } + const makeTimeRange = (startTime, endTime) => { + return { + start: startTime, + end: endTime, + }; + }; + + describe('notifyTimeRange', () => { + it('calculates time based on segment times when available', () => { + const timeline = makeLiveTimeline(/* availability= */ 20); + + const timeRanges = [ + makeTimeRange(0, 10), + makeTimeRange(10, 20), + makeTimeRange(20, 30), + makeTimeRange(30, 40), + makeTimeRange(40, 50), + ]; + + + // In spite of the current time, the explicit segment times will decide + // the availability window. + // See https://github.com/shaka-project/shaka-player/issues/999 + setElapsed(1000); + timeline.notifyTimeRange(timeRanges, 0); + + // last segment time (50) - availability (20) + expect(timeline.getSegmentAvailabilityStart()).toBe(30); + }); + + it('ignores segment times when configured to', () => { + const timeline = makeLiveTimeline( + /* availability= */ 20, /* drift= */ 0, + /* autoCorrectDrift= */ false); + + const timeRanges = [ + makeTimeRange(0, 10), + makeTimeRange(10, 20), + makeTimeRange(20, 30), + makeTimeRange(30, 40), + makeTimeRange(40, 50), + ]; + + setElapsed(100); + timeline.notifyTimeRange(timeRanges, 0); + + // now (100) - max segment duration (10) - availability start time (0) + expect(timeline.getSegmentAvailabilityEnd()).toBe(90); + }); + }); + describe('getSegmentAvailabilityStart', () => { it('returns 0 for VOD and IPR', () => { const timeline1 = makeVodTimeline(/* duration= */ 60);