From 403f1558d03fa20c5f613c782559bd71aeb164a1 Mon Sep 17 00:00:00 2001 From: Stephan Schreiner <92983372+stschr@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:35:56 +0200 Subject: [PATCH] change customCapabilitiesFilter to accept async function (#4453) * change customCapabilitiesFilter to accept async function --- .../custom-capabilities-filter-mca.html | 117 ++++++++++++++++++ samples/samples.json | 11 ++ src/streaming/MediaPlayer.js | 2 +- src/streaming/models/CustomParametersModel.js | 2 +- src/streaming/utils/CapabilitiesFilter.js | 89 +++++++++++-- .../streaming.utils.CapabilitiesFilter.js | 87 +++++++++++-- 6 files changed, 286 insertions(+), 22 deletions(-) create mode 100644 samples/advanced/custom-capabilities-filter-mca.html diff --git a/samples/advanced/custom-capabilities-filter-mca.html b/samples/advanced/custom-capabilities-filter-mca.html new file mode 100644 index 0000000000..ec3f5d1d18 --- /dev/null +++ b/samples/advanced/custom-capabilities-filter-mca.html @@ -0,0 +1,117 @@ + + + + + + Custom capabilities filter example + + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Custom capabilities filter example

+

This sample shows how to filter representations by defining a custom capabilities filter + function.

+
+
+
+ +
+
+
+
+
+
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/samples/samples.json b/samples/samples.json index d4b6de4fc4..099ef40f80 100644 --- a/samples/samples.json +++ b/samples/samples.json @@ -697,6 +697,17 @@ "Audio" ] }, + { + "title": "Custom Capabilities Filter - MediaCapabilitiesAPI", + "description": "This sample shows how to filter representations based on responses from Media Capabilities API.", + "href": "advanced/custom-capabilities-filter-mca.html", + "image": "lib/img/bbb-2.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] + }, { "title": "Custom initial track selection example", "description": "This sample shows how to define your own initial track selection function.", diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js index 12d3fa63e7..1dc73b4800 100644 --- a/src/streaming/MediaPlayer.js +++ b/src/streaming/MediaPlayer.js @@ -1662,7 +1662,7 @@ function MediaPlayer() { */ /** * Registers a custom capabilities filter. This enables application to filter representations to use. - * The provided callback function shall return a boolean based on whether or not to use the representation. + * The provided callback function shall return either a boolean or a promise resolving to a boolean based on whether or not to use the representation. * The filters are applied in the order they are registered. * @param {function} filter - the custom capabilities filter callback * @memberof module:MediaPlayer diff --git a/src/streaming/models/CustomParametersModel.js b/src/streaming/models/CustomParametersModel.js index fd30ff8cdc..13ad18c18d 100644 --- a/src/streaming/models/CustomParametersModel.js +++ b/src/streaming/models/CustomParametersModel.js @@ -164,7 +164,7 @@ function CustomParametersModel() { /** * Registers a custom capabilities filter. This enables application to filter representations to use. - * The provided callback function shall return a boolean based on whether or not to use the representation. + * The provided callback function shall return a boolean or promise resolving to a boolean based on whether or not to use the representation. * The filters are applied in the order they are registered. * @param {function} filter - the custom capabilities filter callback */ diff --git a/src/streaming/utils/CapabilitiesFilter.js b/src/streaming/utils/CapabilitiesFilter.js index 919bb3d8cb..b6906e6df6 100644 --- a/src/streaming/utils/CapabilitiesFilter.js +++ b/src/streaming/utils/CapabilitiesFilter.js @@ -56,9 +56,9 @@ function CapabilitiesFilter() { if (settings.get().streaming.capabilities.filterUnsupportedEssentialProperties) { _filterUnsupportedEssentialProperties(manifest); } - _applyCustomFilters(manifest); - resolve(); }) + .then(() => _applyCustomFilters(manifest)) + .then(() => resolve()) .catch(() => { resolve(); }); @@ -225,25 +225,90 @@ function CapabilitiesFilter() { } function _applyCustomFilters(manifest) { - const customCapabilitiesFilters = customParametersModel.getCustomCapabilitiesFilters(); - if (!customCapabilitiesFilters || customCapabilitiesFilters.length === 0 || !manifest || !manifest.Period || manifest.Period.length === 0) { - return; + if (!manifest || !manifest.Period || manifest.Period.length === 0) { + return Promise.resolve(); } + const promises = []; manifest.Period.forEach((period) => { - period.AdaptationSet = period.AdaptationSet.filter((as) => { + promises.push(_applyCustomFiltersAdaptationSetsOfPeriod(period)); + }); - if (!as.Representation || as.Representation.length === 0) { - return true; - } + return Promise.all(promises); + } + + function _applyCustomFiltersAdaptationSetsOfPeriod(period) { + return new Promise((resolve) => { - as.Representation = as.Representation.filter((representation) => { - return !customCapabilitiesFilters.some(customFilter => !customFilter(representation)); + if (!period || !period.AdaptationSet || period.AdaptationSet.length === 0) { + resolve(); + return; + } + + const promises = []; + period.AdaptationSet.forEach((as) => { + promises.push(_applyCustomFiltersRepresentationsOfAdaptation(as)); + }); + + Promise.all(promises) + .then(() => { + period.AdaptationSet = period.AdaptationSet.filter((as) => { + return as.Representation && as.Representation.length > 0; + }); + resolve(); + }) + .catch(() => { + resolve(); }); + }); - return as.Representation && as.Representation.length > 0; + } + + function _applyCustomFiltersRepresentationsOfAdaptation(as) { + return new Promise((resolve) => { + + if (!as.Representation || as.Representation.length === 0) { + resolve(); + return; + } + + const promises = []; + as.Representation.forEach((rep) => { + promises.push(_applyCustomFiltersRepresentation(rep)); }); + + Promise.all(promises) + .then((supported) => { + as.Representation = as.Representation.filter((rep, i) => { + let isReprSupported = supported[i].every( (s)=>{return s}); + if (!isReprSupported) { + logger.debug('[Stream] Representation '+rep.id+' has been removed because of unsupported CustomFilter'); + } + return isReprSupported; + }); + resolve(); + }) + .catch((err) => { + logger.warn('[Stream] at least one promise rejected in CustomFilter with error: ',err); + resolve(); + }); }); + + } + + function _applyCustomFiltersRepresentation(rep) { + const promises = []; + const customCapabilitiesFilters = customParametersModel.getCustomCapabilitiesFilters(); + + if (!customCapabilitiesFilters || customCapabilitiesFilters.length === 0) { + promises.push(Promise.resolve(true)); + } else { + customCapabilitiesFilters.forEach(customFilter => { + promises.push(new Promise(resolve => resolve(customFilter(rep)))); + }); + } + + return Promise.all(promises) } instance = { diff --git a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js index 0d4e706588..ca1b07a65b 100644 --- a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js +++ b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js @@ -153,11 +153,11 @@ describe('CapabilitiesFilter', function () { describe('filter EssentialProperty values', function () { beforeEach(function () { - settings.update({ streaming: { capabilities: { filterUnsupportedEssentialProperties: true }} }); + settings.update({ streaming: { capabilities: { filterUnsupportedEssentialProperties: true } } }); }); it('should not filter AdaptationSets and Representations if filterUnsupportedEssentialProperties is disabled', function (done) { - settings.update({ streaming: { capabilities: {filterUnsupportedEssentialProperties: false }} }); + settings.update({ streaming: { capabilities: { filterUnsupportedEssentialProperties: false } } }); const manifest = { Period: [{ AdaptationSet: [{ @@ -353,9 +353,20 @@ describe('CapabilitiesFilter', function () { }); describe('custom filters', function () { + let manifest = {}; + + const repHeightFilterFn = function (representation) { + return representation.height >= 720; + }; + const repHeightFilterAsync = function (representation) { + return new Promise(resolve => { resolve(representation.height <= 720) }); + }; + const customFilterRejects = function () { + return Promise.reject('always rejected'); + } - it('should use provided custom filters', function (done) { - const manifest = { + beforeEach(function () { + manifest = { Period: [{ AdaptationSet: [{ mimeType: 'video/mp4', @@ -376,10 +387,24 @@ describe('CapabilitiesFilter', function () { }] }] }; + }); - customParametersModel.registerCustomCapabilitiesFilter(function (representation) { - return representation.height <= 720; - }); + it('should keep manifest unchanged when no custom filter is provided', function (done) { + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(1); + expect(manifest.Period[0].AdaptationSet[0].Representation).to.have.lengthOf(3); + done(); + }) + .catch((e) => { + done(e); + }); + }); + + it('should use provided custom boolean filter', function (done) { + + customParametersModel.registerCustomCapabilitiesFilter(repHeightFilterFn); capabilitiesFilter.filterUnsupportedFeatures(manifest) .then(() => { @@ -390,11 +415,57 @@ describe('CapabilitiesFilter', function () { .catch((e) => { done(e); }); + }); + it('should use provided custom promise filter', function (done) { + customParametersModel.registerCustomCapabilitiesFilter(repHeightFilterAsync); + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(1); + expect(manifest.Period[0].AdaptationSet[0].Representation).to.have.lengthOf(2); + done(); + }) + .catch((e) => { + done(e); + }); }); - }); + it('should use provided custom filters - boolean + promise', function (done) { + + customParametersModel.registerCustomCapabilitiesFilter(repHeightFilterAsync); + customParametersModel.registerCustomCapabilitiesFilter(repHeightFilterFn); + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(1); + expect(manifest.Period[0].AdaptationSet[0].Representation).to.have.lengthOf(1); + done(); + }) + .catch((e) => { + done(e); + }); + }); + + it('should handle rejected promises', function (done) { + + customParametersModel.registerCustomCapabilitiesFilter(repHeightFilterFn); // this function resolves + customParametersModel.registerCustomCapabilitiesFilter(customFilterRejects); // this function rejects + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(1); + // when one promise is rejected, all filters are not applied + expect(manifest.Period[0].AdaptationSet[0].Representation).to.have.lengthOf(3); + + done(); + }) + .catch((e) => { + done(e); + }); + }); + }); }); });