Skip to content

Commit

Permalink
feat: Update minimumUpdatePeriod handling (#942)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
alex-barstow authored Sep 23, 2020
1 parent 8801d1c commit 8648e76
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 41 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
48 changes: 23 additions & 25 deletions src/dash-playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export const updateMaster = (oldMaster, newMaster) => {
}
});

if (newMaster.minimumUpdatePeriod !== oldMaster.minimumUpdatePeriod) {
noChanges = false;
}

if (noChanges) {
return null;
}
Expand Down Expand Up @@ -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.
Expand All @@ -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_();
}

/**
Expand Down Expand Up @@ -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);
Expand All @@ -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_();
});
}

Expand Down
107 changes: 95 additions & 12 deletions test/dash-playlist-loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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;

Expand All @@ -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) {
Expand Down

0 comments on commit 8648e76

Please sign in to comment.