diff --git a/package-lock.json b/package-lock.json index 2375bbefe3..e1f48208b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,12 @@ "license": "BSD-3-Clause", "dependencies": { "@svta/cml-608": "1.0.1", - "@svta/cml-cmcd": "2.0.0", - "@svta/cml-cmsd": "1.0.3", - "@svta/cml-dash": "1.0.3", - "@svta/cml-id3": "1.0.3", - "@svta/cml-request": "1.0.4", - "@svta/cml-xml": "1.1.1", + "@svta/cml-cmcd": "2.1.1", + "@svta/cml-cmsd": "1.0.4", + "@svta/cml-dash": "1.0.4", + "@svta/cml-id3": "1.0.4", + "@svta/cml-request": "1.0.6", + "@svta/cml-xml": "1.1.2", "bcp-47-match": "^2.0.3", "bcp-47-normalize": "^2.3.0", "codem-isoboxer": "0.3.10", @@ -2548,100 +2548,100 @@ } }, "node_modules/@svta/cml-cmcd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@svta/cml-cmcd/-/cml-cmcd-2.0.0.tgz", - "integrity": "sha512-6Eigwk8tXdsKPGUcDzAsjzxq58lDj56XrG2IGDIOOLG1tEFmtFUajtoJTWk69FLHEwbNM9clDtfGXQ8Pr8+7/A==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@svta/cml-cmcd/-/cml-cmcd-2.1.1.tgz", + "integrity": "sha512-cEkULGNbZ2QhOGBby9KwqkjdznJRhPXDEUZM8pmwEg33lKGM0bCCK8kBTpVjs5+jP+6M5g4ua5nrgYDOFYCR3w==", "license": "Apache-2.0", "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-structured-field-values": "1.1.0", - "@svta/cml-utils": "1.2.0" + "@svta/cml-structured-field-values": "1.1.1", + "@svta/cml-utils": "1.3.0" } }, "node_modules/@svta/cml-cmsd": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@svta/cml-cmsd/-/cml-cmsd-1.0.3.tgz", - "integrity": "sha512-8DYYwVrrJMpY6CbnPQHDWgsoip9ewEURAzlLOWwOuIgS6xaf4kzYep0QBKZwKlWKTq+BRXQHNk7Ns3QaKzbw7Q==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@svta/cml-cmsd/-/cml-cmsd-1.0.4.tgz", + "integrity": "sha512-4284bLITFLayFOE7aloXS5WkhlOJjIrw50pOwEdK5wWO02vGjcQmx0vrkc4MCOh2lmpgvuzrNfyaFlTl9JyH2A==", "license": "Apache-2.0", "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-cta": "1.0.3", - "@svta/cml-structured-field-values": "1.1.0", - "@svta/cml-utils": "1.2.0" + "@svta/cml-cta": "1.0.4", + "@svta/cml-structured-field-values": "1.1.1", + "@svta/cml-utils": "1.3.0" } }, "node_modules/@svta/cml-cta": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@svta/cml-cta/-/cml-cta-1.0.3.tgz", - "integrity": "sha512-BoFuFPwCv1h6D2l75GSf1oZzHMK6XmV6Cfe2SDuq0ZfOjzoQ+0vjd8btKk53Gom+sjw6A/54aogFkinriS5rWA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@svta/cml-cta/-/cml-cta-1.0.4.tgz", + "integrity": "sha512-lNIrX9jnM/31q+O/p3QsR0IvRMa2OrHuNeaw86mDBrb3oyJcBOOWGQcCr2yLcf6EuBgbzQmswDEQecmlWRL9dg==", "license": "Apache-2.0", "peer": true, "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-structured-field-values": "1.1.0", - "@svta/cml-utils": "1.2.0" + "@svta/cml-structured-field-values": "1.1.1", + "@svta/cml-utils": "1.3.0" } }, "node_modules/@svta/cml-dash": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@svta/cml-dash/-/cml-dash-1.0.3.tgz", - "integrity": "sha512-x6szXcB7uGq0grbFn2X0zP8Oxlh/R3mmgNtzpnm3Wpa0mXxiaBnLX+ULsuF37JYgMDlIT93e/gi21PJoXQxvzg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@svta/cml-dash/-/cml-dash-1.0.4.tgz", + "integrity": "sha512-1opr9SMb8fCN6FpApTF+T9DCu8gvMNwIi5oi2i2IBXLysszT+f++cDcfNq5E+1AJq4N0uoreKcW4PKwTQ2XDFw==", "license": "Apache-2.0", "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-utils": "1.2.0" + "@svta/cml-utils": "1.3.0" } }, "node_modules/@svta/cml-id3": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@svta/cml-id3/-/cml-id3-1.0.3.tgz", - "integrity": "sha512-nWqDFJndPwT01rHlzlNQkkRSi10nKW6AmhZ+ejrWjaPMNV+GxykmVH13kATKYhluW5iLyyzfQPIIdEWBFmGMvw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@svta/cml-id3/-/cml-id3-1.0.4.tgz", + "integrity": "sha512-v2IU+91SDrDXga6yZITqyBHa2Y1RVGT1ts6BHg3/YOHOCBAXy4o5gq9SQSRlHFvw6jzhJ3JdSpjSaXs3PPBwDw==", "license": "Apache-2.0", "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-utils": "1.2.0" + "@svta/cml-utils": "1.3.0" } }, "node_modules/@svta/cml-request": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@svta/cml-request/-/cml-request-1.0.4.tgz", - "integrity": "sha512-gGhvtiV4fXolVdYQ8daDOpsKfPc1y88cBLCZOHaYIVWNO5nXGhWKiCI3Daez6JFawyDCIyRqtrZo3f8jmujgQA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@svta/cml-request/-/cml-request-1.0.6.tgz", + "integrity": "sha512-+vCTcNN8Y8sFNSxIIFsIFckUcLKFd7v2H5Gg5ej2iKz2Z7MX5U/jxX4/McBDVA7g8AEqR6snHg7TbpUShD5hRA==", "license": "Apache-2.0", "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-utils": "1.2.0", - "@svta/cml-xml": "1.1.1" + "@svta/cml-utils": "1.3.0", + "@svta/cml-xml": "1.1.2" } }, "node_modules/@svta/cml-structured-field-values": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@svta/cml-structured-field-values/-/cml-structured-field-values-1.1.0.tgz", - "integrity": "sha512-MecTsJA0sRITLI87WGrAQ80Ir9+eAanJ7LyhW0fNM6o0x0vezrEYCNdLmqE8iyVYg0Fi2NwCV8tIVHmZPEfiqQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@svta/cml-structured-field-values/-/cml-structured-field-values-1.1.1.tgz", + "integrity": "sha512-04zNbiY2HrOMAQ2F0iZ4yDbKaAg3UybtTARNv930bhIY00vmmd8Elt0jHB+Q3y/hC3I35QU8SGrORKT5LE2sOQ==", "license": "Apache-2.0", "peer": true, "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-utils": "1.2.0" + "@svta/cml-utils": "1.3.0" } }, "node_modules/@svta/cml-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@svta/cml-utils/-/cml-utils-1.2.0.tgz", - "integrity": "sha512-2oCminPTrIGox4zsemHcq4dVUwKX6Ln/OYQsXGHUvV8gTel1glHSfBsNWndyJPFTAQqsld7TFqI7lXOIrFvFQA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@svta/cml-utils/-/cml-utils-1.3.0.tgz", + "integrity": "sha512-TaVyR899SZpjvw0RgA+/wRnij6pf4r0l+W1qHie9H/cPGNJBsQG1sKLx8inmKHwvZzcPt2SP/fv9MwMyqAbC+Q==", "license": "Apache-2.0", "peer": true, "engines": { @@ -2649,15 +2649,15 @@ } }, "node_modules/@svta/cml-xml": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.1.1.tgz", - "integrity": "sha512-yrCYUO/k0KnRPFLv//RsbsUf1qLpEF7bV4toh2+sgU52Kp0kI5Hs4I+qZXqhVqJMUwS/D083A2nDV8z0fNinYQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.1.2.tgz", + "integrity": "sha512-4BCzRc1IpqfA94dsZXnfJ/8KsXaFYjJRbrMafgGT8tXwrKeR7ln6wO4L2FZ7b+D1e18zSHIyEqlU9MqKyMmnYg==", "license": "Apache-2.0", "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-utils": "1.2.0" + "@svta/cml-utils": "1.3.0" } }, "node_modules/@types/body-parser": { diff --git a/package.json b/package.json index 3298ffe8a0..19e1bd3392 100644 --- a/package.json +++ b/package.json @@ -90,12 +90,12 @@ }, "dependencies": { "@svta/cml-608": "1.0.1", - "@svta/cml-cmcd": "2.0.0", - "@svta/cml-cmsd": "1.0.3", - "@svta/cml-dash": "1.0.3", - "@svta/cml-id3": "1.0.3", - "@svta/cml-request": "1.0.4", - "@svta/cml-xml": "1.1.1", + "@svta/cml-cmcd": "2.1.1", + "@svta/cml-cmsd": "1.0.4", + "@svta/cml-dash": "1.0.4", + "@svta/cml-id3": "1.0.4", + "@svta/cml-request": "1.0.6", + "@svta/cml-xml": "1.1.2", "bcp-47-match": "^2.0.3", "bcp-47-normalize": "^2.3.0", "codem-isoboxer": "0.3.10", diff --git a/src/streaming/cmcd/config/CmcdPropertyMap.js b/src/streaming/cmcd/config/CmcdPropertyMap.js index 19cf3b3de7..ce6615b42c 100644 --- a/src/streaming/cmcd/config/CmcdPropertyMap.js +++ b/src/streaming/cmcd/config/CmcdPropertyMap.js @@ -329,13 +329,8 @@ const CmcdPropertyMap = { { path: 'settings.streaming.cmcd.targets[{targetIndex}].enabledKeys', priority: 1, - type: 'array' - }, - { - path: 'settings.streaming.cmcd.enabledKeys', - priority: 2, type: 'array', - default: Constants.CMCD_KEYS + default: [] } ] }, @@ -351,7 +346,7 @@ const CmcdPropertyMap = { path: 'settings.streaming.cmcd.targets[{targetIndex}].events', priority: 1, type: 'array', - default: Object.values(Constants.CMCD_REPORTING_EVENTS) + default: [] } ] }, diff --git a/src/streaming/controllers/CmcdController.js b/src/streaming/controllers/CmcdController.js index f7dfaf936a..5b0ae25d9f 100644 --- a/src/streaming/controllers/CmcdController.js +++ b/src/streaming/controllers/CmcdController.js @@ -169,8 +169,7 @@ function CmcdController() { // Reset flag only after confirming we will rebuild reporterNeedsRebuild = false; - cmcdReporter.stop(); - cmcdReporter.flush(); + cmcdReporter.stop(true); cmcdReporter = _createCmcdReporter(); cmcdReporter.start(); } @@ -243,17 +242,13 @@ function CmcdController() { }); }); } - + function _onStateChange(state) { // Update CmcdReporter with the new player state if (cmcdReporter) { cmcdReporter.update({ sta: state }); } - _onEventChange(Constants.CMCD_REPORTING_EVENTS.PLAY_STATE); - } - - function _onEventChange(event, response){ - triggerCmcdEventMode(event, response); + triggerCmcdEventMode(Constants.CMCD_REPORTING_EVENTS.PLAY_STATE); } function _onPeriodSwitchComplete() { @@ -281,32 +276,31 @@ function CmcdController() { } } - _onEventChange(Constants.CMCD_REPORTING_EVENTS.ERROR); + triggerCmcdEventMode(Constants.CMCD_REPORTING_EVENTS.ERROR); } - function triggerCmcdEventMode(event, response) { + function triggerCmcdEventMode(event) { if (!cmcdReporter) { return; } _rebuildReporterIfNeeded(); - let cmcdData = cmcdModel.triggerCmcdEventMode(); + const cmcdData = cmcdModel.getEventModeData(); - // For RESPONSE_RECEIVED, merge request CMCD data and response metrics - if (event === Constants.CMCD_REPORTING_EVENTS.RESPONSE_RECEIVED && response) { - cmcdData = { ...cmcdData, ...response.request.cmcd }; - cmcdData = _addCmcdResponseReceivedData(response, cmcdData); + // Route MSD through update() for the reporter's internal send-once tracking + const msdData = cmcdModel.calculateMsd(); + if (msdData.msd !== undefined) { + cmcdReporter.update(msdData); } - // Update reporter with calculated data and record the event - cmcdReporter.update(cmcdData); - cmcdReporter.recordEvent(event); + // Pass event-mode data as transient per-event data (not persisted) + cmcdReporter.recordEvent(event, cmcdData); } /** * Applies CMCD data to a request by decorating its URL and/or headers. - * Delegates to CmcdReporter.applyRequestReport() which handles + * Delegates to CmcdReporter.createRequestReport() which handles * transmission mode (query vs header) internally. * * @param {object} request - The request object with at least { url, type }. @@ -320,17 +314,18 @@ function CmcdController() { _rebuildReporterIfNeeded(); try { - const cmcdData = { - ...cmcdModel.calculateCmcdDataForRequest(request), - ...cmcdModel.updateMsdData(Constants.CMCD_REPORTING_MODE.REQUEST), - }; - - request.cmcd = cmcdData; //TODO: wrong because cmcdData only has data from model, not complete data with reporter - cmcdReporter.update(cmcdData); - - const decorated = cmcdReporter.applyRequestReport(request); + const cmcdData = cmcdModel.calculateCmcdDataForRequest(request); + + // Route MSD through update() for the reporter's internal send-once tracking + const msdData = cmcdModel.calculateMsd(); + if (msdData.msd !== undefined) { + cmcdReporter.update(msdData); + } + + const decorated = cmcdReporter.createRequestReport(request, cmcdData); request.url = decorated.url; request.headers = decorated.headers; + request.cmcd = decorated.customData?.cmcd || {}; _triggerCMCDDataGeneratedEvent(request) @@ -516,8 +511,8 @@ function CmcdController() { ...commonMediaRequest, url: request.url, headers: request.headers, - customData: { request }, cmcd: request.cmcd, + customData: { ...commonMediaRequest.customData, cmcd: request.cmcd }, }; return commonMediaRequest; @@ -532,48 +527,46 @@ function CmcdController() { if (requestType === HTTPRequest.CMCD_EVENT) { return response; } - _onEventChange(Constants.CMCD_REPORTING_EVENTS.RESPONSE_RECEIVED, response) + _handleResponseReceived(response); return response; } - function _addCmcdResponseReceivedData(response, cmcdData){ - const responseData = {}; - const request = response.request.customData.request; - const requestType = request.type; - - if (requestType === HTTPRequest.MEDIA_SEGMENT_TYPE){ - responseData.rc = response.status; + function _handleResponseReceived(response) { + if (!cmcdReporter) { + return; } - if (request.startDate && request.firstByteDate){ - responseData.ttfb = request.firstByteDate - request.startDate; - } + _rebuildReporterIfNeeded(); - if (request.endDate && request.startDate){ - responseData.ttlb = request.endDate - request.startDate - } + // Collect event-mode data from the model + const eventData = cmcdModel.getEventModeData(); - if (request.url) { - responseData.url = request.url.split('?')[0] + // Route MSD through update() for the reporter's internal send-once tracking + const msdData = cmcdModel.calculateMsd(); + if (msdData.msd !== undefined) { + cmcdReporter.update(msdData); } - - if (response.headers){ + + // Collect dash.js-specific additional data + const additionalData = {}; + + if (response.headers) { try { const cmsdStaticHeader = response.headers['cmsd-static']; if (cmsdStaticHeader) { - responseData.cmsds = btoa(cmsdStaticHeader); + additionalData.cmsds = btoa(cmsdStaticHeader); } const cmsdDynamicHeader = response.headers['cmsd-dynamic']; if (cmsdDynamicHeader) { - responseData.cmsdd = btoa(cmsdDynamicHeader); + additionalData.cmsdd = btoa(cmsdDynamicHeader); } } catch (e) { logger.warn('Failed to base64 encode CMSD headers, ignoring.', e); } } - return {...cmcdData, ...responseData}; + cmcdReporter.recordResponseReceived(response, { ...eventData, ...additionalData }); } function getCmcdParametersFromManifest() { @@ -592,8 +585,7 @@ function CmcdController() { eventBus.off(MediaPlayerEvents.PLAYBACK_WAITING, _onPlaybackWaiting, instance); if (cmcdReporter) { - cmcdReporter.stop(); - cmcdReporter.flush(); + cmcdReporter.stop(true); cmcdReporter = null; } diff --git a/src/streaming/models/CmcdModel.js b/src/streaming/models/CmcdModel.js index e5c862589f..af7a959713 100644 --- a/src/streaming/models/CmcdModel.js +++ b/src/streaming/models/CmcdModel.js @@ -60,15 +60,10 @@ function CmcdModel() { _playbackStartedTime, _isSeeking, streamProcessors, - _msdSent = { - [Constants.CMCD_REPORTING_MODE.EVENT]: false, - [Constants.CMCD_REPORTING_MODE.REQUEST]: false - }, _rebufferingStartTime = {}, _rebufferingDuration = {}, _streamType, - _streamingFormat, - _playbackRate; + _streamingFormat; let context = this.context; @@ -76,7 +71,7 @@ function CmcdModel() { cmcdConfig = CmcdConfigAccessor(context).getInstance(); resetInitialSettings(); } - + function setConfig(config) { if (!config) { return; @@ -157,29 +152,17 @@ function CmcdModel() { } if (nextRequest) { - if (cmcdConfig.getVersion() === 2) { - if (request.url !== nextRequest.url) { - const relativeUrl = Utils.getRelativeUrl(request.url, nextRequest.url); - const params = nextRequest.range ? { r: nextRequest.range } : undefined; - data.nor = [toCmcdValue(relativeUrl, params)]; - } - } else { - if (request.url !== nextRequest.url) { - data.nor = encodeURIComponent(Utils.getRelativeUrl(request.url, nextRequest.url)); - } else if (nextRequest.range) { - data.nrr = nextRequest.range; - } + if (request.url !== nextRequest.url) { + const relativeUrl = Utils.getRelativeUrl(request.url, nextRequest.url); + const params = nextRequest.range ? { r: nextRequest.range } : undefined; + data.nor = [toCmcdValue(relativeUrl, params)]; } } if (encodedBitrate) { - if (cmcdConfig.getVersion() === 2) { - const videoBr = mediaType === Constants.VIDEO ? encodedBitrate : null; - const audioBr = mediaType === Constants.AUDIO ? encodedBitrate : null; - data.br = _toInnerList(videoBr, audioBr) || [toCmcdValue(encodedBitrate, {})]; - } else { - data.br = encodedBitrate; - } + const videoBr = mediaType === Constants.VIDEO ? encodedBitrate : null; + const audioBr = mediaType === Constants.AUDIO ? encodedBitrate : null; + data.br = _toInnerList(videoBr, audioBr) || [toCmcdValue(encodedBitrate, {})]; } if (ot) { @@ -191,13 +174,9 @@ function CmcdModel() { } if (!isNaN(mtp)) { - if (cmcdConfig.getVersion() === 2) { - const videoMtp = mediaType === Constants.VIDEO ? mtp : null; - const audioMtp = mediaType === Constants.AUDIO ? mtp : null; - data.mtp = _toInnerList(videoMtp, audioMtp) || [toCmcdValue(mtp, {})]; - } else { - data.mtp = mtp; - } + const videoMtp = mediaType === Constants.VIDEO ? mtp : null; + const audioMtp = mediaType === Constants.AUDIO ? mtp : null; + data.mtp = _toInnerList(videoMtp, audioMtp) || [toCmcdValue(mtp, {})]; } if (!isNaN(dl)) { @@ -205,43 +184,27 @@ function CmcdModel() { } if (!isNaN(bl)) { - if (cmcdConfig.getVersion() === 2) { - const videoBl = mediaType === Constants.VIDEO ? bl : null; - const audioBl = mediaType === Constants.AUDIO ? bl : null; - data.bl = _toInnerList(videoBl, audioBl) || [toCmcdValue(bl, {})]; - } else { - data.bl = bl; - } + const videoBl = mediaType === Constants.VIDEO ? bl : null; + const audioBl = mediaType === Constants.AUDIO ? bl : null; + data.bl = _toInnerList(videoBl, audioBl) || [toCmcdValue(bl, {})]; } if (!isNaN(tb) && isFinite(tb)) { - if (cmcdConfig.getVersion() === 2) { - const videoTb = mediaType === Constants.VIDEO ? tb : null; - const audioTb = mediaType === Constants.AUDIO ? tb : null; - data.tb = _toInnerList(videoTb, audioTb) || [toCmcdValue(tb, {})]; - } else { - data.tb = tb; - } + const videoTb = mediaType === Constants.VIDEO ? tb : null; + const audioTb = mediaType === Constants.AUDIO ? tb : null; + data.tb = _toInnerList(videoTb, audioTb) || [toCmcdValue(tb, {})]; } if (tpb !== null && !isNaN(tpb)) { - if (cmcdConfig.getVersion() === 2) { - const videoTpb = mediaType === Constants.VIDEO ? tpb : null; - const audioTpb = mediaType === Constants.AUDIO ? tpb : null; - data.tpb = _toInnerList(videoTpb, audioTpb) || [toCmcdValue(tpb, {})]; - } else { - data.tpb = tpb; - } + const videoTpb = mediaType === Constants.VIDEO ? tpb : null; + const audioTpb = mediaType === Constants.AUDIO ? tpb : null; + data.tpb = _toInnerList(videoTpb, audioTpb) || [toCmcdValue(tpb, {})]; } - + if (pb !== null && !isNaN(pb)) { - if (cmcdConfig.getVersion() === 2) { - const videoPb = mediaType === Constants.VIDEO ? pb : null; - const audioPb = mediaType === Constants.AUDIO ? pb : null; - data.pb = _toInnerList(videoPb, audioPb) || [toCmcdValue(pb, {})]; - } else { - data.pb = pb; - } + const videoPb = mediaType === Constants.VIDEO ? pb : null; + const audioPb = mediaType === Constants.AUDIO ? pb : null; + data.pb = _toInnerList(videoPb, audioPb) || [toCmcdValue(pb, {})]; } if (_bufferLevelStarved[mediaType]) { @@ -250,13 +213,9 @@ function CmcdModel() { } if (_rebufferingDuration[mediaType]) { - if (cmcdConfig.getVersion() === 2) { - const videoBsd = mediaType === Constants.VIDEO ? _rebufferingDuration[mediaType] : null; - const audioBsd = mediaType === Constants.AUDIO ? _rebufferingDuration[mediaType] : null; - data.bsd = _toInnerList(videoBsd, audioBsd) || [toCmcdValue(_rebufferingDuration[mediaType], {})]; - } else { - data.bsd = _rebufferingDuration[mediaType]; - } + const videoBsd = mediaType === Constants.VIDEO ? _rebufferingDuration[mediaType] : null; + const audioBsd = mediaType === Constants.AUDIO ? _rebufferingDuration[mediaType] : null; + data.bsd = _toInnerList(videoBsd, audioBsd) || [toCmcdValue(_rebufferingDuration[mediaType], {})]; delete _rebufferingDuration[mediaType]; } @@ -346,7 +305,7 @@ function CmcdModel() { if (!streamProcessors || streamProcessors.length === 0) { return null; } - + const streamProcessor = streamProcessors.find(sp => sp.getType() === mediaType); const bitrate = streamProcessor?.getRepresentationController()?.getCurrentRepresentation()?.bitrateInKbit; @@ -589,28 +548,18 @@ function CmcdModel() { function getGenericCmcdData(mediaType) { const data = {}; - data.ts = Date.now(); + // Note: ts, st, sf, pr are handled by CmcdReporter: + // - ts: auto-generated by recordEvent() / recordResponseReceived() + // - st, sf: persisted via cmcdReporter.update() in _onManifestLoaded + // - pr: persisted via cmcdReporter.update() in _onPlaybackRateChanged - if (_streamType) { - data.st = _streamType; - } - if (_streamingFormat) { - data.sf = _streamingFormat; - } - if (_playbackRate !== undefined && _playbackRate !== 1) { - data.pr = _playbackRate; + let ltc = playbackController.getCurrentLiveLatency() * 1000; + if (!isNaN(ltc)) { + data.ltc = ltc; } - const cmcdVersion = cmcdConfig.getVersion(); - if (cmcdVersion === 2) { - let ltc = playbackController.getCurrentLiveLatency() * 1000; - if (!isNaN(ltc)) { - data.ltc = ltc; - } - - if (typeof document !== 'undefined' && document.hidden) { - data.bg = true; - } + if (typeof document !== 'undefined' && document.hidden) { + data.bg = true; } if (mediaType && _shouldIncludeDroppedFrames(mediaType)) { @@ -629,10 +578,9 @@ function CmcdModel() { mediaType === Constants.OTHER; } - function triggerCmcdEventMode(){ + function getEventModeData(){ const cmcdData = { ...getGenericCmcdData(), - ...updateMsdData(Constants.CMCD_REPORTING_MODE.EVENT), ..._getAggregatedBitrateData(), ..._getEncodedBitrateData(), ..._getBufferLevelData(), @@ -655,11 +603,6 @@ function CmcdModel() { _rebufferingDuration = {}; _streamType = undefined; _streamingFormat = undefined; - _playbackRate = undefined; - _msdSent = { - [Constants.CMCD_REPORTING_MODE.EVENT]: false, - [Constants.CMCD_REPORTING_MODE.REQUEST]: false - } _updateStreamProcessors(); } @@ -714,16 +657,12 @@ function CmcdModel() { } } - function updateMsdData(mode) { - const cmcdVersion = cmcdConfig.getVersion(); + function calculateMsd() { const data = {}; const msd = _calculateMsd(); - if (cmcdVersion === 2) { - if (!_msdSent[mode] && msd !== null && !isNaN(msd)) { - data.msd = msd; - _msdSent[mode] = true; - } + if (msd !== null && !isNaN(msd)) { + data.msd = msd; } return data; @@ -731,7 +670,6 @@ function CmcdModel() { function onPlaybackRateChanged(data) { if (data.playbackRate !== undefined) { - _playbackRate = data.playbackRate; return { pr: data.playbackRate }; } return null; @@ -861,30 +799,22 @@ function CmcdModel() { const activeStream = playbackController.getStreamController()?.getActiveStream(); if (!activeStream) { return data; - } - + } + // Get current representations const videoRep = activeStream.getCurrentRepresentationForType(Constants.VIDEO); const audioRep = activeStream.getCurrentRepresentationForType(Constants.AUDIO); const currentVideoBitrate = videoRep ? videoRep.bitrateInKbit : 0; const currentAudioBitrate = audioRep ? audioRep.bitrateInKbit : 0; - const isV2 = cmcdConfig.getVersion() === 2; // Calculate aggregated bitrate - if (isV2) { - const abValues = _toInnerList( - currentVideoBitrate > 0 ? Math.round(currentVideoBitrate) : null, - currentAudioBitrate > 0 ? Math.round(currentAudioBitrate) : null - ); - if (abValues) { - data.ab = abValues; - } - } else { - const aggregatedBitrate = currentVideoBitrate + currentAudioBitrate; - if (aggregatedBitrate > 0) { - data.ab = Math.round(aggregatedBitrate); - } + const abValues = _toInnerList( + currentVideoBitrate > 0 ? Math.round(currentVideoBitrate) : null, + currentAudioBitrate > 0 ? Math.round(currentAudioBitrate) : null + ); + if (abValues) { + data.ab = abValues; } // Calculate top aggregated bitrate @@ -892,42 +822,28 @@ function CmcdModel() { const allAudioReps = activeStream.getRepresentationsByType(Constants.AUDIO) || []; const topVideoBitrate = allVideoReps.reduce((max, rep) => Math.max(max, rep.bitrateInKbit), 0); const topAudioBitrate = allAudioReps.reduce((max, rep) => Math.max(max, rep.bitrateInKbit), 0); - if (isV2) { - const tabValues = _toInnerList( - topVideoBitrate > 0 ? Math.round(topVideoBitrate) : null, - topAudioBitrate > 0 ? Math.round(topAudioBitrate) : null - ); - if (tabValues) { - data.tab = tabValues; - } - } else { - const topAggregatedBitrate = topVideoBitrate + topAudioBitrate; - if (topAggregatedBitrate > 0) { - data.tab = Math.round(topAggregatedBitrate); - } + const tabValues = _toInnerList( + topVideoBitrate > 0 ? Math.round(topVideoBitrate) : null, + topAudioBitrate > 0 ? Math.round(topAudioBitrate) : null + ); + if (tabValues) { + data.tab = tabValues; } // Calculate lowest aggregated bitrate const lowestVideoBitrate = allVideoReps.length > 0 ? Math.min(...allVideoReps.map(rep => rep.bitrateInKbit)) : 0; const lowestAudioBitrate = allAudioReps.length > 0 ? Math.min(...allAudioReps.map(rep => rep.bitrateInKbit)) : 0; - if (isV2) { - const labValues = _toInnerList( - lowestVideoBitrate > 0 ? Math.round(lowestVideoBitrate) : null, - lowestAudioBitrate > 0 ? Math.round(lowestAudioBitrate) : null - ); - if (labValues) { - data.lab = labValues; - } - } else { - const lowestAggregatedBitrate = lowestVideoBitrate + lowestAudioBitrate; - if (lowestAggregatedBitrate > 0) { - data.lab = Math.round(lowestAggregatedBitrate); - } + const labValues = _toInnerList( + lowestVideoBitrate > 0 ? Math.round(lowestVideoBitrate) : null, + lowestAudioBitrate > 0 ? Math.round(lowestAudioBitrate) : null + ); + if (labValues) { + data.lab = labValues; } return data; } - + function getLastMediaTypeRequest() { return _lastMediaTypeRequest; } @@ -946,12 +862,12 @@ function CmcdModel() { onPlaybackSeeked, wasPlaying, onBufferLevelStateChanged, - updateMsdData, + calculateMsd, resetInitialSettings, getCmcdParametersFromManifest, onPlaybackRateChanged, onManifestLoaded, - triggerCmcdEventMode, + getEventModeData, isIncludedInRequestFilter, getLastMediaTypeRequest }; @@ -962,4 +878,4 @@ function CmcdModel() { } CmcdModel.__dashjs_factory_name = 'CmcdModel'; -export default FactoryMaker.getSingletonFactory(CmcdModel); \ No newline at end of file +export default FactoryMaker.getSingletonFactory(CmcdModel); diff --git a/src/streaming/net/HTTPLoader.js b/src/streaming/net/HTTPLoader.js index ccce901597..5641d1b70a 100644 --- a/src/streaming/net/HTTPLoader.js +++ b/src/streaming/net/HTTPLoader.js @@ -297,7 +297,8 @@ function HTTPLoader(cfg) { } const _updateResourceTimingInfo = function () { - commonMediaResponse.resourceTiming.responseEnd = Date.now(); + commonMediaResponse.resourceTiming.responseEnd = performance.now(); + commonMediaResponse.resourceTiming.duration = commonMediaResponse.resourceTiming.responseEnd - commonMediaResponse.resourceTiming.startTime; // If enabled the ResourceTimingApi we add the corresponding information to the request object. // These values are more accurate and can be used by the ThroughputController later @@ -314,7 +315,7 @@ function HTTPLoader(cfg) { httpRequest.customData.onabort = _onabort; httpRequest.customData.ontimeout = _ontimeout; - httpResponse.resourceTiming.startTime = Date.now(); + httpResponse.resourceTiming.startTime = performance.now(); loader.load(httpRequest, httpResponse); resolve(); }); @@ -386,7 +387,7 @@ function HTTPLoader(cfg) { const loaderInformation = _getLoader(requestObject); const loader = loaderInformation.loader; requestObject.fileLoaderType = loaderInformation.fileLoaderType; - + requestObject.headers = requestObject.headers || {}; _updateRequestUrlAndHeaders(requestObject); if (requestObject.range) { @@ -409,7 +410,7 @@ function HTTPLoader(cfg) { commonMediaResponse = new CommonMediaResponse({ request: commonMediaRequest, resourceTiming: { - startTime: Date.now(), + startTime: performance.now(), encodedBodySize: 0 }, status: 0 diff --git a/src/streaming/vo/CommonMediaRequest.js b/src/streaming/vo/CommonMediaRequest.js index 96a78884b0..02c837cec3 100644 --- a/src/streaming/vo/CommonMediaRequest.js +++ b/src/streaming/vo/CommonMediaRequest.js @@ -3,25 +3,24 @@ class CommonMediaRequest { * @param {Object} params * @param {string} params.url * @param {string} params.method + * @param {BodyInit} [params.body] * @param {string} [params.responseType] * @param {Object} [params.headers] * @param {RequestCredentials} [params.credentials] * @param {RequestMode} [params.mode] * @param {number} [params.timeout] - * @param {Cmcd} [params.cmcd] - * @param {any} [params.customData] + * @param {Object} [params.customData] */ constructor(params) { this.url = params.url; this.method = params.method; + this.body = params.body !== undefined ? params.body : null; this.responseType = params.responseType !== undefined ? params.responseType : null; this.headers = params.headers !== undefined ? params.headers : {}; this.credentials = params.credentials !== undefined ? params.credentials : null; this.mode = params.mode !== undefined ? params.mode : null; this.timeout = params.timeout !== undefined ? params.timeout : 0; - this.cmcd = params.cmcd !== undefined ? params.cmcd : null; - this.customData = params.customData !== undefined ? params.customData : null; - this.body = params.body !== undefined ? params.body : null; + this.customData = params.customData !== undefined ? params.customData : {}; } } diff --git a/test/unit/test/streaming/streaming.controllers.CmcdController.js b/test/unit/test/streaming/streaming.controllers.CmcdController.js index e224fc0a32..c03ae76ae9 100644 --- a/test/unit/test/streaming/streaming.controllers.CmcdController.js +++ b/test/unit/test/streaming/streaming.controllers.CmcdController.js @@ -104,7 +104,7 @@ describe('CmcdController', function () { expect(metrics).to.have.property('e', 'ps'); }); - it('should send all available keys and events if they are undefined', () => { + it('should not send any event if they are undefined', () => { settings.update({ streaming: { cmcd: { @@ -121,18 +121,7 @@ describe('CmcdController', function () { eventBus.trigger(MediaPlayerEvents.PLAYBACK_PLAYING); - expect(urlLoaderMock.load.calledOnce).to.be.true; - const requestSent = urlLoaderMock.load.firstCall.args[0].request; - expect(requestSent.url).to.equal('https://cmcd.event.collector/api'); - expect(requestSent.method).to.equal(HTTPRequest.POST); - expect(requestSent.body).to.be.a('string'); - - const metrics = decodeCmcd(decodeURIComponent(requestSent.body)); - expect(metrics).to.have.property('e', 'ps'); - expect(metrics).to.have.property('sta', 'p'); - expect(metrics).to.have.property('ts'); - expect(metrics).to.have.property('sid'); - expect(metrics).to.have.property('v'); + expect(urlLoaderMock.load.called).to.be.false; }); it('should send a report with event mode available keys', () => { @@ -266,6 +255,7 @@ describe('CmcdController', function () { targets: [{ url: 'https://cmcd.event.collector/api', enabled: true, + enabledKeys: ['sn'], events: ['ps'] }] } @@ -288,7 +278,7 @@ describe('CmcdController', function () { expect(metrics2).to.have.property('sn', 1); }); - it('should send mandatory keys if enabled keys is empty', () => { + it('should send mandatory keys if enabled keys is not defined', () => { settings.update({ streaming: { cmcd: { @@ -309,6 +299,7 @@ describe('CmcdController', function () { const mockResponse = { status: 200, request: { + url: 'http://test.url/video.m4s', customData: { request: { type: HTTPRequest.MEDIA_SEGMENT_TYPE, @@ -558,6 +549,7 @@ describe('CmcdController', function () { targets: [{ url: 'https://cmcd.event.collector/api', enabled: true, + enabledKeys: ['ab', 'tab', 'lab'], events: ['ps'], timeInterval: 0 }] @@ -663,6 +655,7 @@ describe('CmcdController', function () { const mockResponse = { status: 200, request: { + url: 'http://test.url/video.m4s', customData: { request: { type: HTTPRequest.MEDIA_SEGMENT_TYPE, @@ -673,6 +666,11 @@ describe('CmcdController', function () { } }, cmcd: { sid: 'session-id' }, + }, + resourceTiming: { + startTime: currentTime - 1000, + responseStart: currentTime - 500, + duration: 1000 } }; @@ -720,6 +718,7 @@ describe('CmcdController', function () { 'cmsd-dynamic': cmsdDynamicHeaderValue }, request: { + url: 'http://test.url/video.m4s', customData: { request: { type: HTTPRequest.MEDIA_SEGMENT_TYPE, @@ -761,6 +760,7 @@ describe('CmcdController', function () { const mockResponse = { status: 200, request: { + url: 'http://test.url/video.m4s', customData: { request: { type: HTTPRequest.MEDIA_SEGMENT_TYPE, @@ -811,58 +811,6 @@ describe('CmcdController', function () { expect(urlLoaderMock.load.called).to.be.false; }); - it('should send all available keys if enabledKeys is not defined', () => { - settings.update({ - streaming: { - cmcd: { - version: 2, - sid: 'session-id', - targets: [{ - url: 'https://cmcd.response.collector/api', - enabled: true, - includeOnRequests: ['segment'], - events: ['rr'], - timeInterval: 0 - }] - } - } - }); - cmcdController.initialize(); - - let currentTime = new Date(Date.now()); - const mockResponse = { - status: 200, - request: { - customData: { - request: { - type: HTTPRequest.MEDIA_SEGMENT_TYPE, - url: 'http://test.url/video.m4s', - startDate: currentTime - 1000, - firstByteDate: currentTime - 500, - endDate: new Date() - } - }, - cmcd: { sid: 'session-id' }, - } - }; - - const interceptor = cmcdController.getCmcdResponseInterceptors()[0]; - interceptor(mockResponse); - - expect(urlLoaderMock.load.calledOnce).to.be.true; - const requestSent = urlLoaderMock.load.firstCall.args[0].request; - expect(requestSent.url).to.equal('https://cmcd.response.collector/api'); - expect(requestSent.method).to.equal(HTTPRequest.POST); - - const metrics = decodeCmcd(decodeURIComponent(requestSent.body)); - expect(metrics).to.have.property('rc'); - expect(metrics).to.have.property('sid', 'session-id'); - expect(metrics).to.have.property('url', 'http://test.url/video.m4s'); - expect(metrics).to.have.property('ttfb'); - expect(metrics).to.have.property('ttlb'); - expect(metrics).to.have.property('v'); - }); - it('should send a response report with response mode available keys', () => { settings.update({ streaming: { @@ -885,6 +833,7 @@ describe('CmcdController', function () { const mockResponse = { status: 200, request: { + url: 'http://test.url/video.m4s', customData: { request: { type: HTTPRequest.MEDIA_SEGMENT_TYPE, @@ -895,6 +844,11 @@ describe('CmcdController', function () { } }, cmcd: { sid: 'session-id' }, + }, + resourceTiming: { + startTime: currentTime - 1000, + responseStart: currentTime - 500, + duration: 1000 } }; @@ -921,6 +875,7 @@ describe('CmcdController', function () { url: 'https://cmcd.response.collector/api', enabled: true, includeOnRequests: ['segment'], + enabledKeys: ['sn'], events: ['rr'] }] } @@ -931,6 +886,7 @@ describe('CmcdController', function () { const mockResponse = { status: 200, request: { + url: 'http://test.url/video.m4s', customData: { request: { type: HTTPRequest.MEDIA_SEGMENT_TYPE, diff --git a/test/unit/test/streaming/streaming.models.CmcdModel.js b/test/unit/test/streaming/streaming.models.CmcdModel.js index d5994ace18..a364fd251f 100644 --- a/test/unit/test/streaming/streaming.models.CmcdModel.js +++ b/test/unit/test/streaming/streaming.models.CmcdModel.js @@ -7,6 +7,7 @@ import DashMetricsMock from '../../mocks/DashMetricsMock.js'; import PlaybackControllerMock from '../../mocks/PlaybackControllerMock.js'; import ThroughputControllerMock from '../../mocks/ThroughputControllerMock.js'; import ServiceDescriptionControllerMock from '../../mocks/ServiceDescriptionControllerMock.js'; +import { SfItem } from '@svta/cml-structured-field-values'; import {expect} from 'chai'; import sinon from 'sinon'; @@ -100,7 +101,7 @@ describe('CmcdModel', function () { const data = cmcdModel.calculateCmcdDataForRequest(request); expect(data).to.exist; expect(data.ot).to.equal('v'); // video object type - expect(data.br).to.equal(1000); // bitrate in kbps + expect(data.br).to.deep.equal([new SfItem(1000, { v: true })]); // bitrate in kbps expect(data.d).to.equal(4000); // duration in ms }); @@ -172,48 +173,31 @@ describe('CmcdModel', function () { } } }); - + const isIncluded = cmcdModel.isIncludedInRequestFilter(HTTPRequest.MEDIA_SEGMENT_TYPE); expect(isIncluded).to.be.false; }); }); - describe('updateMsdData', function () { - it('should return MSD data for version 2', function () { - settings.update({ - streaming: { - cmcd: { - version: 2 - } - } - }); - + describe('calculateMsd', function () { + it('should return MSD data when playback has started', function () { cmcdModel.onPlaybackStarted(); cmcdModel.onPlaybackPlaying(); - - const msdData = cmcdModel.updateMsdData(Constants.CMCD_REPORTING_MODE.REQUEST); + + const msdData = cmcdModel.calculateMsd(); expect(msdData).to.have.property('msd').that.is.a('number'); }); - it('should not return MSD data for version 1', function () { - settings.update({ - streaming: { - cmcd: { - version: 1 - } - } - }); - - const msdData = cmcdModel.updateMsdData(Constants.CMCD_REPORTING_MODE.REQUEST); + it('should return empty object when playback has not started', function () { + const msdData = cmcdModel.calculateMsd(); expect(Object.keys(msdData)).to.have.length(0); }); }); - describe('triggerCmcdEventMode', function () { + describe('getEventModeData', function () { it('should return event mode CMCD data', function () { - const eventData = cmcdModel.triggerCmcdEventMode(); + const eventData = cmcdModel.getEventModeData(); expect(eventData).to.exist; - expect(eventData.ts).to.be.a('number'); }); }); @@ -236,7 +220,7 @@ describe('CmcdModel', function () { cmcdModel.onPlaybackPlaying(); const data = cmcdModel.calculateCmcdDataForRequest(request); - expect(data.bsd).to.equal(500); + expect(data.bsd).to.deep.equal([new SfItem(500, { v: true })]); const data2 = cmcdModel.calculateCmcdDataForRequest(request); expect(data2.bsd).to.not.exist; @@ -287,4 +271,4 @@ describe('CmcdModel', function () { expect(result).to.deep.equal({}); }); }); -}); \ No newline at end of file +});