UNPKG

shaka-player

Version:
1,268 lines (1,147 loc) 51.4 kB
/** * @license * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ describe('DashParser Live', function() { const Util = shaka.test.Util; const ManifestParser = shaka.test.ManifestParser; const realTimeout = window.setTimeout; const oldNow = Date.now; const updateTime = 5; const originalUri = 'http://example.com/'; /** @type {!shaka.test.FakeNetworkingEngine} */ let fakeNetEngine; /** @type {!shaka.dash.DashParser} */ let parser; /** @type {shaka.extern.ManifestParser.PlayerInterface} */ let playerInterface; beforeEach(function() { // First, fake the clock so we can control timers. // This does not mock Date.now, which must be done separately. jasmine.clock().install(); // This Promise mock is required for fakeEventLoop. PromiseMock.install(); fakeNetEngine = new shaka.test.FakeNetworkingEngine(); parser = new shaka.dash.DashParser(); parser.configure(shaka.util.PlayerConfiguration.createDefault().manifest); playerInterface = { networkingEngine: fakeNetEngine, filterNewPeriod: function() {}, filterAllPeriods: function() {}, onTimelineRegionAdded: fail, // Should not have any EventStream elements. onEvent: fail, onError: fail, }; }); afterEach(function() { // Dash parser stop is synchronous. parser.stop(); // Uninstall the clock() first. This also undoes mockDate(), and should be // done afterEach, not afterAll. Otherwise, we get conflicts when some // tests use mockDate() and others directly overwrite Date.now. jasmine.clock().uninstall(); // Replace Date.now with the browser built-in. This must come AFTER we // uninstall the clock() module, or else mockDate() doesn't get cleaned up // correctly. Date.now = oldNow; // Finally, uninstall the Promise mock. PromiseMock.uninstall(); // TODO: Clean up this suite so that everyone uses mockDate(). }); /** * Simulate time to trigger a manifest update. */ function delayForUpdatePeriod() { // Tick the virtual clock to trigger an update and resolve all Promises. Util.fakeEventLoop(updateTime); } /** * Makes a simple live manifest with the given representation contents. * * @param {!Array.<string>} lines * @param {number?} updateTime * @param {number=} duration * @return {string} */ function makeSimpleLiveManifestText(lines, updateTime, duration) { let updateAttr = updateTime != null ? 'minimumUpdatePeriod="PT' + updateTime + 'S"' : ''; let durationAttr = duration != undefined ? 'duration="PT' + duration + 'S"' : ''; let template = [ '<MPD type="dynamic" %(updateAttr)s', ' availabilityStartTime="1970-01-01T00:00:00Z">', ' <Period id="1" %(durationAttr)s>', ' <AdaptationSet mimeType="video/mp4">', ' <Representation id="3" bandwidth="500">', ' <BaseURL>http://example.com</BaseURL>', '%(contents)s', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); let text = sprintf(template, { updateAttr: updateAttr, durationAttr: durationAttr, contents: lines.join('\n'), updateTime: updateTime, }); return text; } /** * Creates tests that test the behavior common between SegmentList and * SegmentTemplate. * * @param {!Array.<string>} basicLines * @param {!Array.<!shaka.media.SegmentReference>} basicRefs * @param {!Array.<string>} updateLines * @param {!Array.<!shaka.media.SegmentReference>} updateRefs * @param {!Array.<string>} partialUpdateLines */ function testCommonBehaviors( basicLines, basicRefs, updateLines, updateRefs, partialUpdateLines) { /** * Tests that an update will show the given references. * * @param {function()} done * @param {!Array.<string>} firstLines The Representation contents for the * first manifest. * @param {!Array.<!shaka.media.SegmentReference>} firstReferences The media * references for the first parse. * @param {!Array.<string>} secondLines The Representation contents for the * updated manifest. * @param {!Array.<!shaka.media.SegmentReference>} secondReferences The * media references for the updated manifest. */ function testBasicUpdate( done, firstLines, firstReferences, secondLines, secondReferences) { let firstManifest = makeSimpleLiveManifestText(firstLines, updateTime); let secondManifest = makeSimpleLiveManifestText(secondLines, updateTime); fakeNetEngine.setResponseText('dummy://foo', firstManifest); parser.start('dummy://foo', playerInterface) .then(function(manifest) { let stream = manifest.periods[0].variants[0].video; ManifestParser.verifySegmentIndex(stream, firstReferences); expect(manifest.periods.length).toBe(1); fakeNetEngine.setResponseText('dummy://foo', secondManifest); delayForUpdatePeriod(); ManifestParser.verifySegmentIndex(stream, secondReferences); // In https://github.com/google/shaka-player/issues/963, we // duplicated periods during the first update. This check covers // this case. expect(manifest.periods.length).toBe(1); }).catch(fail).then(done); PromiseMock.flush(); } it('basic support', function(done) { testBasicUpdate(done, basicLines, basicRefs, updateLines, updateRefs); }); it('new manifests don\'t need to include old references', function(done) { testBasicUpdate( done, basicLines, basicRefs, partialUpdateLines, updateRefs); }); it('evicts old references for single-period live stream', function(done) { let template = [ '<MPD type="dynamic" minimumUpdatePeriod="PT%(updateTime)dS"', ' timeShiftBufferDepth="PT30S"', ' suggestedPresentationDelay="PT5S"', ' availabilityStartTime="1970-01-01T00:00:00Z">', ' <Period id="1">', ' <AdaptationSet mimeType="video/mp4">', ' <Representation id="3" bandwidth="500">', ' <BaseURL>http://example.com</BaseURL>', '%(contents)s', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); let text = sprintf( template, {updateTime: updateTime, contents: basicLines.join('\n')}); fakeNetEngine.setResponseText('dummy://foo', text); Date.now = function() { return 0; }; parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(manifest).toBeTruthy(); let stream = manifest.periods[0].variants[0].video; expect(stream).toBeTruthy(); expect(stream.findSegmentPosition).toBeTruthy(); expect(stream.findSegmentPosition(0)).toBe(1); ManifestParser.verifySegmentIndex(stream, basicRefs); // 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); ManifestParser.verifySegmentIndex(stream, basicRefs.slice(1)); }).catch(fail).then(done); PromiseMock.flush(); }); it('evicts old references for multi-period live stream', function(done) { let template = [ '<MPD type="dynamic" minimumUpdatePeriod="PT%(updateTime)dS"', ' timeShiftBufferDepth="PT60S"', ' suggestedPresentationDelay="PT5S"', ' availabilityStartTime="1970-01-01T00:00:00Z">', ' <Period id="1">', ' <AdaptationSet mimeType="video/mp4">', ' <Representation id="3" bandwidth="500">', ' <BaseURL>http://example.com</BaseURL>', '%(contents)s', ' </Representation>', ' </AdaptationSet>', ' </Period>', ' <Period id="2" start="PT%(pStart)dS">', ' <AdaptationSet mimeType="video/mp4">', ' <Representation id="4" bandwidth="500">', ' <BaseURL>http://example.com</BaseURL>', '%(contents)s', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); // Set the period start to the sum of the durations of the references // in the previous period. let durs = basicRefs.map(function(r) { return r.endTime - r.startTime; }); let pStart = durs.reduce(function(p, d) { return p + d; }, 0); let args = { updateTime: updateTime, pStart: pStart, contents: basicLines.join('\n'), }; let text = sprintf(template, args); fakeNetEngine.setResponseText('dummy://foo', text); Date.now = function() { return 0; }; parser.start('dummy://foo', playerInterface) .then(function(manifest) { let stream1 = manifest.periods[0].variants[0].video; let stream2 = manifest.periods[1].variants[0].video; ManifestParser.verifySegmentIndex(stream1, basicRefs); ManifestParser.verifySegmentIndex(stream2, basicRefs); // The 60 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 (11 + pStart) * 1000; }; delayForUpdatePeriod(); ManifestParser.verifySegmentIndex(stream1, []); ManifestParser.verifySegmentIndex(stream2, basicRefs.slice(1)); }).catch(fail).then(done); PromiseMock.flush(); }); it('sets infinite duration for single-period live streams', function(done) { let template = [ '<MPD type="dynamic" minimumUpdatePeriod="PT%(updateTime)dS"', ' timeShiftBufferDepth="PT1S"', ' suggestedPresentationDelay="PT5S"', ' availabilityStartTime="1970-01-01T00:00:00Z">', ' <Period id="1">', ' <AdaptationSet mimeType="video/mp4">', ' <Representation id="3" bandwidth="500">', ' <BaseURL>http://example.com</BaseURL>', '%(contents)s', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); let text = sprintf( template, {updateTime: updateTime, contents: basicLines.join('\n')}); fakeNetEngine.setResponseText('dummy://foo', text); Date.now = function() { return 0; }; parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(manifest.periods.length).toBe(1); let timeline = manifest.presentationTimeline; expect(timeline.getDuration()).toBe(Infinity); }).catch(fail).then(done); PromiseMock.flush(); }); it('sets infinite duration for multi-period live streams', function(done) { let template = [ '<MPD type="dynamic" minimumUpdatePeriod="PT%(updateTime)dS"', ' timeShiftBufferDepth="PT1S"', ' suggestedPresentationDelay="PT5S"', ' availabilityStartTime="1970-01-01T00:00:00Z">', ' <Period id="1">', ' <AdaptationSet mimeType="video/mp4">', ' <Representation id="3" bandwidth="500">', ' <BaseURL>http://example.com</BaseURL>', '%(contents)s', ' </Representation>', ' </AdaptationSet>', ' </Period>', ' <Period id="2" start="PT60S">', ' <AdaptationSet mimeType="video/mp4">', ' <Representation id="4" bandwidth="500">', ' <BaseURL>http://example.com</BaseURL>', '%(contents)s', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); let text = sprintf( template, {updateTime: updateTime, contents: basicLines.join('\n')}); fakeNetEngine.setResponseText('dummy://foo', text); Date.now = function() { return 0; }; parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(manifest.periods.length).toBe(2); expect(manifest.periods[1].startTime).toBe(60); let timeline = manifest.presentationTimeline; expect(timeline.getDuration()).toBe(Infinity); }).catch(fail).then(done); PromiseMock.flush(); }); } it('can add Periods', function(done) { let lines = [ '<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />', ]; let template = [ '<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"', ' suggestedPresentationDelay="PT5S"', ' minimumUpdatePeriod="PT%(updateTime)dS">', ' <Period id="4">', ' <AdaptationSet mimeType="video/mp4">', ' <Representation id="6" bandwidth="500">', ' <BaseURL>http://example.com</BaseURL>', '%(contents)s', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); let secondManifest = sprintf(template, {updateTime: updateTime, contents: lines.join('\n')}); let firstManifest = makeSimpleLiveManifestText(lines, updateTime); let filterNewPeriod = jasmine.createSpy('filterNewPeriod'); playerInterface.filterNewPeriod = Util.spyFunc(filterNewPeriod); let filterAllPeriods = jasmine.createSpy('filterAllPeriods'); playerInterface.filterAllPeriods = Util.spyFunc(filterAllPeriods); fakeNetEngine.setResponseText('dummy://foo', firstManifest); parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(manifest.periods.length).toBe(1); // Should call filterAllPeriods for parsing the first manifest expect(filterNewPeriod.calls.count()).toBe(0); expect(filterAllPeriods.calls.count()).toBe(1); fakeNetEngine.setResponseText('dummy://foo', secondManifest); delayForUpdatePeriod(); // Should update the same manifest object. expect(manifest.periods.length).toBe(2); // Should call filterNewPeriod for parsing the new manifest expect(filterAllPeriods.calls.count()).toBe(1); expect(filterNewPeriod.calls.count()).toBe(1); }).catch(fail).then(done); PromiseMock.flush(); }); it('uses redirect URL for manifest BaseURL and updates', function(done) { let template = [ '<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"', ' suggestedPresentationDelay="PT5S"', ' minimumUpdatePeriod="PT%(updateTime)dS">', ' <Period id="1" duration="PT30S">', ' <AdaptationSet mimeType="video/mp4">', ' <Representation id="3" bandwidth="500">', ' <SegmentTemplate startNumber="1" media="s$Number$.mp4">', ' <SegmentTimeline>', ' <S d="10" t="0" />', ' <S d="5" />', ' <S d="15" />', ' </SegmentTimeline>', ' </SegmentTemplate>', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); let manifestText = sprintf(template, {updateTime: updateTime}); let manifestData = shaka.util.StringUtils.toUTF8(manifestText); let redirectedUri = 'http://redirected.com/'; // The initial manifest request will be redirected. fakeNetEngine.request.and.returnValue( shaka.util.AbortableOperation.completed({ uri: redirectedUri, data: manifestData, })); parser.start(originalUri, playerInterface) .then(function(manifest) { // The manifest request was made to the original URL. // But includes a redirect expect(fakeNetEngine.request.calls.count()).toBe(1); let netRequest = fakeNetEngine.request.calls.argsFor(0)[1]; expect(netRequest.uris).toEqual([redirectedUri, originalUri]); // Since the manifest request was redirected, the segment refers to // the redirected base. let stream = manifest.periods[0].variants[0].video; let segmentUri = stream.getSegmentReference(1).getUris()[0]; expect(segmentUri).toBe(redirectedUri + 's1.mp4'); }).catch(fail).then(done); PromiseMock.flush(); }); it('calls the error callback if an update fails', function(done) { let lines = [ '<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />', ]; let manifest = makeSimpleLiveManifestText(lines, updateTime); let onError = jasmine.createSpy('onError'); playerInterface.onError = Util.spyFunc(onError); fakeNetEngine.setResponseText('dummy://foo', manifest); parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(fakeNetEngine.request.calls.count()).toBe(1); let error = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.NETWORK, shaka.util.Error.Code.BAD_HTTP_STATUS); let operation = shaka.util.AbortableOperation.failed(error); fakeNetEngine.request.and.returnValue(operation); delayForUpdatePeriod(); expect(onError.calls.count()).toBe(1); }).catch(fail).then(done); PromiseMock.flush(); }); it('uses @minimumUpdatePeriod', function(done) { let lines = [ '<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />', ]; // updateTime parameter sets @minimumUpdatePeriod in the manifest. let manifest = makeSimpleLiveManifestText(lines, updateTime); fakeNetEngine.setResponseText('dummy://foo', manifest); parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(fakeNetEngine.request.calls.count()).toBe(1); expect(manifest).toBeTruthy(); let partialTime = updateTime * 1000 * 3 / 4; let remainingTime = updateTime * 1000 - partialTime; jasmine.clock().tick(partialTime); PromiseMock.flush(); // Update period has not passed yet. expect(fakeNetEngine.request.calls.count()).toBe(1); jasmine.clock().tick(remainingTime); PromiseMock.flush(); // Update period has passed. expect(fakeNetEngine.request.calls.count()).toBe(2); }).catch(fail).then(done); PromiseMock.flush(); }); it('still updates when @minimumUpdatePeriod is zero', function(done) { let lines = [ '<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />', ]; // updateTime parameter sets @minimumUpdatePeriod in the manifest. let manifest = makeSimpleLiveManifestText(lines, /* updateTime */ 0); fakeNetEngine.setResponseText('dummy://foo', manifest); parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(manifest).toBeTruthy(); fakeNetEngine.request.calls.reset(); let waitTimeMs = shaka.dash.DashParser['MIN_UPDATE_PERIOD_'] * 1000; jasmine.clock().tick(waitTimeMs); PromiseMock.flush(); // Update period has passed. expect(fakeNetEngine.request).toHaveBeenCalled(); }).catch(fail).then(done); PromiseMock.flush(); }); it('does not update when @minimumUpdatePeriod is missing', function(done) { let lines = [ '<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />', ]; // updateTime parameter sets @minimumUpdatePeriod in the manifest. let manifest = makeSimpleLiveManifestText(lines, /* updateTime */ null); fakeNetEngine.setResponseText('dummy://foo', manifest); parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(manifest).toBeTruthy(); fakeNetEngine.request.calls.reset(); let waitTimeMs = shaka.dash.DashParser['MIN_UPDATE_PERIOD_'] * 1000; jasmine.clock().tick(waitTimeMs * 2); PromiseMock.flush(); // Even though we have waited longer than the minimum update period, // the missing attribute means "do not update". So no update should // have happened. expect(fakeNetEngine.request).not.toHaveBeenCalled(); }).catch(fail).then(done); PromiseMock.flush(); }); it('delays subsequent updates when an update is slow', function(done) { // For this test, we want Date.now() to follow the ticks of the fake clock. jasmine.clock().mockDate(); const lines = [ '<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />', ]; const idealUpdateTime = shaka.dash.DashParser['MIN_UPDATE_PERIOD_']; const manifestText = makeSimpleLiveManifestText(lines, idealUpdateTime); fakeNetEngine.setResponseText('dummy://foo', manifestText); parser.start('dummy://foo', playerInterface).then((manifest) => { fakeNetEngine.request.calls.reset(); // Make the first update take a long time. const delay = fakeNetEngine.delayNextRequest(); // Wait for the update to start. jasmine.clock().tick(idealUpdateTime * 1000); PromiseMock.flush(); // Update period has passed, so an update has been requested. expect(fakeNetEngine.request).toHaveBeenCalled(); fakeNetEngine.request.calls.reset(); // Make the update take an extra 15 seconds, then end the delay. const extraWaitTimeMs = 15.0; jasmine.clock().tick(extraWaitTimeMs * 1000); delay.resolve(); PromiseMock.flush(); // No new calls, since we are still working on the same one. expect(fakeNetEngine.request).not.toHaveBeenCalled(); fakeNetEngine.request.calls.reset(); // From now on, the updates should be farther apart. jasmine.clock().tick(idealUpdateTime * 1000); PromiseMock.flush(); // The update should not have happened yet. expect(fakeNetEngine.request).not.toHaveBeenCalled(); fakeNetEngine.request.calls.reset(); // After waiting the extra time, the update request should fire. jasmine.clock().tick(extraWaitTimeMs * 1000); PromiseMock.flush(); expect(fakeNetEngine.request).toHaveBeenCalled(); }).catch(fail).then(done); PromiseMock.flush(); }); it('uses Mpd.Location', function(done) { let manifestText = [ '<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"', ' suggestedPresentationDelay="PT5S"', ' minimumUpdatePeriod="PT' + updateTime + 'S">', ' <Location>http://foobar</Location>', ' <Location>http://foobar2</Location>', ' <Location>foobar3</Location>', ' <Period id="1" duration="PT10S">', ' <AdaptationSet mimeType="video/mp4">', ' <Representation id="3" bandwidth="500">', '<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifestText); const manifestRequest = shaka.net.NetworkingEngine.RequestType.MANIFEST; parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(fakeNetEngine.request.calls.count()).toBe(1); fakeNetEngine.expectRequest('dummy://foo', manifestRequest); fakeNetEngine.request.calls.reset(); // Create a mock so we can verify it gives two URIs. // The third location is a relative url, and should be resolved as an // absolute url. fakeNetEngine.request.and.callFake(function(type, request) { expect(type).toBe(manifestRequest); expect(request.uris).toEqual( ['http://foobar', 'http://foobar2', 'dummy://foo/foobar3']); let data = shaka.util.StringUtils.toUTF8(manifestText); return shaka.util.AbortableOperation.completed( {uri: request.uris[0], data: data, headers: {}}); }); delayForUpdatePeriod(); expect(fakeNetEngine.request.calls.count()).toBe(1); }).catch(fail).then(done); PromiseMock.flush(); }); it('uses @suggestedPresentationDelay', function(done) { let manifest = [ '<MPD type="dynamic" suggestedPresentationDelay="PT60S"', ' minimumUpdatePeriod="PT5S"', ' timeShiftBufferDepth="PT2M"', ' maxSegmentDuration="PT10S"', ' availabilityStartTime="1970-01-01T00:05:00Z">', ' <Period id="1">', ' <AdaptationSet mimeType="video/mp4">', ' <Representation id="3" bandwidth="500">', ' <BaseURL>http://example.com</BaseURL>', '<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifest); 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(); // We are 5 minutes into the presentation, with a // @timeShiftBufferDepth of 120 seconds and a @maxSegmentDuration of // 10 seconds, the start will be 2:50. expect(timeline.getSegmentAvailabilityStart()).toBe(170); // Normally the end should be 4:50; but with a 60 second // @suggestedPresentationDelay it will be 3:50 minutes. expect(timeline.getSegmentAvailabilityEnd()).toBe(290); expect(timeline.getSeekRangeEnd()).toBe(230); }).catch(fail).then(done); PromiseMock.flush(); }); describe('availabilityWindowOverride', function() { it('overrides @timeShiftBufferDepth', function(done) { let manifest = [ '<MPD type="dynamic" suggestedPresentationDelay="PT60S"', ' minimumUpdatePeriod="PT5S"', ' timeShiftBufferDepth="PT2M"', ' maxSegmentDuration="PT10S"', ' availabilityStartTime="1970-01-01T00:05:00Z">', ' <Period id="1">', ' <AdaptationSet mimeType="video/mp4">', ' <Representation id="3" bandwidth="500">', ' <BaseURL>http://example.com</BaseURL>', ' <SegmentTemplate media="s$Number$.mp4" duration="2" />', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifest); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.availabilityWindowOverride = 4 * 60; parser.configure(config); 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 = [ '<MPD type="dynamic" suggestedPresentationDelay="PT0S"', ' minimumUpdatePeriod="PT5S"', ' timeShiftBufferDepth="PT2M"', ' maxSegmentDuration="PT15S"', ' availabilityStartTime="1970-01-01T00:05:00Z">', ' <Period id="1">', ' <AdaptationSet id="2" mimeType="video/mp4">', ' <Representation id="3" bandwidth="500">', ' <BaseURL>http://example.com</BaseURL>', '<SegmentTemplate media="s$Number$.mp4" duration="2" />', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', manifest); 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(); expect(timeline.getMaxSegmentDuration()).toBe(15); }).catch(fail).then(done); PromiseMock.flush(); }); it('derived from SegmentTemplate w/ SegmentTimeline', function(done) { let lines = [ '<SegmentTemplate media="s$Number$.mp4">', ' <SegmentTimeline>', ' <S t="0" d="7" />', ' <S d="8" />', ' <S d="6" />', ' </SegmentTimeline>', '</SegmentTemplate>', ]; testDerived(lines, done); }); it('derived from SegmentTemplate w/ @duration', function(done) { let lines = [ '<SegmentTemplate media="s$Number$.mp4" duration="8" />', ]; testDerived(lines, done); }); it('derived from SegmentList', function(done) { let lines = [ '<SegmentList duration="8">', ' <SegmentURL media="s1.mp4" />', ' <SegmentURL media="s2.mp4" />', '</SegmentList>', ]; testDerived(lines, done); }); it('derived from SegmentList w/ SegmentTimeline', function(done) { let lines = [ '<SegmentList duration="8">', ' <SegmentTimeline>', ' <S t="0" d="5" />', ' <S d="4" />', ' <S d="8" />', ' </SegmentTimeline>', ' <SegmentURL media="s1.mp4" />', ' <SegmentURL media="s2.mp4" />', '</SegmentList>', ]; testDerived(lines, done); }); function testDerived(lines, done) { let template = [ '<MPD type="dynamic" suggestedPresentationDelay="PT0S"', ' minimumUpdatePeriod="PT5S"', ' timeShiftBufferDepth="PT2M"', ' availabilityStartTime="1970-01-01T00:05:00Z">', ' <Period id="1">', ' <AdaptationSet id="2" mimeType="video/mp4">', ' <Representation id="3" bandwidth="500">', ' <BaseURL>http://example.com</BaseURL>', '%(contents)s', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); let manifest = sprintf(template, {contents: lines.join('\n')}); fakeNetEngine.setResponseText('dummy://foo', manifest); 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(); // NOTE: the largest segment is 8 seconds long in each test. expect(timeline.getMaxSegmentDuration()).toBe(8); }).catch(fail).then(done); PromiseMock.flush(); } }); // describe('maxSegmentDuration') describe('stop', function() { const manifestRequestType = shaka.net.NetworkingEngine.RequestType.MANIFEST; const dateRequestType = shaka.net.NetworkingEngine.RequestType.TIMING; const manifestUri = 'dummy://foo'; const dateUri = 'http://foo.bar/date'; beforeEach(function() { let manifest = [ '<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"', ' minimumUpdatePeriod="PT' + updateTime + 'S">', ' <UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014"', ' value="http://foo.bar/date" />', ' <UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014"', ' value="http://foo.bar/date" />', ' <Period id="1">', ' <AdaptationSet mimeType="video/mp4">', ' <Representation id="3" bandwidth="500">', ' <BaseURL>http://example.com</BaseURL>', ' <SegmentTemplate startNumber="1" media="s$Number$.mp4"', ' duration="2" />', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); fakeNetEngine .setResponseText('http://foo.bar/date', '1970-01-01T00:00:30Z') .setResponseText('dummy://foo', manifest); }); it('stops updates', function(done) { parser.start(manifestUri, playerInterface) .then(function(manifest) { fakeNetEngine.expectRequest(manifestUri, manifestRequestType); fakeNetEngine.request.calls.reset(); parser.stop(); delayForUpdatePeriod(); expect(fakeNetEngine.request).not.toHaveBeenCalled(); }).catch(fail).then(done); PromiseMock.flush(); }); it('stops initial parsing', function(done) { parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(manifest).toBe(null); fakeNetEngine.expectRequest(manifestUri, manifestRequestType); fakeNetEngine.request.calls.reset(); delayForUpdatePeriod(); // An update should not occur. expect(fakeNetEngine.request).not.toHaveBeenCalled(); }).catch(() => {}).then(done); // start will only begin the network request, calling stop here will be // after the request has started but before any parsing has been done. expect(fakeNetEngine.request.calls.count()).toBe(1); parser.stop(); PromiseMock.flush(); }); it('interrupts manifest updates', function(done) { parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(manifest).toBeTruthy(); fakeNetEngine.expectRequest(manifestUri, manifestRequestType); fakeNetEngine.request.calls.reset(); let delay = fakeNetEngine.delayNextRequest(); delayForUpdatePeriod(); // The request was made but should not be resolved yet. expect(fakeNetEngine.request.calls.count()).toBe(1); fakeNetEngine.expectRequest(manifestUri, manifestRequestType); fakeNetEngine.request.calls.reset(); parser.stop(); delay.resolve(); PromiseMock.flush(); // Wait for another update period. delayForUpdatePeriod(); // A second update should not occur. expect(fakeNetEngine.request).not.toHaveBeenCalled(); }).catch(fail).then(done); PromiseMock.flush(); }); it('interrupts UTCTiming requests', function(done) { /** @type {!shaka.util.PublicPromise} */ let delay = fakeNetEngine.delayNextRequest(); Util.delay(0.2, realTimeout).then(function() { // This is the initial manifest request. expect(fakeNetEngine.request.calls.count()).toBe(1); fakeNetEngine.expectRequest(manifestUri, manifestRequestType); fakeNetEngine.request.calls.reset(); // Resolve the manifest request and wait on the UTCTiming request. delay.resolve(); delay = fakeNetEngine.delayNextRequest(); return Util.delay(0.2, realTimeout); }).then(function() { // This is the first UTCTiming request. expect(fakeNetEngine.request.calls.count()).toBe(1); fakeNetEngine.expectRequest(dateUri, dateRequestType); fakeNetEngine.request.calls.reset(); // Interrupt the parser, then fail the request. parser.stop(); delay.reject(); return Util.delay(0.1, realTimeout); }).then(function() { // Wait for another update period. delayForUpdatePeriod(); // No more updates should occur. expect(fakeNetEngine.request).not.toHaveBeenCalled(); }).catch(fail).then(done); parser.start('dummy://foo', playerInterface).catch(() => {}); PromiseMock.flush(); }); }); describe('SegmentTemplate w/ SegmentTimeline', function() { let basicLines = [ '<SegmentTemplate startNumber="1" media="s$Number$.mp4">', ' <SegmentTimeline>', ' <S d="10" t="0" />', ' <S d="5" />', ' <S d="15" />', ' </SegmentTimeline>', '</SegmentTemplate>', ]; let basicRefs = [ shaka.test.ManifestParser.makeReference('s1.mp4', 1, 0, 10, originalUri), shaka.test.ManifestParser.makeReference('s2.mp4', 2, 10, 15, originalUri), shaka.test.ManifestParser.makeReference('s3.mp4', 3, 15, 30, originalUri), ]; let updateLines = [ '<SegmentTemplate startNumber="1" media="s$Number$.mp4">', ' <SegmentTimeline>', ' <S d="10" t="0" />', ' <S d="5" />', ' <S d="15" />', ' <S d="10" />', ' </SegmentTimeline>', '</SegmentTemplate>', ]; let updateRefs = [ shaka.test.ManifestParser.makeReference('s1.mp4', 1, 0, 10, originalUri), shaka.test.ManifestParser.makeReference('s2.mp4', 2, 10, 15, originalUri), shaka.test.ManifestParser.makeReference('s3.mp4', 3, 15, 30, originalUri), shaka.test.ManifestParser.makeReference('s4.mp4', 4, 30, 40, originalUri), ]; let partialUpdateLines = [ '<SegmentTemplate startNumber="3" media="s$Number$.mp4">', ' <SegmentTimeline>', ' <S d="15" t="15" />', ' <S d="10" />', ' </SegmentTimeline>', '</SegmentTemplate>', ]; testCommonBehaviors( basicLines, basicRefs, updateLines, updateRefs, partialUpdateLines); }); describe('SegmentList w/ SegmentTimeline', function() { let basicLines = [ '<SegmentList>', ' <SegmentURL media="s1.mp4" />', ' <SegmentURL media="s2.mp4" />', ' <SegmentURL media="s3.mp4" />', ' <SegmentTimeline>', ' <S d="10" t="0" />', ' <S d="5" />', ' <S d="15" />', ' </SegmentTimeline>', '</SegmentList>', ]; let basicRefs = [ shaka.test.ManifestParser.makeReference('s1.mp4', 1, 0, 10, originalUri), shaka.test.ManifestParser.makeReference('s2.mp4', 2, 10, 15, originalUri), shaka.test.ManifestParser.makeReference('s3.mp4', 3, 15, 30, originalUri), ]; let updateLines = [ '<SegmentList>', ' <SegmentURL media="s1.mp4" />', ' <SegmentURL media="s2.mp4" />', ' <SegmentURL media="s3.mp4" />', ' <SegmentURL media="s4.mp4" />', ' <SegmentTimeline>', ' <S d="10" t="0" />', ' <S d="5" />', ' <S d="15" />', ' <S d="10" />', ' </SegmentTimeline>', '</SegmentList>', ]; let updateRefs = [ shaka.test.ManifestParser.makeReference('s1.mp4', 1, 0, 10, originalUri), shaka.test.ManifestParser.makeReference('s2.mp4', 2, 10, 15, originalUri), shaka.test.ManifestParser.makeReference('s3.mp4', 3, 15, 30, originalUri), shaka.test.ManifestParser.makeReference('s4.mp4', 4, 30, 40, originalUri), ]; let partialUpdateLines = [ '<SegmentList startNumber="3">', ' <SegmentURL media="s3.mp4" />', ' <SegmentURL media="s4.mp4" />', ' <SegmentTimeline>', ' <S d="15" t="15" />', ' <S d="10" />', ' </SegmentTimeline>', '</SegmentList>', ]; testCommonBehaviors( basicLines, basicRefs, updateLines, updateRefs, partialUpdateLines); }); describe('SegmentList w/ @duration', function() { let basicLines = [ '<SegmentList duration="10">', ' <SegmentURL media="s1.mp4" />', ' <SegmentURL media="s2.mp4" />', ' <SegmentURL media="s3.mp4" />', '</SegmentList>', ]; let basicRefs = [ shaka.test.ManifestParser.makeReference('s1.mp4', 1, 0, 10, originalUri), shaka.test.ManifestParser.makeReference('s2.mp4', 2, 10, 20, originalUri), shaka.test.ManifestParser.makeReference('s3.mp4', 3, 20, 30, originalUri), ]; let updateLines = [ '<SegmentList duration="10">', ' <SegmentURL media="s1.mp4" />', ' <SegmentURL media="s2.mp4" />', ' <SegmentURL media="s3.mp4" />', ' <SegmentURL media="s4.mp4" />', '</SegmentList>', ]; let updateRefs = [ shaka.test.ManifestParser.makeReference('s1.mp4', 1, 0, 10, originalUri), shaka.test.ManifestParser.makeReference('s2.mp4', 2, 10, 20, originalUri), shaka.test.ManifestParser.makeReference('s3.mp4', 3, 20, 30, originalUri), shaka.test.ManifestParser.makeReference('s4.mp4', 4, 30, 40, originalUri), ]; let partialUpdateLines = [ '<SegmentList startNumber="3" duration="10">', ' <SegmentURL media="s3.mp4" />', ' <SegmentURL media="s4.mp4" />', '</SegmentList>', ]; testCommonBehaviors( basicLines, basicRefs, updateLines, updateRefs, partialUpdateLines); }); describe('SegmentTemplate w/ duration', function() { let templateLines = [ '<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />', ]; it('produces sane references without assertions', function(done) { let manifest = makeSimpleLiveManifestText(templateLines, updateTime); fakeNetEngine.setResponseText('dummy://foo', manifest); parser.start('dummy://foo', playerInterface).then(function(manifest) { expect(manifest.periods.length).toBe(1); let stream = manifest.periods[0].variants[0].video; // In https://github.com/google/shaka-player/issues/1204, this // failed an assertion and returned endTime == 0. let ref = stream.getSegmentReference(1); expect(ref.endTime).toBeGreaterThan(0); }).catch(fail).then(done); PromiseMock.flush(); }); }); describe('EventStream', function() { const originalManifest = [ '<MPD type="dynamic" minimumUpdatePeriod="PT' + updateTime + 'S"', ' availabilityStartTime="1970-01-01T00:00:00Z">', ' <Period id="1" duration="PT60S" start="PT10S">', ' <EventStream schemeIdUri="http://example.com" value="foobar"', ' timescale="100">', ' <Event duration="5000" />', ' <Event id="abc" presentationTime="300" duration="1000" />', ' </EventStream>', ' <AdaptationSet mimeType="video/mp4">', ' <Representation bandwidth="1">', ' <SegmentBase indexRange="100-200" />', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); /** @type {!jasmine.Spy} */ let onTimelineRegionAddedSpy; beforeEach(function() { onTimelineRegionAddedSpy = jasmine.createSpy('onTimelineRegionAdded'); playerInterface.onTimelineRegionAdded = shaka.test.Util.spyFunc(onTimelineRegionAddedSpy); }); it('will parse EventStream nodes', function(done) { fakeNetEngine.setResponseText('dummy://foo', originalManifest); parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(onTimelineRegionAddedSpy).toHaveBeenCalledTimes(2); expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith({ schemeIdUri: 'http://example.com', value: 'foobar', startTime: 10, endTime: 60, id: '', eventElement: jasmine.any(Element), }); expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith({ schemeIdUri: 'http://example.com', value: 'foobar', startTime: 13, endTime: 23, id: 'abc', eventElement: jasmine.any(Element), }); }) .catch(fail) .then(done); PromiseMock.flush(); }); it('will add timeline regions on manifest update', function(done) { let newManifest = [ '<MPD type="dynamic" minimumUpdatePeriod="PT' + updateTime + 'S"', ' availabilityStartTime="1970-01-01T00:00:00Z">', ' <Period id="1" duration="PT30S">', ' <EventStream schemeIdUri="http://example.com" timescale="100">', ' <Event id="100" />', ' </EventStream>', ' <AdaptationSet mimeType="video/mp4">', ' <Representation bandwidth="1">', ' <SegmentBase indexRange="100-200" />', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', originalManifest); parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(onTimelineRegionAddedSpy).toHaveBeenCalledTimes(2); onTimelineRegionAddedSpy.calls.reset(); fakeNetEngine.setResponseText('dummy://foo', newManifest); delayForUpdatePeriod(); expect(onTimelineRegionAddedSpy).toHaveBeenCalledTimes(1); }) .catch(fail) .then(done); PromiseMock.flush(); }); it('will not let an event exceed the Period duration', function(done) { let newManifest = [ '<MPD>', ' <Period id="1" duration="PT30S">', ' <EventStream schemeIdUri="http://example.com" timescale="1">', ' <Event presentationTime="10" duration="15"/>', ' <Event presentationTime="25" duration="50"/>', ' <Event presentationTime="50" duration="10"/>', ' </EventStream>', ' <AdaptationSet mimeType="video/mp4">', ' <Representation bandwidth="1">', ' <SegmentBase indexRange="100-200" />', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', newManifest); parser.start('dummy://foo', playerInterface) .then(function(manifest) { expect(onTimelineRegionAddedSpy).toHaveBeenCalledTimes(3); expect(onTimelineRegionAddedSpy) .toHaveBeenCalledWith( jasmine.objectContaining({startTime: 10, endTime: 25})); expect(onTimelineRegionAddedSpy) .toHaveBeenCalledWith( jasmine.objectContaining({startTime: 25, endTime: 30})); expect(onTimelineRegionAddedSpy) .toHaveBeenCalledWith( jasmine.objectContaining({startTime: 30, endTime: 30})); }) .catch(fail) .then(done); PromiseMock.flush(); }); it('will parse multiple events at same offset', () => { const newManifest = [ '<MPD>', ' <Period id="1" duration="PT30S">', ' <EventStream schemeIdUri="http://example.com" timescale="1">', ' <Event id="1" presentationTime="10" duration="15"/>', ' <Event id="2" presentationTime="10" duration="15"/>', ' </EventStream>', ' <AdaptationSet mimeType="video/mp4">', ' <Representation bandwidth="1">', ' <SegmentBase indexRange="100-200" />', ' </Representation>', ' </AdaptationSet>', ' </Period>', '</MPD>', ].join('\n'); fakeNetEngine.setResponseText('dummy://foo', newManifest); parser.start('dummy://foo', playerInterface).catch(fail); PromiseMock.flush(); expect(onTimelineRegionAddedSpy).toHaveBeenCalledTimes(2); expect(onTimelineRegionAddedSpy) .toHaveBeenCalledWith( jasmine.objectContaining({id: '1', startTime: 10, endTime: 25})); expect(onTimelineRegionAddedSpy) .toHaveBeenCalledWith( jasmine.objectContaining({id: '2', startTime: 10, endTime: 25})); }); }); it('honors clockSyncUri for in-progress recordings', (done) => { const manifestText = [ '<MPD type="dynamic" availabilityStartTime="1970-01-01T00:05: