diff --git a/package-lock.json b/package-lock.json index 7f613a542..2ee6d8f3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1284,6 +1284,19 @@ "mpd-parser": "0.14.0", "mux.js": "5.6.7", "video.js": "^6 || ^7" + }, + "dependencies": { + "mpd-parser": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.14.0.tgz", + "integrity": "sha512-HqXQS3WLofcnYFcxv5oWdlciddUaEnN3NasXLVQ793mdnZRrinjz2Yk1DsUYPDYOUWf6ZBBqbFhaJT5LiT2ouA==", + "requires": { + "@babel/runtime": "^7.5.5", + "@videojs/vhs-utils": "^2.2.1", + "global": "^4.3.2", + "xmldom": "^0.1.27" + } + } } }, "@videojs/vhs-utils": { @@ -7131,9 +7144,9 @@ "dev": true }, "mpd-parser": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.14.0.tgz", - "integrity": "sha512-HqXQS3WLofcnYFcxv5oWdlciddUaEnN3NasXLVQ793mdnZRrinjz2Yk1DsUYPDYOUWf6ZBBqbFhaJT5LiT2ouA==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.15.0.tgz", + "integrity": "sha512-GfspJVaEnVbWKZQASvh9nsJkvxWh3M/c5Kb2RPnN5ZXPZ7jWWfarWkNKTEuqvoaAKIT8IB/r6PFTWA1GY4fzGg==", "requires": { "@babel/runtime": "^7.5.5", "@videojs/vhs-utils": "^2.2.1", diff --git a/package.json b/package.json index ba4bbf1ca..0a5a9451b 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "aes-decrypter": "3.1.0", "global": "^4.3.2", "m3u8-parser": "4.5.0", - "mpd-parser": "0.14.0", + "mpd-parser": "0.15.0", "mux.js": "5.6.7", "video.js": "^6 || ^7" }, diff --git a/src/dash-playlist-loader.js b/src/dash-playlist-loader.js index 64c513fec..4fef31e62 100644 --- a/src/dash-playlist-loader.js +++ b/src/dash-playlist-loader.js @@ -1,6 +1,7 @@ import videojs from 'video.js'; import { parse as parseMpd, + addSidxSegmentsToPlaylist, parseUTCTiming } from 'mpd-parser'; import { @@ -48,6 +49,18 @@ export const parseMasterXml = ({ masterXml, srcUrl, clientOffset, sidxMapping }) return master; }; +export const generateSidxKey = (sidxInfo) => { + // should be non-inclusive + const sidxByteRangeEnd = + sidxInfo.byterange.offset + + sidxInfo.byterange.length - + 1; + + return sidxInfo.uri + '-' + + sidxInfo.byterange.offset + '-' + + sidxByteRangeEnd; +}; + /** * Returns a new master manifest that is the result of merging an updated master manifest * into the original version. @@ -60,7 +73,7 @@ export const parseMasterXml = ({ masterXml, srcUrl, clientOffset, sidxMapping }) * A new object representing the original master manifest with the updated media * playlists merged in */ -export const updateMaster = (oldMaster, newMaster) => { +export const updateMaster = (oldMaster, newMaster, sidxMapping) => { let noChanges = true; let update = mergeOptions(oldMaster, { // These are top level properties that can be updated @@ -70,7 +83,16 @@ export const updateMaster = (oldMaster, newMaster) => { // First update the playlists in playlist list for (let i = 0; i < newMaster.playlists.length; i++) { - const playlistUpdate = updatePlaylist(update, newMaster.playlists[i]); + const playlist = newMaster.playlists[i]; + + if (playlist.sidx) { + const sidxKey = generateSidxKey(playlist.sidx); + + if (sidxMapping && sidxMapping[sidxKey]) { + addSidxSegmentsToPlaylist(playlist, sidxMapping[sidxKey].sidx, playlist.sidx.resolvedUri); + } + } + const playlistUpdate = updatePlaylist(update, playlist); if (playlistUpdate) { update = playlistUpdate; @@ -104,18 +126,6 @@ export const updateMaster = (oldMaster, newMaster) => { return update; }; -export const generateSidxKey = (sidxInfo) => { - // should be non-inclusive - const sidxByteRangeEnd = - sidxInfo.byterange.offset + - sidxInfo.byterange.length - - 1; - - return sidxInfo.uri + '-' + - sidxInfo.byterange.offset + '-' + - sidxByteRangeEnd; -}; - // SIDX should be equivalent if the URI and byteranges of the SIDX match. // If the SIDXs have maps, the two maps should match, // both `a` and `b` missing SIDXs is considered matching. @@ -164,18 +174,10 @@ export const compareSidxEntry = (playlists, oldSidxMapping) => { * * The method is exported for testing * - * @param {Object} masterXml the mpd XML - * @param {string} srcUrl the mpd url - * @param {Date} clientOffset a time difference between server and client (passed through and not used) + * @param {Object} master the parsed mpd XML returned via mpd-parser * @param {Object} oldSidxMapping the SIDX to compare against */ -export const filterChangedSidxMappings = (masterXml, srcUrl, clientOffset, oldSidxMapping) => { - // Don't pass current sidx mapping - const master = parseMpd(masterXml, { - manifestUri: srcUrl, - clientOffset - }); - +export const filterChangedSidxMappings = (master, oldSidxMapping) => { const videoSidx = compareSidxEntry(master.playlists, oldSidxMapping); let mediaGroupSidx = videoSidx; @@ -438,10 +440,12 @@ export default class DashPlaylistLoader extends EventTarget { // update loader's sidxMapping with parsed sidx box sidxMapping[sidxKey].sidx = sidx; + addSidxSegmentsToPlaylist(playlist, sidx, playlist.sidx.resolvedUri); + // everything is ready just continue to haveMetadata this.haveMetadata({ startingState, - playlist: newMaster.playlists[playlist.id] + playlist }); }) ); @@ -509,10 +513,7 @@ export default class DashPlaylistLoader extends EventTarget { // We don't need to request the master manifest again // Call this asynchronously to match the xhr request behavior below if (!this.isMaster_) { - this.mediaRequest_ = window.setTimeout( - this.haveMaster_.bind(this), - 0 - ); + this.mediaRequest_ = window.setTimeout(() => this.haveMaster_(false), 0); return; } @@ -543,6 +544,10 @@ export default class DashPlaylistLoader extends EventTarget { return this.trigger('error'); } + if (req.responseText === this.masterPlaylistLoader_.masterXml_) { + return this.haveMaster_(false); + } + this.masterPlaylistLoader_.masterXml_ = req.responseText; if (req.responseHeaders && req.responseHeaders.date) { @@ -616,18 +621,12 @@ export default class DashPlaylistLoader extends EventTarget { }); } - haveMaster_() { + haveMaster_(masterChanged = true) { this.state = 'HAVE_MASTER'; - // clear media request - this.mediaRequest_ = null; - if (this.isMaster_) { - this.updateMainManifest_(parseMasterXml({ - masterXml: this.masterPlaylistLoader_.masterXml_, - srcUrl: this.masterPlaylistLoader_.srcUrl, - clientOffset: this.masterPlaylistLoader_.clientOffset_, - sidxMapping: this.masterPlaylistLoader_.sidxMapping_ - })); + if (masterChanged) { + this.handleMaster_(); + } // We have the master playlist at this point, so // trigger this to allow MasterPlaylistController // to make an initial playlist selection @@ -639,6 +638,37 @@ export default class DashPlaylistLoader extends EventTarget { } } + handleMaster_() { + // clear media request + this.mediaRequest_ = null; + + let newMaster = parseMasterXml({ + masterXml: this.masterPlaylistLoader_.masterXml_, + srcUrl: this.masterPlaylistLoader_.srcUrl, + clientOffset: this.masterPlaylistLoader_.clientOffset_, + sidxMapping: this.masterPlaylistLoader_.sidxMapping_ + }); + + // if we have an old master to compare the new master against + if (this.masterPlaylistLoader_.master) { + newMaster = updateMaster( + this.masterPlaylistLoader_.master, + newMaster, + this.masterPlaylistLoader_.sidxMapping_ + ); + } + + // only update master if we have a new master + this.masterPlaylistLoader_.master = newMaster ? newMaster : this.masterPlaylistLoader_.master; + const location = this.masterPlaylistLoader_.master.locations && this.masterPlaylistLoader_.master.locations[0]; + + if (location && location !== this.masterPlaylistLoader_.srcUrl) { + this.masterPlaylistLoader_.srcUrl = location; + } + + return Boolean(newMaster); + } + updateMinimumUpdatePeriodTimeout_() { // Clear existing timeout window.clearTimeout(this.minimumUpdatePeriodTimeout_); @@ -685,26 +715,6 @@ export default class DashPlaylistLoader extends EventTarget { this.updateMinimumUpdatePeriodTimeout_(); } - /** - * Given a new manifest, update our pointer to it and update the srcUrl based on the location elements of the manifest, if they exist. - * - * @param {Object} updatedManifest the manifest to update to - */ - updateMainManifest_(updatedManifest) { - this.master = updatedManifest; - - // if locations isn't set or is an empty array, exit early - if (!this.master.locations || !this.master.locations.length) { - return; - } - - const location = this.master.locations[0]; - - if (location !== this.masterPlaylistLoader_.srcUrl) { - this.masterPlaylistLoader_.srcUrl = location; - } - } - /** * Sends request to refresh the master xml and updates the parsed master manifest * TODO: Does the client offset need to be recalculated when the xml is refreshed? @@ -738,65 +748,63 @@ export default class DashPlaylistLoader extends EventTarget { return this.trigger('error'); } + // xml is the same do nothing. + if (req.responseText === this.masterPlaylistLoader_.masterXml_) { + return; + } + this.masterPlaylistLoader_.masterXml_ = req.responseText; + this.handleMaster_(); // This will filter out updated sidx info from the mapping this.masterPlaylistLoader_.sidxMapping_ = filterChangedSidxMappings( - this.masterPlaylistLoader_.masterXml_, - this.masterPlaylistLoader_.srcUrl, - this.masterPlaylistLoader_.clientOffset_, + this.masterPlaylistLoader_.master, this.masterPlaylistLoader_.sidxMapping_ ); - - const master = parseMasterXml({ - masterXml: this.masterPlaylistLoader_.masterXml_, - srcUrl: this.masterPlaylistLoader_.srcUrl, - clientOffset: this.masterPlaylistLoader_.clientOffset_, - sidxMapping: this.masterPlaylistLoader_.sidxMapping_ - }); - const updatedMaster = updateMaster(this.master, master); const currentSidxInfo = this.media().sidx; - if (updatedMaster) { - if (currentSidxInfo) { - const sidxKey = generateSidxKey(currentSidxInfo); + if (this.media_) { + this.media_ = this.master.playlists[this.media_.id]; + } - // the sidx was updated, so the previous mapping was removed - if (!this.masterPlaylistLoader_.sidxMapping_[sidxKey]) { - const playlist = this.media(); + this.updateMinimumUpdatePeriodTimeout_(); + // current sidx not updated + if (!currentSidxInfo) { + return; + } + const sidxKey = generateSidxKey(currentSidxInfo); - this.request = requestSidx_( - this, - playlist.sidx, - playlist, - this.vhs_.xhr, - { handleManifestRedirects: this.handleManifestRedirects }, - this.sidxRequestFinished_(playlist, master, this.state, (newMaster, sidx) => { - if (!newMaster || !sidx) { - throw new Error('failed to request sidx on minimumUpdatePeriod'); - } + // sidxKey already exists + if (this.masterPlaylistLoader_.sidxMapping_[sidxKey]) { + return; + } + const playlist = this.media(); - // update loader's sidxMapping with parsed sidx box - this.masterPlaylistLoader_.sidxMapping_[sidxKey].sidx = sidx; + this.request = requestSidx_( + this, + playlist.sidx, + playlist, + this.vhs_.xhr, + { handleManifestRedirects: this.handleManifestRedirects }, + this.sidxRequestFinished_(playlist, this.masterPlaylistLoader_.master, this.state, (newMaster, sidx) => { + if (!newMaster || !sidx) { + throw new Error('failed to request sidx on minimumUpdatePeriod'); + } - this.updateMinimumUpdatePeriodTimeout_(); + // update loader's sidxMapping with parsed sidx box + this.masterPlaylistLoader_.sidxMapping_[sidxKey].sidx = sidx; - // TODO: do we need to reload the current playlist? - this.refreshMedia_(this.media().id); + addSidxSegmentsToPlaylist(playlist, sidx, playlist.sidx.resolvedUri); - return; - }) - ); - } - } else { - this.updateMainManifest_(updatedMaster); - if (this.media_) { - this.media_ = this.master.playlists[this.media_.id]; - } - } - } + this.updateMinimumUpdatePeriodTimeout_(); + + // TODO: do we need to reload the current playlist? + this.refreshMedia_(this.media().id); + + return; + }) + ); - this.updateMinimumUpdatePeriodTimeout_(); }); } @@ -810,28 +818,21 @@ export default class DashPlaylistLoader extends EventTarget { throw new Error('refreshMedia_ must take a media id'); } - const oldMaster = this.masterPlaylistLoader_.master; - const newMaster = parseMasterXml({ - masterXml: this.masterPlaylistLoader_.masterXml_, - srcUrl: this.masterPlaylistLoader_.srcUrl, - clientOffset: this.masterPlaylistLoader_.clientOffset_, - sidxMapping: this.masterPlaylistLoader_.sidxMapping_ - }); + this.handleMaster_(); - const updatedMaster = updateMaster(oldMaster, newMaster); + const playlists = this.masterPlaylistLoader_.master.playlists; + const mediaChanged = !this.media_ || this.media_ !== playlists[mediaID]; - if (updatedMaster) { - this.masterPlaylistLoader_.master = updatedMaster; - this.media_ = updatedMaster.playlists[mediaID]; + if (mediaChanged) { + this.media_ = playlists[mediaID]; } else { - this.media_ = oldMaster.playlists[mediaID]; this.trigger('playlistunchanged'); } if (!this.media().endList) { this.mediaUpdateTimeout = window.setTimeout(() => { this.trigger('mediaupdatetimeout'); - }, refreshDelay(this.media(), !!updatedMaster)); + }, refreshDelay(this.media(), Boolean(mediaChanged))); } this.trigger('loadedplaylist'); diff --git a/test/dash-playlist-loader.test.js b/test/dash-playlist-loader.test.js index 9551f5d52..f9f85de15 100644 --- a/test/dash-playlist-loader.test.js +++ b/test/dash-playlist-loader.test.js @@ -467,7 +467,6 @@ QUnit.test('compareSidxEntry: will remove non-matching sidxes from a mapping', f QUnit.test('filterChangedSidxMappings: removes change sidx info from mapping', function(assert) { const loader = new DashPlaylistLoader('dash-sidx.mpd', this.fakeVhs); - let masterXml; loader.load(); this.standardXHRResponse(this.requests.shift()); @@ -482,9 +481,7 @@ QUnit.test('filterChangedSidxMappings: removes change sidx info from mapping', f const oldSidxMapping = loader.sidxMapping_; let newSidxMapping = filterChangedSidxMappings( - loader.masterXml_, - loader.srcUrl, - loader.clientOffset_, + loader.master, loader.sidxMapping_ ); @@ -497,12 +494,16 @@ QUnit.test('filterChangedSidxMappings: removes change sidx info from mapping', f const oldVideoKey = generateSidxKey(playlists['0-placeholder-uri-0'].sidx); const oldAudioEnKey = generateSidxKey(playlists['0-placeholder-uri-AUDIO-audio-en'].sidx); + let masterXml = loader.masterXml_.replace(/(indexRange)=\"\d+-\d+\"/, '$1="201-400"'); // should change the video playlist - masterXml = loader.masterXml_.replace(/(indexRange)=\"\d+-\d+\"/, '$1="201-400"'); - newSidxMapping = filterChangedSidxMappings( + let newMaster = parseMasterXml({ masterXml, - loader.srcUrl, - loader.clientOffset_, + srcUrl: loader.srcUrl, + clientOffset: loader.clientOffset_ + }); + + newSidxMapping = filterChangedSidxMappings( + newMaster, loader.sidxMapping_ ); const newVideoKey = `${playlists['0-placeholder-uri-0'].sidx.uri}-201-400`; @@ -521,11 +522,14 @@ QUnit.test('filterChangedSidxMappings: removes change sidx info from mapping', f ); // should change the English audio group - masterXml = masterXml.replace(/(indexRange)=\"\d+-\d+\"/g, '$1="201-400"'); - newSidxMapping = filterChangedSidxMappings( + masterXml = loader.masterXml_.replace(/(indexRange)=\"\d+-\d+\"/g, '$1="201-400"'); + newMaster = parseMasterXml({ masterXml, - loader.srcUrl, - loader.clientOffset_, + srcUrl: loader.srcUrl, + clientOffset: loader.clientOffset_ + }); + newSidxMapping = filterChangedSidxMappings( + newMaster, loader.sidxMapping_ ); assert.notOk(