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);
+ });
+ });
+ });
});
});