From 5e31f9fb4cf711da9c5b12c4fa1fa50687b3bfd9 Mon Sep 17 00:00:00 2001 From: dsilhavy Date: Sun, 5 Sep 2021 09:29:14 +0200 Subject: [PATCH] Add track selection mode based on selectionPriority attribute (#3751) * Add track selection mode based on selectionPriority attribute in the manifest. Make this the default mode and fallback to TRACK_SELECTION_MODE_HIGHEST_BITRATE and TRACK_SELECTION_MODE_WIDEST_RANGE in case no selectionPriority is given. --- index.d.ts | 2 +- src/core/Settings.js | 9 +- src/dash/DashAdapter.js | 1 + src/dash/models/DashManifestModel.js | 11 ++ src/dash/vo/MediaInfo.js | 1 + src/streaming/constants/Constants.js | 7 + src/streaming/controllers/MediaController.js | 102 +++++++++++--- test/unit/dash.models.DashManifestModel.js | 93 ++++++++----- .../streaming.controllers.MediaController.js | 126 ++++++++++++------ 9 files changed, 257 insertions(+), 95 deletions(-) diff --git a/index.d.ts b/index.d.ts index e68cbcd949..cdd98e398e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1475,7 +1475,7 @@ declare namespace dashjs { export type MetricType = 'ManifestUpdate' | 'RequestsQueue'; export type TrackSwitchMode = 'alwaysReplace' | 'neverReplace'; - export type TrackSelectionMode = 'highestBitrate' | 'firstTrack' | 'highestEfficiency' | 'widestRange'; + export type TrackSelectionMode = 'highestSelectionPriority' | 'highestBitrate' | 'firstTrack' | 'highestEfficiency' | 'widestRange'; export function supportsMediaSource(): boolean; diff --git a/src/core/Settings.js b/src/core/Settings.js index 2b67eca068..b15817c389 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -142,7 +142,7 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; * audio: Constants.TRACK_SWITCH_MODE_ALWAYS_REPLACE, * video: Constants.TRACK_SWITCH_MODE_NEVER_REPLACE * }, - * selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE, + * selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_SELECTION_PRIORITY, * fragmentRequestTimeout: 0, * retryIntervals: { * [HTTPRequest.MPD_TYPE]: 500, @@ -682,8 +682,11 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; * * Possible values * + * - Constants.TRACK_SELECTION_MODE_HIGHEST_SELECTION_PRIORITY + * This mode makes the player select the track with the highest selectionPriority as defined in the manifest. If not selectionPriority is given we fallback to TRACK_SELECTION_MODE_HIGHEST_BITRATE. This mode is a default mode. + * * - Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE - * This mode makes the player select the track with a highest bitrate. This mode is a default mode. + * This mode makes the player select the track with a highest bitrate. * * - Constants.TRACK_SELECTION_MODE_FIRST_TRACK * This mode makes the player select the first track found in the manifest. @@ -825,7 +828,7 @@ function Settings() { audio: Constants.TRACK_SWITCH_MODE_ALWAYS_REPLACE, video: Constants.TRACK_SWITCH_MODE_NEVER_REPLACE }, - selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE, + selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_SELECTION_PRIORITY, fragmentRequestTimeout: 0, retryIntervals: { [HTTPRequest.MPD_TYPE]: 500, diff --git a/src/dash/DashAdapter.js b/src/dash/DashAdapter.js index e359313ad4..17a0c8861b 100644 --- a/src/dash/DashAdapter.js +++ b/src/dash/DashAdapter.js @@ -1023,6 +1023,7 @@ function DashAdapter() { mediaInfo.mimeType = dashManifestModel.getMimeType(realAdaptation); mediaInfo.contentProtection = dashManifestModel.getContentProtectionData(realAdaptation); mediaInfo.bitrateList = dashManifestModel.getBitrateListForAdaptation(realAdaptation); + mediaInfo.selectionPriority = dashManifestModel.getSelectionPriority(realAdaptation); if (mediaInfo.contentProtection) { mediaInfo.contentProtection.forEach(function (item) { diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index 6a636941b8..e93ede9c23 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -406,6 +406,16 @@ function DashManifestModel() { }); } + function getSelectionPriority(realAdaption) { + try { + const priority = realAdaption && typeof realAdaption.selectionPriority !== 'undefined' ? parseInt(realAdaption.selectionPriority) : 1; + + return isNaN(priority) ? 1 : priority; + } catch (e) { + return 1; + } + } + function getEssentialPropertiesForRepresentation(realRepresentation) { if (!realRepresentation || !realRepresentation.EssentialProperty_asArray || !realRepresentation.EssentialProperty_asArray.length) return null; @@ -1169,6 +1179,7 @@ function DashManifestModel() { getRealPeriods, getRealPeriodForIndex, getCodec, + getSelectionPriority, getMimeType, getKID, getLabelsForAdaptation, diff --git a/src/dash/vo/MediaInfo.js b/src/dash/vo/MediaInfo.js index bfc733139c..59cae50fda 100644 --- a/src/dash/vo/MediaInfo.js +++ b/src/dash/vo/MediaInfo.js @@ -52,6 +52,7 @@ class MediaInfo { this.bitrateList = null; this.isFragmented = null; this.isEmbedded = null; + this.selectionPriority = 1; } } diff --git a/src/streaming/constants/Constants.js b/src/streaming/constants/Constants.js index 0aea8749e8..6cc19fa89b 100644 --- a/src/streaming/constants/Constants.js +++ b/src/streaming/constants/Constants.js @@ -248,6 +248,13 @@ class Constants { */ this.TRACK_SELECTION_MODE_WIDEST_RANGE = 'widestRange'; + /** + * @constant {string} TRACK_SELECTION_MODE_WIDEST_RANGE makes the player select the track with the highest selectionPriority as defined in the manifest + * @memberof Constants# + * @static + */ + this.TRACK_SELECTION_MODE_HIGHEST_SELECTION_PRIORITY = 'highestSelectionPriority'; + /** * @constant {string} CMCD_MODE_QUERY specifies to attach CMCD metrics as query parameters. * @memberof Constants# diff --git a/src/streaming/controllers/MediaController.js b/src/streaming/controllers/MediaController.js index d8c0d3aa75..ad0aabfe2f 100644 --- a/src/streaming/controllers/MediaController.js +++ b/src/streaming/controllers/MediaController.js @@ -330,6 +330,28 @@ function MediaController() { }; } + function getTracksWithHighestSelectionPriority(trackArr) { + let max = 0; + let result = []; + + trackArr.forEach((track) => { + if(!isNaN(track.selectionPriority)) { + // Higher max value. Reset list and add new entry + if (track.selectionPriority > max) { + max = track.selectionPriority; + result = [track]; + } + // Same max value add to list + else if (track.selectionPriority === max) { + result.push(track); + } + + } + }) + + return result; + } + function getTracksWithHighestBitrate(trackArr) { let max = 0; let result = []; @@ -402,45 +424,87 @@ function MediaController() { if (type === Constants.TEXT) return tracks[0]; let mode = settings.get().streaming.selectionModeForInitialTrack; - let tmpArr = []; + let tmpArr; if (customInitialTrackSelectionFunction && typeof customInitialTrackSelectionFunction === 'function') { tmpArr = customInitialTrackSelectionFunction(tracks); } else { switch (mode) { + case Constants.TRACK_SELECTION_MODE_HIGHEST_SELECTION_PRIORITY: + tmpArr = _trackSelectionModeHighestSelectionPriority(tracks); + break; case Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE: - tmpArr = getTracksWithHighestBitrate(tracks); - - if (tmpArr.length > 1) { - tmpArr = getTracksWithWidestRange(tmpArr); - } + tmpArr = _trackSelectionModeHighestBitrate(tracks); break; case Constants.TRACK_SELECTION_MODE_FIRST_TRACK: - tmpArr.push(tracks[0]); + tmpArr = _trackSelectionModeFirstTrack(tracks); break; case Constants.TRACK_SELECTION_MODE_HIGHEST_EFFICIENCY: - tmpArr = getTracksWithHighestEfficiency(tracks); - - if (tmpArr.length > 1) { - tmpArr = getTracksWithHighestBitrate(tmpArr); - } + tmpArr = _trackSelectionModeHighestEfficiency(tracks); break; case Constants.TRACK_SELECTION_MODE_WIDEST_RANGE: - tmpArr = getTracksWithWidestRange(tracks); - - if (tmpArr.length > 1) { - tmpArr = getTracksWithHighestBitrate(tracks); - } + tmpArr = _trackSelectionModeWidestRange(tracks); break; default: - logger.warn('Track selection mode is not supported: ' + mode); + logger.warn(`Track selection mode ${mode} is not supported. Falling back to TRACK_SELECTION_MODE_FIRST_TRACK`); + tmpArr = _trackSelectionModeFirstTrack(tracks); break; } } - return tmpArr[0]; + return tmpArr.length > 0 ? tmpArr[0] : tracks[0]; } + + function _trackSelectionModeHighestSelectionPriority(tracks) { + let tmpArr = getTracksWithHighestSelectionPriority(tracks); + + if (tmpArr.length > 1) { + tmpArr = getTracksWithHighestBitrate(tmpArr); + } + + if (tmpArr.length > 1) { + tmpArr = getTracksWithWidestRange(tmpArr); + } + + return tmpArr; + } + + function _trackSelectionModeHighestBitrate(tracks) { + let tmpArr = getTracksWithHighestBitrate(tracks); + + if (tmpArr.length > 1) { + tmpArr = getTracksWithWidestRange(tmpArr); + } + + return tmpArr; + } + + function _trackSelectionModeFirstTrack(tracks) { + return tracks[0]; + } + + function _trackSelectionModeHighestEfficiency(tracks) { + let tmpArr = getTracksWithHighestEfficiency(tracks); + + if (tmpArr.length > 1) { + tmpArr = getTracksWithHighestBitrate(tmpArr); + } + + return tmpArr; + } + + function _trackSelectionModeWidestRange(tracks) { + let tmpArr = getTracksWithWidestRange(tracks); + + if (tmpArr.length > 1) { + tmpArr = getTracksWithHighestBitrate(tracks); + } + + return tmpArr; + } + + function createTrackInfo() { return { audio: { diff --git a/test/unit/dash.models.DashManifestModel.js b/test/unit/dash.models.DashManifestModel.js index 27092a5c2f..029291fcb7 100644 --- a/test/unit/dash.models.DashManifestModel.js +++ b/test/unit/dash.models.DashManifestModel.js @@ -82,7 +82,7 @@ describe('DashManifestModel', function () { }); it('should return 5 when getSuggestedPresentationDelay is called and mpd is an object with suggestedPresentationDelay attribute', () => { - const suggestedPresentationDelay = dashManifestModel.getSuggestedPresentationDelay({suggestedPresentationDelay: 5}); + const suggestedPresentationDelay = dashManifestModel.getSuggestedPresentationDelay({ suggestedPresentationDelay: 5 }); expect(suggestedPresentationDelay).to.be.equal(5); // jshint ignore:line }); @@ -101,7 +101,7 @@ describe('DashManifestModel', function () { it('should return correct value when getAvailabilityStartTime is called and mpd is object with the availabilityStartTime attribute', () => { const now = new Date(); - const availabilityStartTime = dashManifestModel.getAvailabilityStartTime({availabilityStartTime: now}); + const availabilityStartTime = dashManifestModel.getAvailabilityStartTime({ availabilityStartTime: now }); expect(availabilityStartTime).to.be.equal(now.getTime()); // jshint ignore:line }); @@ -146,28 +146,28 @@ describe('DashManifestModel', function () { }); it('should return null when getAdaptationForId is called and id and periodIndex are undefined', () => { - const manifest = {Period_asArray: []}; + const manifest = { Period_asArray: [] }; const adaptation = dashManifestModel.getAdaptationForId(undefined, manifest, undefined); expect(adaptation).to.be.null; // jshint ignore:line }); it('should return null when getAdaptationForId is called and id is undefined', () => { - const manifest = {Period_asArray: []}; + const manifest = { Period_asArray: [] }; const adaptation = dashManifestModel.getAdaptationForId(undefined, manifest, 2); expect(adaptation).to.be.null; // jshint ignore:line }); it('should return null when getAdaptationForId is called and id is undefined and periodIndex = 0', () => { - const manifest = {Period_asArray: [{AdaptationSet_asArray: [{id: 0}]}]}; + const manifest = { Period_asArray: [{ AdaptationSet_asArray: [{ id: 0 }] }] }; const adaptation = dashManifestModel.getAdaptationForId(undefined, manifest, 0); expect(adaptation).to.be.null; // jshint ignore:line }); it('should return valid value when getAdaptationForId is called and id is 0 and periodIndex = 0', () => { - const manifest = {Period_asArray: [{AdaptationSet_asArray: [{id: 0}]}]}; + const manifest = { Period_asArray: [{ AdaptationSet_asArray: [{ id: 0 }] }] }; const adaptation = dashManifestModel.getAdaptationForId(0, manifest, 0); expect(adaptation.id).to.equal(0); // jshint ignore:line @@ -180,28 +180,28 @@ describe('DashManifestModel', function () { }); it('should return null when getAdaptationForIndex is called and id and periodIndex are undefined', () => { - const manifest = {Period_asArray: []}; + const manifest = { Period_asArray: [] }; const adaptation = dashManifestModel.getAdaptationForIndex(undefined, manifest, undefined); expect(adaptation).to.be.null; // jshint ignore:line }); it('should return null when getAdaptationForIndex is called and id is undefined', () => { - const manifest = {Period_asArray: []}; + const manifest = { Period_asArray: [] }; const adaptation = dashManifestModel.getAdaptationForIndex(undefined, manifest, 2); expect(adaptation).to.be.null; // jshint ignore:line }); it('should return null when getAdaptationForIndex is called and id is undefined and periodIndex = 0', () => { - const manifest = {Period_asArray: [{AdaptationSet_asArray: [{id: 0}]}]}; + const manifest = { Period_asArray: [{ AdaptationSet_asArray: [{ id: 0 }] }] }; const adaptation = dashManifestModel.getAdaptationForIndex(undefined, manifest, 0); expect(adaptation).to.be.null; // jshint ignore:line }); it('should return valid value when getAdaptationForIndex is called and id is 0 and periodIndex = 0', () => { - const manifest = {Period_asArray: [{AdaptationSet_asArray: [{id: 0}]}]}; + const manifest = { Period_asArray: [{ AdaptationSet_asArray: [{ id: 0 }] }] }; const adaptation = dashManifestModel.getAdaptationForIndex(0, manifest, 0); expect(adaptation.id).to.equal(0); // jshint ignore:line @@ -214,7 +214,7 @@ describe('DashManifestModel', function () { }); it('should return -1 when getIndexForAdaptation is called and manifest and periodIndex are undefined', () => { - const manifest = {Period_asArray: []}; + const manifest = { Period_asArray: [] }; var adaptation = mpdHelper.composeAdaptation('video'); const index = dashManifestModel.getIndexForAdaptation(adaptation, manifest, undefined); @@ -236,7 +236,7 @@ describe('DashManifestModel', function () { }); it('should return an empty array when getAdaptationsForType is called and periodIndex and type are undefined', () => { - const manifest = {Period_asArray: []}; + const manifest = { Period_asArray: [] }; const adaptationsArray = dashManifestModel.getAdaptationsForType(manifest, undefined, undefined); expect(adaptationsArray).to.be.instanceOf(Array); // jshint ignore:line @@ -244,7 +244,7 @@ describe('DashManifestModel', function () { }); it('should return an empty array when getAdaptationsForType is called and type is undefined', () => { - const manifest = {Period_asArray: [{AdaptationSet_asArray: [{id: 0}]}]}; + const manifest = { Period_asArray: [{ AdaptationSet_asArray: [{ id: 0 }] }] }; expect(dashManifestModel.getAdaptationsForType.bind(dashManifestModel, manifest, 0, undefined)).to.throw('type is not defined'); }); @@ -262,13 +262,13 @@ describe('DashManifestModel', function () { }); it('should return null when getCodec is called and adaptation.Representation_asArray.length is -1', () => { - const codec = dashManifestModel.getCodec({Representation_asArray: {length: -1}}); + const codec = dashManifestModel.getCodec({ Representation_asArray: { length: -1 } }); expect(codec).to.be.null; // jshint ignore:line }); it('should return null when getCodec is called and representationId is not an integer', () => { - const codec = dashManifestModel.getCodec({Representation_asArray: {length: 1}}, true); + const codec = dashManifestModel.getCodec({ Representation_asArray: { length: 1 } }, true); expect(codec).to.be.null; // jshint ignore:line }); @@ -338,7 +338,7 @@ describe('DashManifestModel', function () { }); it('should return null when getMimeType is called and adaptation.Representation_asArray.length is -1', () => { - const mimeType = dashManifestModel.getMimeType({Representation_asArray: {length: -1}}); + const mimeType = dashManifestModel.getMimeType({ Representation_asArray: { length: -1 } }); expect(mimeType).to.be.null; // jshint ignore:line }); @@ -350,7 +350,7 @@ describe('DashManifestModel', function () { }); it('should return kid value when getKID is called and adaptation is well defined', () => { - const kid = dashManifestModel.getKID({'cenc:default_KID': 'testKid'}); + const kid = dashManifestModel.getKID({ 'cenc:default_KID': 'testKid' }); expect(kid).to.equal('testKid'); // jshint ignore:line }); @@ -370,14 +370,14 @@ describe('DashManifestModel', function () { }); it('should return empty array when getLabelsForAdaptation is called and adaptation is not well defined', () => { - const labels = dashManifestModel.getLabelsForAdaptation({Label_asArray: true}); + const labels = dashManifestModel.getLabelsForAdaptation({ Label_asArray: true }); expect(labels).to.be.instanceOf(Array); // jshint ignore:line expect(labels).to.be.empty; // jshint ignore:line }); it('should return empty array when getLabelsForAdaptation is called and adaptation is well defined with an empty Label_asArray', () => { - const labels = dashManifestModel.getLabelsForAdaptation({Label_asArray: []}); + const labels = dashManifestModel.getLabelsForAdaptation({ Label_asArray: [] }); expect(labels).to.be.instanceOf(Array); // jshint ignore:line expect(labels).to.be.empty; // jshint ignore:line @@ -388,7 +388,7 @@ describe('DashManifestModel', function () { Label_asArray: [{ lang: 'fre', __text: 'french' - }, {lang: 'eng', __text: 'english'}] + }, { lang: 'eng', __text: 'english' }] }); expect(labels).to.be.instanceOf(Array); // jshint ignore:line @@ -403,7 +403,7 @@ describe('DashManifestModel', function () { }); it('should return null when getContentProtectionData is called and adaptation is defined, but ContentProtection_asArray is an empty array', () => { - const adaptation = {ContentProtection_asArray: []}; + const adaptation = { ContentProtection_asArray: [] }; const contentProtection = dashManifestModel.getContentProtectionData(adaptation); expect(contentProtection).to.be.null; // jshint ignore:line @@ -422,13 +422,13 @@ describe('DashManifestModel', function () { }); it('should return duration when getDuration is called and manifest has a defined mediaPresentationDuration', () => { - const duration = dashManifestModel.getDuration({mediaPresentationDuration: 50}); + const duration = dashManifestModel.getDuration({ mediaPresentationDuration: 50 }); expect(duration).to.equal(50); // jshint ignore:line }); it('should return infinity when getDuration is called and manifest is a dynamic one', () => { - const duration = dashManifestModel.getDuration({type: DashConstants.DYNAMIC}); + const duration = dashManifestModel.getDuration({ type: DashConstants.DYNAMIC }); expect(duration).to.equal(Infinity); // jshint ignore:line }); @@ -446,7 +446,7 @@ describe('DashManifestModel', function () { }); it('should return correct value when getBandwidth is called and representation is defined', () => { - const bdtw = dashManifestModel.getBandwidth({bandwidth: 9600}); + const bdtw = dashManifestModel.getBandwidth({ bandwidth: 9600 }); expect(bdtw).to.equal(9600); // jshint ignore:line }); @@ -459,7 +459,7 @@ describe('DashManifestModel', function () { }); it('should not return empty array when getBitrateListForAdaptation is called and adaptation is defined', () => { - const realAdaptation = {Representation_asArray: [{}]}; + const realAdaptation = { Representation_asArray: [{}] }; const bitrateList = dashManifestModel.getBitrateListForAdaptation(realAdaptation); @@ -507,7 +507,7 @@ describe('DashManifestModel', function () { }); it('should return valid location when getLocation is called and manifest is a valid object', () => { - const location = dashManifestModel.getLocation({Location: '', Location_asArray: ['location_1']}); + const location = dashManifestModel.getLocation({ Location: '', Location_asArray: ['location_1'] }); expect(location).to.be.equal('location_1'); // jshint ignore:line }); @@ -558,26 +558,26 @@ describe('DashManifestModel', function () { Period: [ { 'id': '153199', - AdaptationSet: [{Representation: [{InbandEventStream: []}]}] + AdaptationSet: [{ Representation: [{ InbandEventStream: [] }] }] }, { 'id': '153202', - AdaptationSet: [{Representation: [{InbandEventStream: []}]}] + AdaptationSet: [{ Representation: [{ InbandEventStream: [] }] }] } ], Period_asArray: [ { 'id': '153199', - AdaptationSet_asArray: [{Representation_asArray: [{InbandEventStream_asArray: []}]}] + AdaptationSet_asArray: [{ Representation_asArray: [{ InbandEventStream_asArray: [] }] }] }, { 'id': '153202', - AdaptationSet_asArray: [{Representation_asArray: [{InbandEventStream_asArray: []}]}] + AdaptationSet_asArray: [{ Representation_asArray: [{ InbandEventStream_asArray: [] }] }] } ], 'type': 'static' }; - const representation = {adaptation: {index: 0, period: {index: 0}}, index: 0}; + const representation = { adaptation: { index: 0, period: { index: 0 } }, index: 0 }; const eventsStream = dashManifestModel.getEventStreamForRepresentation(manifest, representation); expect(eventsStream).to.be.instanceOf(Array); // jshint ignore:line @@ -902,7 +902,7 @@ describe('DashManifestModel', function () { it('should return valid value when minimumUpdatePeriod is present in manifest and latencyOfLastUpdate is defined', () => { const minimumUpdatePeriod = 30; const latencyOfLastUpdate = 0.5; - const manifest = {minimumUpdatePeriod: minimumUpdatePeriod}; + const manifest = { minimumUpdatePeriod: minimumUpdatePeriod }; const expectedResult = minimumUpdatePeriod - latencyOfLastUpdate; const updatePeriod = dashManifestModel.getManifestUpdatePeriod(manifest, latencyOfLastUpdate); expect(updatePeriod).to.equal(expectedResult); // jshint ignore:line @@ -910,7 +910,7 @@ describe('DashManifestModel', function () { it('should return valid value when minimumUpdatePeriod is present in manifest and latencyOfLastUpdate is not defined', () => { const minimumUpdatePeriod = 30; - const manifest = {minimumUpdatePeriod: minimumUpdatePeriod}; + const manifest = { minimumUpdatePeriod: minimumUpdatePeriod }; const expectedResult = minimumUpdatePeriod; const updatePeriod = dashManifestModel.getManifestUpdatePeriod(manifest); expect(updatePeriod).to.equal(expectedResult); // jshint ignore:line @@ -1106,5 +1106,32 @@ describe('DashManifestModel', function () { expect(obj[0].url).to.equal(TEST_URL); // jshint ignore:line }); }); + + describe('getSelectionPriority', () => { + + it('should return 1 when adaptation is not defined', () => { + const priority = dashManifestModel.getSelectionPriority(); + + expect(priority).to.equal(1); + }) + + it('should return 1 when adaptation does not have field selectionPriority', () => { + const priority = dashManifestModel.getSelectionPriority({}); + + expect(priority).to.equal(1); + }) + + it('should return 1 when selectionPriority is not a number', () => { + const priority = dashManifestModel.getSelectionPriority({ selectionPriority: 'xy' }); + + expect(priority).to.equal(1); + }) + + it('should return valid selectionPriority', () => { + const priority = dashManifestModel.getSelectionPriority({ selectionPriority: '5' }); + + expect(priority).to.equal(5); + }) + }) }); }); diff --git a/test/unit/streaming.controllers.MediaController.js b/test/unit/streaming.controllers.MediaController.js index 30fd111cee..dc36d2bd28 100644 --- a/test/unit/streaming.controllers.MediaController.js +++ b/test/unit/streaming.controllers.MediaController.js @@ -169,7 +169,7 @@ describe('MediaController', function () { }); it('getTracksFor should return an empty array if parameters are defined, but internal tracks array is empty', function () { - const trackArray = mediaController.getTracksFor(Constants.VIDEO, {id: 'id'}); + const trackArray = mediaController.getTracksFor(Constants.VIDEO, { id: 'id' }); expect(trackArray).to.be.instanceOf(Array); // jshint ignore:line expect(trackArray).to.be.empty; // jshint ignore:line @@ -450,140 +450,187 @@ describe('MediaController', function () { describe('Initial Track Selection', function () { - function testSelectInitialTrack(type, expectedBitrateList, otherBitrateList) { - const tracks = [ expectedBitrateList, otherBitrateList ].map(function (bitrateList) { + function testSelectInitialTrack(type, expectedTrack, otherTrack) { + const tracks = [expectedTrack, otherTrack].map(function (track) { return { - bitrateList: bitrateList, - representationCount: bitrateList.length + bitrateList: track.bitrateList, + representationCount: track.bitrateList.length, + selectionPriority: !isNaN(track.selectionPriority) ? track.selectionPriority : 1 }; }); const selection = mediaController.selectInitialTrack(type, tracks); - expect(objectUtils.areEqual(selection.bitrateList, expectedBitrateList)).to.be.true; // jshint ignore:line + expect(objectUtils.areEqual(selection.bitrateList, expectedTrack.bitrateList)).to.be.true; // jshint ignore:line } + describe('"highestSelectionPriority" mode', function () { + beforeEach(function () { + settings.update({ streaming: { selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_SELECTION_PRIORITY } }); + }); + + it('should select track with highest priority', function () { + testSelectInitialTrack( + 'video', + { bitrateList: [{ bandwidth: 1000 }], selectionPriority: 2 }, + { bitrateList: [{ bandwidth: 2000 }], selectionPriority: 1 } + ); + }); + + it('should select track with highest bitrate if both tracks have same priority', function () { + testSelectInitialTrack( + 'video', + { bitrateList: [{ bandwidth: 1000 }, { bandwidth: 3000 }], selectionPriority: 1 }, + { bitrateList: [{ bandwidth: 2000 }], selectionPriority: 1 } + ); + }); + + it('should select track with highest bitrate if no priority is given', function () { + testSelectInitialTrack( + 'video', + { bitrateList: [{ bandwidth: 1000 }, { bandwidth: 3000 }] }, + { bitrateList: [{ bandwidth: 2000 }] } + ); + }); + + it('should tie break using "widestRange"', function () { + testSelectInitialTrack( + 'video', + { bitrateList: [{ bandwidth: 2000 }, { bandwidth: 1000 }] }, + { bitrateList: [{ bandwidth: 2000 }] } + ); + }); + + }); + describe('"highestBitrate" mode', function () { beforeEach(function () { - settings.update({ streaming: { selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE }}); + settings.update({ streaming: { selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE } }); }); it('should select track with highest bitrate', function () { testSelectInitialTrack( 'video', - [ { bandwidth: 2000 } ], - [ { bandwidth: 1000 } ] + { bitrateList: [{ bandwidth: 2000 }] }, + { bitrateList: [{ bandwidth: 1000 }] } ); }); it('should tie break using "widestRange"', function () { testSelectInitialTrack( 'video', - [ { bandwidth: 2000 }, { bandwidth: 1000 } ], - [ { bandwidth: 2000 } ] + { bitrateList: [{ bandwidth: 2000 }, { bandwidth: 1000 }] }, + { bitrateList: [{ bandwidth: 2000 }] } ); }); it('should select track with highest bitrate, expected list only one entry"', function () { testSelectInitialTrack( 'video', - [ { bandwidth: 2100 } ], - [ { bandwidth: 2000 }, { bandwidth: 1000 } ] + { bitrateList: [{ bandwidth: 2100 }] }, + { bitrateList: [{ bandwidth: 2000 }, { bandwidth: 1000 }] }, ); }); }); describe('"firstTrack" mode', function () { beforeEach(function () { - settings.update({ streaming: { selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_FIRST_TRACK }}); + settings.update({ streaming: { selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_FIRST_TRACK } }); }); it('should select first track', function () { testSelectInitialTrack( 'video', - [ { bandwidth: 1000 } ], - [ { bandwidth: 2000 } ] + { bitrateList: [{ bandwidth: 1000 }] }, + { bitrateList: [{ bandwidth: 2000 }] } ); }); it('should select first track, other bitrate list more than one entry"', function () { testSelectInitialTrack( 'video', - [ { bandwidth: 2000 }], - [ { bandwidth: 3000 }, { bandwidth: 1000 } ] + { bitrateList: [{ bandwidth: 2000 }] }, + { bitrateList: [{ bandwidth: 3000 }, { bandwidth: 1000 }] } ); }); it('should select first track, expected bitrate list more than one entry"', function () { testSelectInitialTrack( 'video', - [{ bandwidth: 3000 }, { bandwidth: 1000 }], - [ { bandwidth: 2000 } ] + { bitrateList: [{ bandwidth: 3000 }, { bandwidth: 1000 }] }, + { bitrateList: [{ bandwidth: 2000 }] } ); }); }); describe('"highestEfficiency" mode', function () { beforeEach(function () { - settings.update({ streaming: { selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_EFFICIENCY }}); + settings.update({ streaming: { selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_EFFICIENCY } }); }); it('should select video track with lowest bitrate among equal resolutions', function () { testSelectInitialTrack( 'video', - [ { bandwidth: 1000, width: 1920, height: 1280 } ], - [ { bandwidth: 2000, width: 1920, height: 1280 } ] + { bitrateList: [{ bandwidth: 1000, width: 1920, height: 1280 }] }, + { bitrateList: [{ bandwidth: 2000, width: 1920, height: 1280 }] } ); }); it('should select video track with lowest bitrate among different resolutions', function () { testSelectInitialTrack( 'video', - [ { bandwidth: 1000, width: 1920, height: 1280 } ], - [ { bandwidth: 1000, width: 1080, height: 720 } ] + { bitrateList: [{ bandwidth: 1000, width: 1920, height: 1280 }] }, + { bitrateList: [{ bandwidth: 1000, width: 1280, height: 720 }] } ); }); it('should select audio track with lowest avg bitrate', function () { testSelectInitialTrack( 'audio', - [ { bandwidth: 1000, width: 0, height: 0 } ], - [ { bandwidth: 2000, width: 0, height: 0 } ] + { bitrateList: [{ bandwidth: 1000, width: 0, height: 0 }] }, + { bitrateList: [{ bandwidth: 2000, width: 0, height: 0 }] } ); }); it('should tie break using "highestBitrate"', function () { testSelectInitialTrack( 'video', - [ { bandwidth: 1500, width: 1920, height: 1280 }, { bandwidth: 1000, width: 1080, height: 720 } ], - [ { bandwidth: 1000, width: 1080, height: 720 } ] + { + bitrateList: [{ bandwidth: 1500, width: 1920, height: 1280 }, { + bandwidth: 1000, + width: 1080, + height: 720 + }] + }, + { bitrateList: [{ bandwidth: 1000, width: 1080, height: 720 }] } ); }); }); describe('"widestRange" mode', function () { beforeEach(function () { - settings.update({ streaming: { selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_WIDEST_RANGE }}); + settings.update({ streaming: { selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_WIDEST_RANGE } }); }); it('should select track with most bitrates', function () { testSelectInitialTrack( 'video', - [ { bandwidth: 2000 }, { bandwidth: 1000 } ], - [ { bandwidth: 2000 } ] + { bitrateList: [{ bandwidth: 2000 }, { bandwidth: 1000 }] }, + { bitrateList: [{ bandwidth: 2000 }] } ); }); it('should tie break using "highestBitrate"', function () { testSelectInitialTrack( 'video', - [ { bandwidth: 3000 }, { bandwidth: 2000 } ], - [ { bandwidth: 2000 }, { bandwidth: 1000 } ] + { bitrateList: [{ bandwidth: 3000 }, { bandwidth: 2000 }] }, + { bitrateList: [{ bandwidth: 2000 }, { bandwidth: 1000 }] } ); }); }); - describe('custom initial track selection function', function() { - beforeEach(function(){ - settings.update({ streaming: { selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE }}); + describe('custom initial track selection function', function () { + beforeEach(function () { + settings.update({ streaming: { selectionModeForInitialTrack: Constants.TRACK_SELECTION_MODE_HIGHEST_BITRATE } }); + function getTrackWithLowestBitrate(trackArr) { let min = Infinity; let result = []; @@ -602,14 +649,15 @@ describe('MediaController', function () { return result; } + mediaController.setCustomInitialTrackSelectionFunction(getTrackWithLowestBitrate); }); it('should return the track with the lowest bitrate', function () { testSelectInitialTrack( 'video', - [ { bandwidth: 1000 }, { bandwidth: 5000 }], - [ { bandwidth: 2000 }, { bandwidth: 8000 }] + { bitrateList: [{ bandwidth: 1000 }, { bandwidth: 5000 }] }, + { bitrateList: [{ bandwidth: 2000 }, { bandwidth: 8000 }] } ) });