Skip to content

Commit

Permalink
change customCapabilitiesFilter to accept async function (#4453)
Browse files Browse the repository at this point in the history
* change customCapabilitiesFilter to accept async function
  • Loading branch information
stschr committed Apr 15, 2024
1 parent adc5d4f commit 403f155
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 22 deletions.
117 changes: 117 additions & 0 deletions samples/advanced/custom-capabilities-filter-mca.html
@@ -0,0 +1,117 @@
<!doctype html>
<html lang="en">

<head>
<meta charset="utf-8">
<title>Custom capabilities filter example</title>

<script src="../../dist/dash.all.debug.js"></script>

<!-- Bootstrap core CSS -->
<link href="../lib/bootstrap/bootstrap.min.css" rel="stylesheet">
<link href="../lib/main.css" rel="stylesheet">

<style>
video {
width: 640px;
height: 360px;
}
</style>

<script class="code">
function init() {

var video,
player,
url = "https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd";

var filterCapabilities = async function (representation) {
/*
* This example only illustrates how calls to async functions such as
* MediaCapabilitiesAPI can be used with dash.js' CustomCapabilitiesFilter.
* Note that dash.js already performs basic capability checks internally.
*/
if (navigator.mediaCapabilities) {
if (representation.mimeType === "video/mp4") {
const mimeType = `${representation.mimeType};codecs="${representation.codecs}"`
let cfg = {
type: 'media-source',
video: {
contentType: mimeType,
framerate: representation.frameRate,
width: representation.width,
height: representation.height,
bitrate: representation.bandwidth,
colorGammut: 'srgb',
transferFunction: 'srgb'
}
};
console.log('[sample] Representation '+representation.id+' - Config : %o', cfg);

const ok = await navigator.mediaCapabilities.decodingInfo(cfg).then((result) => result.supported);
console.log('[sample] MediaCapabilities said ' + ok + ' for Representation-id: ' + representation.id + ' and config: ' + JSON.stringify(cfg, null, 4));
return ok;
} else {
console.log('[sample] Representation '+representation.id+' - mimeType ' + representation.mimeType + ' not subject to filtering.');
return true;
}
}
console.log('[sample] MediaCapabilities API not detected; no filtering applied.');
return true;
}

video = document.querySelector("video");
player = dashjs.MediaPlayer().create();

player.registerCustomCapabilitiesFilter(filterCapabilities);

player.initialize(video, url, true);
player.updateSettings({
'debug': {
'logLevel': dashjs.Debug.LOG_LEVEL_INFO
}
});
}
</script>
</head>

<body>

<main>
<div class="container py-4">
<header class="pb-3 mb-4 border-bottom">
<img class="" src="../lib/img/dashjs-logo.png" width="200">
</header>
<div class="row">
<div class="col-md-4">
<div class="h-100 p-5 bg-light border rounded-3">
<h3>Custom capabilities filter example</h3>
<p>This sample shows how to filter representations by defining a custom capabilities filter
function.</p>
</div>
</div>
<div class="col-md-8">
<video controls="true"></video>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div id="code-output"></div>
</div>
</div>
<footer class="pt-3 mt-4 text-muted border-top">
&copy; DASH-IF
</footer>
</div>
</main>


<script>
document.addEventListener('DOMContentLoaded', function () {
init();
});
</script>
<script src="../highlighter.js"></script>
</body>

</html>
11 changes: 11 additions & 0 deletions samples/samples.json
Expand Up @@ -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.",
Expand Down
2 changes: 1 addition & 1 deletion src/streaming/MediaPlayer.js
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/streaming/models/CustomParametersModel.js
Expand Up @@ -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
*/
Expand Down
89 changes: 77 additions & 12 deletions src/streaming/utils/CapabilitiesFilter.js
Expand Up @@ -56,9 +56,9 @@ function CapabilitiesFilter() {
if (settings.get().streaming.capabilities.filterUnsupportedEssentialProperties) {
_filterUnsupportedEssentialProperties(manifest);
}
_applyCustomFilters(manifest);
resolve();
})
.then(() => _applyCustomFilters(manifest))
.then(() => resolve())
.catch(() => {
resolve();
});
Expand Down Expand Up @@ -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 = {
Expand Down
87 changes: 79 additions & 8 deletions test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js
Expand Up @@ -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: [{
Expand Down Expand Up @@ -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',
Expand All @@ -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(() => {
Expand All @@ -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);
});
});
});

});
});
Expand Down

0 comments on commit 403f155

Please sign in to comment.