diff --git a/externs/shaka/player.js b/externs/shaka/player.js index b5cfde0054..b0e9516fc7 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -537,7 +537,9 @@ shaka.extern.DashManifestConfiguration; * Retry parameters for manifest requests. * @property (number) availabilityWindowOverride * A number, in seconds, that overrides the availability window in the - * manifest, or NaN if the default value should be used. + * manifest, or NaN if the default value should be used. This is enforced by + * the manifest parser, so custom manifest parsers should take care to honor + * this parameter. * @property {shaka.extern.DashManifestConfiguration} dash * Advanced parameters used by the DASH manifest parser. * diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 3a359da58f..676c74a6ec 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -550,10 +550,23 @@ shaka.dash.DashParser.prototype.processManifest_ = // Ignore duration calculated from Period lengths if this is dynamic. presentationTimeline.setDuration(duration || Infinity); } + + let isLive = presentationTimeline.isLive(); + + // If it's live, we check for an override. + if (isLive && !isNaN(this.config_.availabilityWindowOverride)) { + segmentAvailabilityDuration = this.config_.availabilityWindowOverride; + } + + // If it's null, that means segments are always available. This is always the + // case for VOD, and sometimes the case for live. + if (segmentAvailabilityDuration == null) { + segmentAvailabilityDuration = Infinity; + } + presentationTimeline.setSegmentAvailabilityDuration( - segmentAvailabilityDuration != null ? - segmentAvailabilityDuration : - Infinity); + segmentAvailabilityDuration); + // Use @maxSegmentDuration to override smaller, derived values. presentationTimeline.notifyMaxSegmentDuration(maxSegmentDuration || 1); if (goog.DEBUG) presentationTimeline.assertIsValid(); @@ -567,8 +580,6 @@ shaka.dash.DashParser.prototype.processManifest_ = // the clock offset. let timingElements = XmlUtils.findChildren(mpd, 'UTCTiming'); - let isLive = presentationTimeline.isLive(); - return this.parseUtcTiming_( baseUris, timingElements, isLive).then(function(offset) { // Detect calls to stop(). diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index f9cbfcee36..d69c58dced 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -351,9 +351,14 @@ shaka.hls.HlsParser.prototype.parseManifest_ = function(data, uri) { // will be able to buffer ahead three segments, but the seek window will // be zero-sized. const PresentationType = shaka.hls.HlsParser.PresentationType_; + if (this.presentationType_ == PresentationType.LIVE) { + let segmentAvailabilityDuration = threeSegmentDurations; + if (!isNaN(this.config_.availabilityWindowOverride)) { + segmentAvailabilityDuration = this.config_.availabilityWindowOverride; + } this.presentationTimeline_.setSegmentAvailabilityDuration( - threeSegmentDurations); + segmentAvailabilityDuration); } let rolloverSeconds = diff --git a/lib/player.js b/lib/player.js index 2032df4e8e..c86c0b4f1a 100644 --- a/lib/player.js +++ b/lib/player.js @@ -733,11 +733,6 @@ shaka.Player.prototype.load = async function( if (cancelValue) throw cancelValue; - if (!isNaN(this.config_.manifest.availabilityWindowOverride)) { - this.manifest_.presentationTimeline.setSegmentAvailabilityDuration( - this.config_.manifest.availabilityWindowOverride); - } - this.filterManifestForAVVariants_(); this.drmEngine_ = this.createDrmEngine(); diff --git a/test/dash/dash_parser_live_unit.js b/test/dash/dash_parser_live_unit.js index cf0271e41e..b59b7c0872 100644 --- a/test/dash/dash_parser_live_unit.js +++ b/test/dash/dash_parser_live_unit.js @@ -678,6 +678,55 @@ describe('DashParser Live', function() { PromiseMock.flush(); }); + describe('availabilityWindowOverride', function() { + it('overrides @timeShiftBufferDepth', function(done) { + let manifest = [ + '', + ' ', + ' ', + ' ', + ' http://example.com', + ' ', + ' ', + ' ', + ' ', + '', + ].join('\n'); + fakeNetEngine.setResponseMapAsText({'dummy://foo': manifest}); + + parser.configure({ + retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), + availabilityWindowOverride: 4 * 60, + dash: { + clockSyncUri: '', + customScheme: function(node) { return null; }, + ignoreDrmInfo: false, + xlinkFailGracefully: false, + defaultPresentationDelay: 10, + }, + }); + + Date.now = function() { return 600000; /* 10 minutes */ }; + parser.start('dummy://foo', playerInterface) + .then(function(manifest) { + expect(manifest).toBeTruthy(); + let timeline = manifest.presentationTimeline; + expect(timeline).toBeTruthy(); + + // The parser was configured to have a manifest availability window + // of 4 minutes. + let end = timeline.getSegmentAvailabilityEnd(); + let start = timeline.getSegmentAvailabilityStart(); + expect(end - start).toEqual(4 * 60); + }).catch(fail).then(done); + PromiseMock.flush(); + }); + }); + describe('maxSegmentDuration', function() { it('uses @maxSegmentDuration', function(done) { let manifest = [ diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index c88251b5c3..8d2e4276c6 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -431,6 +431,17 @@ describe('HlsParser live', function() { 'test:/main2.mp4\n', ].join(''); + let mediaWithManySegments = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:5\n', + '#EXT-X-MAP:URI="test:/init.mp4",BYTERANGE="616@0"\n', + '#EXT-X-MEDIA-SEQUENCE:0\n', + ].join(''); + for (let i = 0; i < 1000; ++i) { + mediaWithManySegments += '#EXTINF:2,\n'; + mediaWithManySegments += 'test:/main.mp4\n'; + } + it('starts presentation as VOD when ENDLIST is present', function(done) { fakeNetEngine.setResponseMap({ 'test:/master': toUTF8(master), @@ -455,6 +466,48 @@ describe('HlsParser live', function() { parser.start('test:/master', playerInterface).catch(fail).then(done); }); + describe('availabilityWindowOverride', function() { + async function testWindowOverride(expectedWindow) { + fakeNetEngine.setResponseMap({ + 'test:/master': toUTF8(master), + 'test:/video': toUTF8(mediaWithManySegments), + 'test:/init.mp4': initSegmentData, + 'test:/main.mp4': segmentData, + }); + + let manifest = await parser.start('test:/master', playerInterface); + expect(manifest).toBeTruthy(); + let timeline = manifest.presentationTimeline; + expect(timeline).toBeTruthy(); + + const start = timeline.getSegmentAvailabilityStart(); + const end = timeline.getSegmentAvailabilityEnd(); + expect(end - start).toEqual(expectedWindow); + } + + it('does not affect seek range if unset', async () => { + // 15 seconds is three segment durations. + await testWindowOverride(15); + }); + + it('overrides default seek range if set', async () => { + parser.configure({ + retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), + availabilityWindowOverride: 240, + dash: { + customScheme: function(node) { return null; }, + clockSyncUri: '', + ignoreDrmInfo: false, + xlinkFailGracefully: false, + defaultPresentationDelay: 10, + }, + }); + + // 240 is the availabilityWindowOverride setting. + await testWindowOverride(240); + }); + }); + it('offsets VTT text with rolled over TS timestamps', function(done) { const masterWithVtt = [ '#EXTM3U\n', diff --git a/test/player_unit.js b/test/player_unit.js index 37da58feba..95acebbf97 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -3135,45 +3135,6 @@ describe('Player', function() { }); }); - describe('live', function() { - function availabilityWindowTest(expectedAvailabilityWindow) { - // Create a live timeline and manifest. - let timeline = new shaka.media.PresentationTimeline(300, 0); - timeline.setStatic(false); - timeline.setSegmentAvailabilityDuration(100); - - manifest = new shaka.test.ManifestGenerator() - .setTimeline(timeline) - .addPeriod(0) - .addVariant(0) - .addVideo(1) - .build(); - - let parser = new shaka.test.FakeManifestParser(manifest); - let parserFactory = function() { return parser; }; - - return player.load('', /* startTime */ 0, parserFactory).then(() => { - // Availability window can be determined by finding the time difference - // between the avalability end and start. Thus, we can compare the - // expected and actual window. - let end = timeline.getSegmentAvailabilityEnd(); - expect(end - timeline.getSegmentAvailabilityStart()) - .toBeCloseTo(expectedAvailabilityWindow, 1); - }); - } - - it('uses normal availability window when not overridden', async () => { - // The availability start should be what was in the timeline (100). - await availabilityWindowTest(100); - }); - - it('honors availabilityWindowOverride', async () => { - player.configure({manifest: {availabilityWindowOverride: 200}}); - // The availability start should be what was configured (200). - await availabilityWindowTest(200); - }); - }); - describe('language methods', function() { let videoOnlyManifest; let parserFactory = function() {