From 8648e76de9d4117fcf54c47c9670881b1dc279cc Mon Sep 17 00:00:00 2001 From: Alex Barstow Date: Wed, 23 Sep 2020 16:48:13 -0400 Subject: [PATCH] feat: Update minimumUpdatePeriod handling (#942) This is part of a set of upcoming changes to add support for live DASH playback. Specifically, this PR differentiates the handling of 2 cases which were formerly conflated: - The MPD@minimumUpdatePeriod attribute has a value of 0, indicating that the MPD has no validity after the moment it was retrieved. - The MPD@minimumUpdatePeriod attribute is absent, indicating the MPD has infinite validity and will never be updated --- package-lock.json | 6 +- package.json | 2 +- src/dash-playlist-loader.js | 48 +++++++------- test/dash-playlist-loader.test.js | 107 ++++++++++++++++++++++++++---- 4 files changed, 122 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index d8eb1a241..a652d5d5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6823,9 +6823,9 @@ "dev": true }, "mpd-parser": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.11.0.tgz", - "integrity": "sha512-z9DIs++G+dRopPT5ROQgNdnDbby+j/9dkGoCC20Hd/3swyR8YxTxM8jOtl3zO9cnhjoikVFJuMbH+V8SE0/f5Q==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.12.0.tgz", + "integrity": "sha512-Ov5Oz9bw5X/G8V/6PlO+rHuqKywYYjQ6USyv8fqFMs413HkrzlpDjgUKSBD7C+/J19ID5mWtxzrpMf4Yp++iZg==", "requires": { "@babel/runtime": "^7.5.5", "@videojs/vhs-utils": "^1.1.0", diff --git a/package.json b/package.json index cf78faaa5..a2dae5696 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "aes-decrypter": "3.0.2", "global": "^4.3.2", "m3u8-parser": "4.4.3", - "mpd-parser": "0.11.0", + "mpd-parser": "0.12.0", "mux.js": "5.6.6", "video.js": "^6 || ^7" }, diff --git a/src/dash-playlist-loader.js b/src/dash-playlist-loader.js index 8c54b67b4..92a6d571f 100644 --- a/src/dash-playlist-loader.js +++ b/src/dash-playlist-loader.js @@ -93,6 +93,10 @@ export const updateMaster = (oldMaster, newMaster) => { } }); + if (newMaster.minimumUpdatePeriod !== oldMaster.minimumUpdatePeriod) { + noChanges = false; + } + if (noChanges) { return null; } @@ -645,6 +649,22 @@ export default class DashPlaylistLoader extends EventTarget { } } + updateMinimumUpdatePeriodTimeout_() { + // Clear existing timeout + window.clearTimeout(this.minimumUpdatePeriodTimeout_); + + const minimumUpdatePeriod = this.master && this.master.minimumUpdatePeriod; + + if (minimumUpdatePeriod >= 0) { + this.minimumUpdatePeriodTimeout_ = window.setTimeout(() => { + this.trigger('minimumUpdatePeriod'); + // We use the target duration here because a minimumUpdatePeriod value of 0 + // indicates that the current MPD has no future validity, so a new one will + // need to be acquired when new media segments are to be made available + }, minimumUpdatePeriod || this.media().targetDuration * 1000); + } + } + /** * Handler for after client/server clock synchronization has happened. Sets up * xml refresh timer if specificed by the manifest. @@ -656,17 +676,7 @@ export default class DashPlaylistLoader extends EventTarget { this.media(this.master.playlists[0]); } - // TODO: minimumUpdatePeriod can have a value of 0. Currently the manifest will not - // be refreshed when this is the case. The inter-op guide says that when the - // minimumUpdatePeriod is 0, the manifest should outline all currently available - // segments, but future segments may require an update. I think a good solution - // would be to update the manifest at the same rate that the media playlists - // are "refreshed", i.e. every targetDuration. - if (this.master && this.master.minimumUpdatePeriod) { - this.minimumUpdatePeriodTimeout_ = window.setTimeout(() => { - this.trigger('minimumUpdatePeriod'); - }, this.master.minimumUpdatePeriod); - } + this.updateMinimumUpdatePeriodTimeout_(); } /** @@ -763,13 +773,7 @@ export default class DashPlaylistLoader extends EventTarget { // update loader's sidxMapping with parsed sidx box this.sidxMapping_[sidxKey].sidx = sidx; - // Clear & reset timeout with new minimumUpdatePeriod - window.clearTimeout(this.minimumUpdatePeriodTimeout_); - if (this.master.minimumUpdatePeriod) { - this.minimumUpdatePeriodTimeout_ = window.setTimeout(() => { - this.trigger('minimumUpdatePeriod'); - }, this.master.minimumUpdatePeriod); - } + this.updateMinimumUpdatePeriodTimeout_(); // TODO: do we need to reload the current playlist? this.refreshMedia_(this.media().id); @@ -786,13 +790,7 @@ export default class DashPlaylistLoader extends EventTarget { } } - // Clear & reset timeout with new minimumUpdatePeriod - window.clearTimeout(this.minimumUpdatePeriodTimeout_); - if (this.master.minimumUpdatePeriod) { - this.minimumUpdatePeriodTimeout_ = window.setTimeout(() => { - this.trigger('minimumUpdatePeriod'); - }, this.master.minimumUpdatePeriod); - } + this.updateMinimumUpdatePeriodTimeout_(); }); } diff --git a/test/dash-playlist-loader.test.js b/test/dash-playlist-loader.test.js index aac25ea5b..06e2d50f7 100644 --- a/test/dash-playlist-loader.test.js +++ b/test/dash-playlist-loader.test.js @@ -330,6 +330,62 @@ QUnit.test('updateMaster: updates playlists and mediaGroups', function(assert) { ); }); +QUnit.test('updateMaster: updates minimumUpdatePeriod', function(assert) { + const master = { + playlists: { + length: 1, + 0: { + uri: '0', + id: '0', + segments: [] + } + }, + mediaGroups: { + AUDIO: {}, + SUBTITLES: {} + }, + duration: 0, + minimumUpdatePeriod: 0 + }; + + const update = { + playlists: { + length: 1, + 0: { + uri: '0', + id: '0', + segments: [] + } + }, + mediaGroups: { + AUDIO: {}, + SUBTITLES: {} + }, + duration: 0, + minimumUpdatePeriod: 2 + }; + + assert.deepEqual( + updateMaster(master, update), + { + playlists: { + length: 1, + 0: { + uri: '0', + id: '0', + segments: [] + } + }, + mediaGroups: { + AUDIO: {}, + SUBTITLES: {} + }, + duration: 0, + minimumUpdatePeriod: 2 + } + ); +}); + QUnit.test('generateSidxKey: generates correct key', function(assert) { const sidxInfo = { byterange: { @@ -2386,7 +2442,7 @@ QUnit.test('refreshes the xml if there is a minimumUpdatePeriod', function(asser assert.equal(minimumUpdatePeriods, 1, 'refreshed manifest'); }); -QUnit.test('stop xml refresh if minimumUpdatePeriod changes from `mUP > 0` to `mUP == 0`', function(assert) { +QUnit.test('stop xml refresh if minimumUpdatePeriod is removed', function(assert) { const loader = new DashPlaylistLoader('dash-live.mpd', this.fakeVhs); let minimumUpdatePeriods = 0; @@ -2399,25 +2455,52 @@ QUnit.test('stop xml refresh if minimumUpdatePeriod changes from `mUP > 0` to `m this.standardXHRResponse(this.requests.shift()); assert.equal(minimumUpdatePeriods, 0, 'no refreshes immediately after response'); - // First Refresh Tick + // First Refresh Tick: MPD loaded this.clock.tick(4 * 1000); - this.standardXHRResponse(this.requests[0], loader.masterXml_); assert.equal(this.requests.length, 1, 'refreshed manifest'); assert.equal(this.requests[0].uri, 'dash-live.mpd', 'refreshed manifest'); assert.equal(minimumUpdatePeriods, 1, 'total minimumUpdatePeriods'); - // Second Refresh Tick: MinimumUpdatePeriod Removed - this.clock.tick(4 * 1000); - this.standardXHRResponse(this.requests[1], loader.masterXml_.replace('minimumUpdatePeriod="PT4S"', '')); + this.standardXHRResponse(this.requests[0], loader.masterXml_.replace('minimumUpdatePeriod="PT4S"', '')); + + // Second Refresh Tick: MUP removed this.clock.tick(4 * 1000); - this.standardXHRResponse(this.requests[2]); - assert.equal(this.requests.length, 3, 'final manifest refresh'); - assert.equal(minimumUpdatePeriods, 3, 'final minimumUpdatePeriods'); + assert.equal(this.requests.length, 1, 'no more manifest refreshes'); + assert.equal(minimumUpdatePeriods, 1, 'no more minimumUpdatePeriods'); +}); - // Third Refresh Tick: No Additional Requests Expected +QUnit.test('continue xml refresh every targetDuration if minimumUpdatePeriod is 0', function(assert) { + const loader = new DashPlaylistLoader('dash-live.mpd', this.fakeVhs); + let minimumUpdatePeriods = 0; + + loader.on('minimumUpdatePeriod', () => minimumUpdatePeriods++); + + loader.load(); + + // Start Request + assert.equal(minimumUpdatePeriods, 0, 'no refreshes to start'); + this.standardXHRResponse(this.requests.shift()); + assert.equal(minimumUpdatePeriods, 0, 'no refreshes immediately after response'); + + // First Refresh Tick this.clock.tick(4 * 1000); - assert.equal(this.requests.length, 3, 'final manifest refresh'); - assert.equal(minimumUpdatePeriods, 3, 'final minimumUpdatePeriods'); + assert.equal(this.requests.length, 1, 'refreshed manifest'); + assert.equal(this.requests[0].uri, 'dash-live.mpd', 'refreshed manifest'); + assert.equal(minimumUpdatePeriods, 1, 'total minimumUpdatePeriods'); + + this.standardXHRResponse(this.requests[0], loader.masterXml_.replace('minimumUpdatePeriod="PT4S"', 'minimumUpdatePeriod="PT0S"')); + + // Second Refresh Tick: MinimumUpdatePeriod set to 0 + // The manifest should refresh after one target duration, in this case 2 seconds. At this point + // it should not have occurred. + this.clock.tick(1 * 1000); + assert.equal(this.requests.length, 1, 'no 3rd manifest refresh yet'); + assert.equal(minimumUpdatePeriods, 1, 'no 3rd minimumUpdatePeriod yet'); + + // Now the refresh should happen + this.clock.tick(1 * 1000); + assert.equal(this.requests.length, 2, '3rd manifest refresh after targetDuration'); + assert.equal(minimumUpdatePeriods, 2, '3rd minimumUpdatePeriod after targetDuration'); }); QUnit.test('media playlists "refresh" by re-parsing master xml', function(assert) {