From 930c158629d352b1e5186b5ad0eeb370e95c7aaa Mon Sep 17 00:00:00 2001 From: Enson Choy Date: Fri, 20 Aug 2021 15:14:21 +0800 Subject: [PATCH] Fix: Failover in geo-redundant streams This is for fixing a case in geo-redundant streams failover: 1. ShakaPlayer identifies DASH period based on period id 2. Period ids generated by two synchronized packager could be inconsistent temporarily in some negative scenarios 3. After the scenario the packagers resync at the live edge, but the inconsistency is preserved in the manifest for the length of the DVR window 4. When ShakaPlayer receives a new mpd with inconsistent period ids (i.e. jumping between mpds from two packagers), it might result in - Wrong presentation time - Wrong buffer ahead estimation - Old segment references added into segment index 5. Consequently, playback gets stalled and requires end-user to refresh the player (i.e. VPF) --- lib/dash/dash_parser.js | 24 ++++++ test/dash/dash_parser_manifest_unit.js | 106 +++++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 1cb4971d96..d1ed81fc05 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -95,6 +95,12 @@ shaka.dash.DashParser = class { /** @private {!shaka.util.OperationManager} */ this.operationManager_ = new shaka.util.OperationManager(); + /** + * Largest period start time seen. + * @private {?number} + */ + this.largestPeriodStartTime_ = null; + /** * The minimum of the availabilityTimeOffset values among the adaptation * sets. @@ -571,6 +577,24 @@ shaka.dash.DashParser = class { periodDuration = givenDuration; } + // Skip all periods with start time < maximum period start time, excepts + // the last period in manifest + if (this.largestPeriodStartTime_ !== null && start !== null && + start < this.largestPeriodStartTime_ && + i + 1 != periodNodes.length) { + shaka.log.debug( + 'Skipping Period', i + 1, ' as its start time is smaller than ' + + 'the largest period start time that has been seen.'); + continue; + } + + // Save maximum period start time if it is the last period + if (start !== null && + (this.largestPeriodStartTime_ === null || + start > this.largestPeriodStartTime_)) { + this.largestPeriodStartTime_ = start; + } + // Parse child nodes. const info = { start: start, diff --git a/test/dash/dash_parser_manifest_unit.js b/test/dash/dash_parser_manifest_unit.js index 017226d962..37b03124d1 100644 --- a/test/dash/dash_parser_manifest_unit.js +++ b/test/dash/dash_parser_manifest_unit.js @@ -2150,4 +2150,110 @@ describe('DashParser Manifest', () => { expect(uri).not.toContain('/p2/'); } }); + + it('parses ServiceConfiguration', async () => { + const manifestText = [ + `', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' http://example.com/p1/', + ' ', + ' ', + ' ', + '', + ].join('\n'); + + fakeNetEngine.setResponseText('dummy://foo', manifestText); + + await parser.start('dummy://foo', playerInterface); + + const serviceDescription = parser.getServiceDescription(); + expect(serviceDescription).toBeDefined(); + expect(serviceDescription.scope.schemeIdUri).toBe('1'); + expect(serviceDescription.scope.value).toBe('scope1'); + expect(serviceDescription.latency.target).toBe(5000); + expect(serviceDescription.latency.max).toBe(7000); + expect(serviceDescription.latency.min).toBe(4000); + expect(serviceDescription.playbackRate.max).toBe(1.1); + expect(serviceDescription.playbackRate.min).toBe(0.9); + }); + + /** + * @param {!Array.} periods Start time of multiple periods + * @return {string} + */ + function buildManifestWithPeriodStartTime(periods) { + const mpdTemplate = [ + `', + ' %(periods)s', + '', + ].join('\n'); + const periodTemplate = (id, period, duration) => { + return [ + ` `, + ' ', + ' ', + ' ', + ` `, + ' ', + ' ', + ' ', + ' ', + ' ', + ].join('\n'); + }; + const periodXmls = periods.map((period, i) => { + const duration = i+1 === periods.length ? 10 : periods[i+1] - period; + return periodTemplate(i+1, period, duration); + }); + return sprintf(mpdTemplate, { + periods: periodXmls.join('\n'), + }); + } + + // Bug description: Inconsistent period start time in the manifests due + // to failover triggered in backend servers + + // When one of the servers is down, the manifest will be served by other + // redundant servers. The period start time might become out of sync + // during the switch-over/recovery. + + // Solution: Ignore old DASH periods that are older than the latest one. + + it('skip periods that are earlier than max period start time', async () => { + const sources = [ + buildManifestWithPeriodStartTime([5, 15]), + buildManifestWithPeriodStartTime([4, 15]), // simulate out-of-sync of -1s + ]; + const segments = []; + + for (const source of sources) { + fakeNetEngine.setResponseText('dummy://foo', source); + // eslint-disable-next-line no-await-in-loop + const manifest = await parser.start('dummy://foo', playerInterface); + const video = manifest.variants[0].video; + // eslint-disable-next-line no-await-in-loop + await video.createSegmentIndex(); + segments.push(Array.from(video.segmentIndex)); + } + + // Expect identical segments + expect(segments[0][0].startTime).toBe(5); + expect(segments[1][0].startTime).toBe(5); + expect(segments[0].length).toBe(2); + expect(segments[1].length).toBe(2); + }); });