diff --git a/index.d.ts b/index.d.ts index b83829a62f..ccd090d8ff 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1281,7 +1281,8 @@ declare namespace dashjs { rtpSafetyFactor?: number, mode?: 'query' | 'header', enabledKeys?: Array, - includeInRequests?: Array + includeInRequests?: Array, + version?: number }, cmsd?: { enabled?: boolean, diff --git a/src/core/Settings.js b/src/core/Settings.js index 9a75f9ea35..93007303a5 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -308,7 +308,8 @@ import Events from './events/Events.js'; * rtpSafetyFactor: 5, * mode: Constants.CMCD_MODE_QUERY, * enabledKeys: ['br', 'd', 'ot', 'tb' , 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su' , 'bs', 'rtp' , 'cid', 'pr', 'sf', 'sid', 'st', 'v'] - * includeInRequests: ['segment', 'mpd'] + * includeInRequests: ['segment', 'mpd'], + * version: 1 * }, * cmsd: { * enabled: false, @@ -882,6 +883,10 @@ import Events from './events/Events.js'; * Specifies which HTTP GET requests shall carry parameters. * * If not specified this value defaults to ['segment']. + * @property {number} [version=1] + * The version of the CMCD to use. + * + * If not specified this value defaults to 1. */ /** @@ -1325,7 +1330,8 @@ function Settings() { rtpSafetyFactor: 5, mode: Constants.CMCD_MODE_QUERY, enabledKeys: Constants.CMCD_AVAILABLE_KEYS, - includeInRequests: ['segment', 'mpd'] + includeInRequests: ['segment', 'mpd'], + version: 1 }, cmsd: { enabled: false, diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js index e212ce0cc6..d60219305b 100644 --- a/src/streaming/MediaPlayer.js +++ b/src/streaming/MediaPlayer.js @@ -2482,7 +2482,7 @@ function MediaPlayer() { textController.initialize(); gapController.initialize(); catchupController.initialize(); - cmcdModel.initialize(); + cmcdModel.initialize(autoPlay); cmsdModel.initialize(); contentSteeringController.initialize(); segmentBaseController.initialize(); diff --git a/src/streaming/constants/Constants.js b/src/streaming/constants/Constants.js index a95bc2d6c0..87e1d8d8df 100644 --- a/src/streaming/constants/Constants.js +++ b/src/streaming/constants/Constants.js @@ -216,14 +216,20 @@ export default { CMCD_MODE_HEADER: 'header', /** - * @constant {string} CMCD_AVAILABLE_KEYS specifies all the availables keys for CMCD metrics. + * @constant {string} CMCD_AVAILABLE_KEYS specifies all the available keys for CMCD metrics. * @memberof Constants# * @static */ CMCD_AVAILABLE_KEYS: ['br', 'd', 'ot', 'tb', 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su', 'bs', 'rtp', 'cid', 'pr', 'sf', 'sid', 'st', 'v'], + /** + * @constant {string} CMCD_AVAILABLE_KEYS_V2 specifies all the available keys for CMCD version 2 metrics. + * @memberof Constants# + * @static + */ + CMCD_V2_AVAILABLE_KEYS: ['msd', 'ltc'], /** - * @constant {string} CMCD_AVAILABLE_REQUESTS specifies all the availables requests type for CMCD metrics. + * @constant {string} CMCD_AVAILABLE_REQUESTS specifies all the available requests type for CMCD metrics. * @memberof Constants# * @static */ diff --git a/src/streaming/models/CmcdModel.js b/src/streaming/models/CmcdModel.js index c689097671..ada7412eaa 100644 --- a/src/streaming/models/CmcdModel.js +++ b/src/streaming/models/CmcdModel.js @@ -44,8 +44,8 @@ import {CmcdStreamType} from '@svta/common-media-library/cmcd/CmcdStreamType'; import {CmcdStreamingFormat} from '@svta/common-media-library/cmcd/CmcdStreamingFormat'; import {encodeCmcd} from '@svta/common-media-library/cmcd/encodeCmcd'; import {toCmcdHeaders} from '@svta/common-media-library/cmcd/toCmcdHeaders'; - -const CMCD_VERSION = 1; +import {CmcdHeaderField} from '@svta/common-media-library/cmcd/CmcdHeaderField'; +const DEFAULT_CMCD_VERSION = 1; const DEFAULT_INCLUDE_IN_REQUESTS = 'segment'; const RTP_SAFETY_FACTOR = 5; @@ -64,7 +64,9 @@ function CmcdModel() { _lastMediaTypeRequest, _isStartup, _bufferLevelStarved, - _initialMediaRequestsDone; + _initialMediaRequestsDone, + _playbackStartedTime, + _msdSent; let context = this.context; let eventBus = EventBus(context).getInstance(); @@ -77,12 +79,19 @@ function CmcdModel() { _resetInitialSettings(); } - function initialize() { + function initialize(autoPlay) { eventBus.on(MediaPlayerEvents.PLAYBACK_RATE_CHANGED, _onPlaybackRateChanged, instance); eventBus.on(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, instance); eventBus.on(MediaPlayerEvents.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, instance); eventBus.on(MediaPlayerEvents.PLAYBACK_SEEKED, _onPlaybackSeeked, instance); eventBus.on(MediaPlayerEvents.PERIOD_SWITCH_COMPLETED, _onPeriodSwitchComplete, instance); + if (autoPlay) { + eventBus.on(MediaPlayerEvents.MANIFEST_LOADING_STARTED, _onPlaybackStarted, instance); + } + else { + eventBus.on(MediaPlayerEvents.PLAYBACK_STARTED, _onPlaybackStarted, instance); + } + eventBus.on(MediaPlayerEvents.PLAYBACK_PLAYING, _onPlaybackPlaying, instance); } function setConfig(config) { @@ -124,6 +133,8 @@ function CmcdModel() { _isStartup = {}; _initialMediaRequestsDone = {}; _lastMediaTypeRequest = undefined; + _playbackStartedTime = undefined; + _msdSent = false; _updateStreamProcessors(); } @@ -131,6 +142,20 @@ function CmcdModel() { _updateStreamProcessors(); } + function _onPlaybackStarted() { + if (!_playbackStartedTime) { + _playbackStartedTime = Date.now(); + } + } + + function _onPlaybackPlaying() { + if (!_playbackStartedTime || internalData.msd) { + return; + } + + internalData.msd = Date.now() - _playbackStartedTime; + } + function _updateStreamProcessors() { if (!playbackController) { return; @@ -195,7 +220,8 @@ function CmcdModel() { if (isCmcdEnabled()) { const cmcdData = getCmcdData(request); const filteredCmcdData = _applyWhitelist(cmcdData); - const headers = toCmcdHeaders(filteredCmcdData) + const options = _createCmcdV2HeadersCustomMap(); + const headers = toCmcdHeaders(filteredCmcdData, options); eventBus.trigger(MetricsReportingEvents.CMCD_DATA_GENERATED, { url: request.url, @@ -219,8 +245,8 @@ function CmcdModel() { function _canBeEnabled(cmcdParametersFromManifest) { if (Object.keys(cmcdParametersFromManifest).length) { - if (!cmcdParametersFromManifest.version) { - logger.error(`version parameter must be defined.`); + if (parseInt(cmcdParametersFromManifest.version) !== 1) { + logger.error(`version parameter must be defined in 1.`); return false; } if (!cmcdParametersFromManifest.keys) { @@ -257,15 +283,17 @@ function CmcdModel() { function _checkAvailableKeys(cmcdParametersFromManifest) { const defaultAvailableKeys = Constants.CMCD_AVAILABLE_KEYS; + const defaultV2AvailableKeys = Constants.CMCD_V2_AVAILABLE_KEYS; const enabledCMCDKeys = cmcdParametersFromManifest.version ? cmcdParametersFromManifest.keys : settings.get().streaming.cmcd.enabledKeys; - const invalidKeys = enabledCMCDKeys.filter(k => !defaultAvailableKeys.includes(k)); + const cmcdVersion = settings.get().streaming.cmcd.version; + const invalidKeys = enabledCMCDKeys.filter(k => !defaultAvailableKeys.includes(k) && !(cmcdVersion === 2 && defaultV2AvailableKeys.includes(k))); if (invalidKeys.length === enabledCMCDKeys.length && enabledCMCDKeys.length > 0) { - logger.error(`None of the keys are implemented.`); + logger.error(`None of the keys are implemented for CMCD version ${cmcdVersion}.`); return false; } invalidKeys.map((k) => { - logger.warn(`key parameter ${k} is not implemented.`); + logger.warn(`key parameter ${k} is not implemented for CMCD version ${cmcdVersion}.`); }); return true; @@ -497,7 +525,7 @@ function CmcdModel() { let cid = settings.get().streaming.cmcd.cid ? settings.get().streaming.cmcd.cid : internalData.cid; cid = cmcdParametersFromManifest.contentID ? cmcdParametersFromManifest.contentID : cid; - data.v = CMCD_VERSION; + data.v = settings.get().streaming.cmcd.version ?? DEFAULT_CMCD_VERSION; data.sid = settings.get().streaming.cmcd.sid ? settings.get().streaming.cmcd.sid : internalData.sid; data.sid = cmcdParametersFromManifest.sessionID ? cmcdParametersFromManifest.sessionID : data.sid; @@ -520,9 +548,33 @@ function CmcdModel() { data.sf = internalData.sf; } + if (data.v === 2) { + let ltc = playbackController.getCurrentLiveLatency() * 1000; + if (!isNaN(ltc)) { + data.ltc = ltc; + } + const msd = internalData.msd; + if (!_msdSent && !isNaN(msd)) { + data.msd = msd; + _msdSent = true; + } + } + + + return data; } + function _createCmcdV2HeadersCustomMap() { + const cmcdVersion = settings.get().streaming.cmcd.version; + return cmcdVersion === 1 ? {} : { + customHeaderMap: { + [CmcdHeaderField.REQUEST]: ['ltc'], + [CmcdHeaderField.SESSION]: ['msd'] + } + }; + } + function _getBitrateByRequest(request) { try { return parseInt(request.bandwidth / 1000); @@ -658,7 +710,7 @@ function CmcdModel() { playbackRate = 1; } let { bandwidth, mediaType, representation, duration } = request; - const mediaInfo = representation.mediaInfo + const mediaInfo = representation.mediaInfo; if (!mediaInfo) { return NaN; @@ -688,6 +740,8 @@ function CmcdModel() { eventBus.off(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this); eventBus.off(MediaPlayerEvents.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, instance); eventBus.off(MediaPlayerEvents.PLAYBACK_SEEKED, _onPlaybackSeeked, instance); + eventBus.off(MediaPlayerEvents.PLAYBACK_STARTED, _onPlaybackStarted, instance); + eventBus.off(MediaPlayerEvents.PLAYBACK_PLAYING, _onPlaybackPlaying, instance); _resetInitialSettings(); } diff --git a/test/unit/mocks/PlaybackControllerMock.js b/test/unit/mocks/PlaybackControllerMock.js index 1d7f4211dc..d66793d881 100644 --- a/test/unit/mocks/PlaybackControllerMock.js +++ b/test/unit/mocks/PlaybackControllerMock.js @@ -133,6 +133,10 @@ class PlaybackControllerMock { this.lowLatencyEnabled = value; } + getCurrentLiveLatency() { + return 15; + } + } diff --git a/test/unit/test/streaming/streaming.models.CmcdModel.js b/test/unit/test/streaming/streaming.models.CmcdModel.js index e6934d9971..f5b5069794 100644 --- a/test/unit/test/streaming/streaming.models.CmcdModel.js +++ b/test/unit/test/streaming/streaming.models.CmcdModel.js @@ -715,6 +715,70 @@ describe('CmcdModel', function () { }); }); + + describe('getHeadersParameters() return CMCD v2 data correctly', () => { + it('getHeadersParameters() should return cmcd v2 data if version is 2', function () { + const REQUEST_TYPE = HTTPRequest.MEDIA_SEGMENT_TYPE; + const MEDIA_TYPE = 'video'; + + let request = { + type: REQUEST_TYPE, + mediaType: MEDIA_TYPE + }; + + settings.update({ + streaming: { + cmcd: { + version: 2, + enabledKeys: ['br', 'd', 'ot', 'tb', 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su', 'bs', 'rtp', 'cid', 'pr', 'sf', 'sid', 'st', 'v', 'msd', 'ltc', 'msd', 'ltc'], + } + } + }); + + let headers = cmcdModel.getHeaderParameters(request); + let metrics = decodeCmcd(headers[REQUEST_HEADER_NAME]); + expect(metrics).to.have.property('ltc'); + + eventBus.trigger(MediaPlayerEvents.PLAYBACK_STARTED); + eventBus.trigger(MediaPlayerEvents.PLAYBACK_PLAYING); + + headers = cmcdModel.getHeaderParameters(request); + metrics = decodeCmcd(headers[SESSION_HEADER_NAME]); + expect(metrics).to.have.property('msd'); + }); + + it('getHeadersParameters() should not return cmcd v2 data if the cmcd version is 1', function () { + const REQUEST_TYPE = HTTPRequest.MEDIA_SEGMENT_TYPE; + const MEDIA_TYPE = 'video'; + + let request = { + type: REQUEST_TYPE, + mediaType: MEDIA_TYPE + }; + + settings.update({ + streaming: { + cmcd: { + enabled: true, + version: 1 + }, + enabledKeys: ['br', 'd', 'ot', 'tb', 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su', 'bs', 'rtp', 'cid', 'pr', 'sf', 'sid', 'st', 'v', 'msd', 'ltc', 'msd', 'ltc'], + } + }); + + let headers = cmcdModel.getHeaderParameters(request); + let metrics = decodeCmcd(headers[REQUEST_HEADER_NAME]); + expect(metrics).to.not.have.property('ltc'); + + eventBus.trigger(MediaPlayerEvents.PLAYBACK_STARTED); + eventBus.trigger(MediaPlayerEvents.PLAYBACK_PLAYING); + + headers = cmcdModel.getHeaderParameters(request); + metrics = decodeCmcd(headers[REQUEST_HEADER_NAME]); + expect(metrics).to.not.have.property('msd'); + }); + }); + }) }) @@ -1396,6 +1460,68 @@ describe('CmcdModel', function () { }); }); + describe('getQueryParameter() return CMCD v2 data correctly', () => { + it('getQueryParameter() should return cmcd v2 data if the cmcd version is 2', function () { + const REQUEST_TYPE = HTTPRequest.MEDIA_SEGMENT_TYPE; + const MEDIA_TYPE = 'video'; + + let request = { + type: REQUEST_TYPE, + mediaType: MEDIA_TYPE + }; + + settings.update({ + streaming: { + cmcd: { + version: 2, + enabledKeys: ['br', 'd', 'ot', 'tb', 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su', 'bs', 'rtp', 'cid', 'pr', 'sf', 'sid', 'st', 'v', 'msd', 'ltc', 'msd', 'ltc'], + } + } + }); + let parameters = cmcdModel.getQueryParameter(request); + let metrics = decodeCmcd(parameters.value); + expect(metrics).to.have.property('ltc'); + + eventBus.trigger(MediaPlayerEvents.PLAYBACK_STARTED); + eventBus.trigger(MediaPlayerEvents.PLAYBACK_PLAYING); + + parameters = cmcdModel.getQueryParameter(request); + metrics = decodeCmcd(parameters.value); + expect(metrics).to.have.property('msd'); + }); + + it('getQueryParameter() sould not return cmcd v2 data if the cmcd version is 1', function () { + const REQUEST_TYPE = HTTPRequest.MEDIA_SEGMENT_TYPE; + const MEDIA_TYPE = 'video'; + + let request = { + type: REQUEST_TYPE, + mediaType: MEDIA_TYPE + }; + + settings.update({ + streaming: { + cmcd: { + enabled: true, + version: 1, + enabledKeys: ['br', 'd', 'ot', 'tb', 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su', 'bs', 'rtp', 'cid', 'pr', 'sf', 'sid', 'st', 'v', 'msd', 'ltc', 'msd', 'ltc'], + } + } + }); + + let parameters = cmcdModel.getQueryParameter(request); + let metrics = decodeCmcd(parameters.value); + expect(metrics).to.not.have.property('ltc'); + + eventBus.trigger(MediaPlayerEvents.PLAYBACK_STARTED); + eventBus.trigger(MediaPlayerEvents.PLAYBACK_PLAYING); + + parameters = cmcdModel.getQueryParameter(request); + metrics = decodeCmcd(parameters.value); + expect(metrics).to.not.have.property('msd'); + }); + }); + describe('applyParametersFromMpd', () => { it('should ignore service description cmcd configuration when applyParametersFromMpd is false', function () { const REQUEST_TYPE = HTTPRequest.MEDIA_SEGMENT_TYPE;