From 3cad924cf1ace87f3c157b22a4237aaa5b405ba4 Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Fri, 26 Aug 2016 20:56:43 -0700 Subject: [PATCH] In-progress recording (IPR) support, phase 2 Behavior for IPR streams: * offline storage disallowed * segment references will not be stretched to the period * seek range starts at 0 * seek range end is calculated like the live edge * seek bar is from 0 to duration, not the seek range Closes #477 Change-Id: Ia36874bb7208c2473c79cb817395ce03925b8c95 --- demo/controls.js | 11 +++-- lib/dash/dash_parser.js | 4 ++ lib/dash/mpd_utils.js | 9 +++- lib/dash/segment_base.js | 3 +- lib/dash/segment_list.js | 2 +- lib/dash/segment_template.js | 2 +- lib/media/presentation_timeline.js | 7 +-- lib/offline/storage.js | 3 +- lib/util/error.js | 3 +- test/media/presentation_timeline_unit.js | 60 +++++++++++++++++------- 10 files changed, 74 insertions(+), 30 deletions(-) diff --git a/demo/controls.js b/demo/controls.js index 32c8c0a851..075b8a227b 100644 --- a/demo/controls.js +++ b/demo/controls.js @@ -547,10 +547,11 @@ ShakaControls.prototype.updateTimeAndSeekRange_ = function() { var bufferedEnd = bufferedLength ? this.video_.buffered.end(0) : 0; var seekRange = this.player_.seekRange(); - this.seekBar_.min = seekRange.start; - this.seekBar_.max = seekRange.end; - if (this.player_.isLive()) { + // For live, the seek bar size is the seek range. + this.seekBar_.min = seekRange.start; + this.seekBar_.max = seekRange.end; + // The amount of time we are behind the live edge. var behindLive = Math.floor(seekRange.end - displayTime); displayTime = Math.max(0, behindLive); @@ -572,6 +573,10 @@ ShakaControls.prototype.updateTimeAndSeekRange_ = function() { this.seekBar_.value = seekRange.end - displayTime; } } else { + // For VOD and IPR, the seek bar size is from 0 to duration. + this.seekBar_.min = 0; + this.seekBar_.max = duration; + var showHour = duration >= 3600; this.currentTime_.textContent = this.buildTimeString_(displayTime, showHour); diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index e58a409f2e..c732559f83 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -170,6 +170,7 @@ shaka.dash.DashParser.InheritanceFrame; /** * @typedef {{ + * dynamic: boolean, * presentationTimeline: !shaka.media.PresentationTimeline, * period: ?shaka.dash.DashParser.InheritanceFrame, * periodInfo: ?shaka.dash.DashParser.PeriodInfo, @@ -181,6 +182,8 @@ shaka.dash.DashParser.InheritanceFrame; * @description * Contains context data for the streams. * + * @property {boolean} dynamic + * True if the MPD is dynamic (not all segments available at once) * @property {!shaka.media.PresentationTimeline} presentationTimeline * The PresentationTimeline. * @property {?shaka.dash.DashParser.InheritanceFrame} period @@ -487,6 +490,7 @@ shaka.dash.DashParser.prototype.parseManifest_ = /** @type {shaka.dash.DashParser.Context} */ var context = { + dynamic: mpdType != 'static', presentationTimeline: presentationTimeline, period: null, periodInfo: null, diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index 9c63616435..6bd82382d4 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -288,11 +288,12 @@ shaka.dash.MpdUtils.createTimeline = function( * contracts the last SegmentReference so it ends at the end of its Period for * VOD presentations. * + * @param {boolean} dynamic * @param {?number} periodDuration * @param {!Array.} references */ shaka.dash.MpdUtils.fitSegmentReferences = function( - periodDuration, references) { + dynamic, periodDuration, references) { if (references.length == 0) return; @@ -311,8 +312,12 @@ shaka.dash.MpdUtils.fitSegmentReferences = function( firstReference.startByte, firstReference.endByte); } - if (periodDuration == null || periodDuration == Infinity) + if (dynamic) return; + goog.asserts.assert(periodDuration != null, + 'Period duration must be known for static content!'); + goog.asserts.assert(periodDuration != Infinity, + 'Period duration must be finite for static content!'); var lastReference = references[references.length - 1]; diff --git a/lib/dash/segment_base.js b/lib/dash/segment_base.js index 7fbeafae4e..e2fedf6e91 100644 --- a/lib/dash/segment_base.js +++ b/lib/dash/segment_base.js @@ -151,7 +151,8 @@ shaka.dash.SegmentBase.createSegmentIndexFromUris = function( presentationTimeOffset); } - shaka.dash.MpdUtils.fitSegmentReferences(periodDuration, references); + shaka.dash.MpdUtils.fitSegmentReferences( + context.dynamic, periodDuration, references); presentationTimeline.notifySegments(periodStartTime, references); // Since containers are never updated, we don't need to store the diff --git a/lib/dash/segment_list.js b/lib/dash/segment_list.js index 0d2b17a694..1d031e079a 100644 --- a/lib/dash/segment_list.js +++ b/lib/dash/segment_list.js @@ -65,7 +65,7 @@ shaka.dash.SegmentList.createStream = function(context, segmentIndexMap) { context.periodInfo.duration, info.startNumber, context.representation.baseUris, info); shaka.dash.MpdUtils.fitSegmentReferences( - context.periodInfo.duration, references); + context.dynamic, context.periodInfo.duration, references); if (segmentIndex) { segmentIndex.merge(references); var start = context.presentationTimeline.getSegmentAvailabilityStart(); diff --git a/lib/dash/segment_template.js b/lib/dash/segment_template.js index 750f0c3dad..ec100849d2 100644 --- a/lib/dash/segment_template.js +++ b/lib/dash/segment_template.js @@ -77,7 +77,7 @@ shaka.dash.SegmentTemplate.createStream = function( var references = SegmentTemplate.createFromTimeline_(context, info); shaka.dash.MpdUtils.fitSegmentReferences( - context.periodInfo.duration, references); + context.dynamic, context.periodInfo.duration, references); if (segmentIndex) { segmentIndex.merge(references); var start = context.presentationTimeline.getSegmentAvailabilityStart(); diff --git a/lib/media/presentation_timeline.js b/lib/media/presentation_timeline.js index 2c6923c6ee..2d196d83e5 100644 --- a/lib/media/presentation_timeline.js +++ b/lib/media/presentation_timeline.js @@ -266,7 +266,7 @@ shaka.media.PresentationTimeline.prototype.getSegmentAvailabilityStart = */ shaka.media.PresentationTimeline.prototype.getSegmentAvailabilityEnd = function() { - if (!this.isLive()) + if (!this.isLive() && !this.isInProgress()) return this.duration_; return Math.min(this.getLiveEdge_(), this.duration_); @@ -311,8 +311,9 @@ if (!COMPILED) { 'Detected as live stream, but does not match our model of live!'); } else if (this.isInProgress()) { // Implied by isInProgress(): finite and dynamic. - // IPR segments should not expire. - goog.asserts.assert(this.segmentAvailabilityDuration_ == Infinity, + // IPR streams should have a start time, and segments should not expire. + goog.asserts.assert(this.presentationStartTime_ != null && + this.segmentAvailabilityDuration_ == Infinity, 'Detected as IPR stream, but does not match our model of IPR!'); } else { // VOD // VOD segments should not expire and the presentation should be finite diff --git a/lib/offline/storage.js b/lib/offline/storage.js index c7e54e5c47..dd65fb35b2 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -197,7 +197,8 @@ shaka.offline.Storage.prototype.store = function( this.manifest_ = data.manifest; this.drmEngine_ = data.drmEngine; - if (this.manifest_.presentationTimeline.isLive()) { + if (this.manifest_.presentationTimeline.isLive() || + this.manifest_.presentationTimeline.isInProgress()) { throw new shaka.util.Error( shaka.util.Error.Category.STORAGE, shaka.util.Error.Code.CANNOT_STORE_LIVE_OFFLINE, manifestUri); diff --git a/lib/util/error.js b/lib/util/error.js index 12326a85a0..83d19c8708 100644 --- a/lib/util/error.js +++ b/lib/util/error.js @@ -580,7 +580,8 @@ shaka.util.Error.Code = { 'MALFORMED_OFFLINE_URI': 9004, /** - * The specified manifest is live. Live manifests cannot be stored offline. + * The specified content is live or in-progress. + * Live and in-progress streams cannot be stored offline. *
error.data[0] is the URI. */ 'CANNOT_STORE_LIVE_OFFLINE': 9005, diff --git a/test/media/presentation_timeline_unit.js b/test/media/presentation_timeline_unit.js index cad5219a52..6d322ab37a 100644 --- a/test/media/presentation_timeline_unit.js +++ b/test/media/presentation_timeline_unit.js @@ -88,8 +88,9 @@ describe('PresentationTimeline', function() { * @return {shaka.media.PresentationTimeline} */ function makeIprTimeline(duration) { + var now = Date.now() / 1000; var timeline = makePresentationTimeline( - /* static */ false, duration, /* start time */ null, + /* static */ false, duration, /* start time */ now, /* availability */ Infinity, /* max seg dur */ 10, /* clock offset */ 0); expect(timeline.isLive()).toBe(false); @@ -170,17 +171,36 @@ describe('PresentationTimeline', function() { }); describe('getSegmentAvailabilityEnd', function() { - it('returns duration for VOD and IPR', function() { - var timeline1 = makeVodTimeline(/* duration */ 60); - var timeline2 = makeIprTimeline(/* duration */ 60); + it('returns duration for VOD', function() { + var timeline = makeVodTimeline(/* duration */ 60); setElapsed(0); - expect(timeline1.getSegmentAvailabilityEnd()).toBe(60); - expect(timeline2.getSegmentAvailabilityEnd()).toBe(60); + expect(timeline.getSegmentAvailabilityEnd()).toBe(60); setElapsed(100); - expect(timeline1.getSegmentAvailabilityEnd()).toBe(60); - expect(timeline2.getSegmentAvailabilityEnd()).toBe(60); + expect(timeline.getSegmentAvailabilityEnd()).toBe(60); + }); + + it('calculates time for IPR', function() { + var timeline = makeIprTimeline(/* duration */ 60); + + setElapsed(0); + expect(timeline.getSegmentAvailabilityEnd()).toBe(0); + + setElapsed(10); + expect(timeline.getSegmentAvailabilityEnd()).toBe(0); + + setElapsed(11); + expect(timeline.getSegmentAvailabilityEnd()).toBe(1); + + setElapsed(69); + expect(timeline.getSegmentAvailabilityEnd()).toBe(59); + + setElapsed(70); + expect(timeline.getSegmentAvailabilityEnd()).toBe(60); + + setElapsed(100); + expect(timeline.getSegmentAvailabilityEnd()).toBe(60); }); it('calculates time for live', function() { @@ -228,17 +248,23 @@ describe('PresentationTimeline', function() { }); describe('setDuration', function() { - it('affects availability end for VOD and IPR', function() { + it('affects availability end for VOD', function() { setElapsed(0); - var timeline1 = makeVodTimeline(/* duration */ 60); - var timeline2 = makeIprTimeline(/* duration */ 60); - expect(timeline1.getSegmentAvailabilityEnd()).toBe(60); - expect(timeline2.getSegmentAvailabilityEnd()).toBe(60); + var timeline = makeVodTimeline(/* duration */ 60); + expect(timeline.getSegmentAvailabilityEnd()).toBe(60); + + timeline.setDuration(90); + expect(timeline.getSegmentAvailabilityEnd()).toBe(90); + }); + + it('affects availability end for IPR', function() { + var timeline = makeIprTimeline(/* duration */ 60); + + setElapsed(85); + expect(timeline.getSegmentAvailabilityEnd()).toBe(60); - timeline1.setDuration(90); - timeline2.setDuration(90); - expect(timeline1.getSegmentAvailabilityEnd()).toBe(90); - expect(timeline2.getSegmentAvailabilityEnd()).toBe(90); + timeline.setDuration(90); + expect(timeline.getSegmentAvailabilityEnd()).toBe(75); }); });