diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index cb3419fb034..351f5a6c387 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -752,15 +752,8 @@ export class ChunkMetadata { // @public export class CMCDController implements ComponentAPI { constructor(hls: Hls); - static appendQueryToUri(uri: any, query: any): any; // (undocumented) destroy(): void; - // Warning: (ae-forgotten-export) The symbol "CMCD" needs to be exported by the entry point hls.d.ts - static serialize(data: CMCD): string; - // Warning: (ae-forgotten-export) The symbol "CMCDHeaders" needs to be exported by the entry point hls.d.ts - static toHeaders(data: CMCD): Partial; - static toQuery(data: CMCD): string; - static uuid(): string; } // Warning: (ae-missing-release-tag) "CMCDControllerConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -770,6 +763,7 @@ export type CMCDControllerConfig = { sessionId?: string; contentId?: string; useHeaders?: boolean; + includeKeys?: string[]; }; // Warning: (ae-missing-release-tag) "ComponentAPI" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/build-config.js b/build-config.js index 71225bf5230..ad3fb6dadc4 100644 --- a/build-config.js +++ b/build-config.js @@ -29,7 +29,7 @@ const buildTypeToOutputName = { light: `hls.light`, }; -/* Allow to customise builds through env-vars */ +/* Allow to customize builds through env-vars */ // eslint-disable-next-line no-undef const env = process.env; @@ -96,7 +96,7 @@ const babelTsWithPresetEnvTargets = ({ targets, stripConsole }) => babel({ extensions, babelHelpers: 'bundled', - exclude: 'node_modules/**', + exclude: /node_modules\/(?!(@svta)\/).*/, assumptions: { noDocumentAll: true, noClassCalls: true, diff --git a/package-lock.json b/package-lock.json index ee08b905b1d..9e6683f22e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3219,6 +3219,12 @@ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "dev": true }, + "node_modules/@svta/common-media-library": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.4.5.tgz", + "integrity": "sha512-H7fcIcfuWJfmGdaZDgIHUQwOniPnz3jZKc4vujfzx6c9pDaarm7T/TCuqHWJYVPLicQ2PH1W8a0RjpNEXON97Q==", + "dev": true + }, "node_modules/@testim/chrome-version": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.3.tgz", @@ -15257,6 +15263,12 @@ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "dev": true }, + "@svta/common-media-library": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.4.5.tgz", + "integrity": "sha512-H7fcIcfuWJfmGdaZDgIHUQwOniPnz3jZKc4vujfzx6c9pDaarm7T/TCuqHWJYVPLicQ2PH1W8a0RjpNEXON97Q==", + "dev": true + }, "@testim/chrome-version": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.3.tgz", diff --git a/package.json b/package.json index 12886eacfbe..5d7faf25717 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "@rollup/plugin-replace": "5.0.5", "@rollup/plugin-terser": "0.4.4", "@rollup/plugin-typescript": "11.1.5", + "@svta/common-media-library": "0.4.5", "@types/chai": "4.3.10", "@types/chart.js": "2.9.40", "@types/mocha": "10.0.4", diff --git a/src/config.ts b/src/config.ts index ecc1da73a84..ae639b23216 100644 --- a/src/config.ts +++ b/src/config.ts @@ -66,6 +66,7 @@ export type CMCDControllerConfig = { sessionId?: string; contentId?: string; useHeaders?: boolean; + includeKeys?: string[]; }; export type DRMSystemOptions = { diff --git a/src/controller/cmcd-controller.ts b/src/controller/cmcd-controller.ts index 3aaf40132c3..3a5e14bd325 100644 --- a/src/controller/cmcd-controller.ts +++ b/src/controller/cmcd-controller.ts @@ -1,12 +1,11 @@ import { Events } from '../events'; import Hls from '../hls'; -import { - CMCD, - CMCDHeaders, - CMCDObjectType, - CMCDStreamingFormatHLS, - CMCDVersion, -} from '../types/cmcd'; +import { Cmcd } from '@svta/common-media-library/cmcd/Cmcd'; +import { CmcdObjectType } from '@svta/common-media-library/cmcd/CmcdObjectType'; +import { CmcdStreamingFormat } from '@svta/common-media-library/cmcd/CmcdStreamingFormat'; +import { appendCmcdHeaders } from '@svta/common-media-library/cmcd/appendCmcdHeaders'; +import { appendCmcdQuery } from '@svta/common-media-library/cmcd/appendCmcdQuery'; +import { uuid } from '@svta/common-media-library/utils/uuid'; import { BufferHelper } from '../utils/buffer-helper'; import { logger } from '../utils/logger'; import type { ComponentAPI } from '../types/component-api'; @@ -37,6 +36,7 @@ export default class CMCDController implements ComponentAPI { private sid?: string; private cid?: string; private useHeaders: boolean = false; + private includeKeys?: string[]; private initialized: boolean = false; private starved: boolean = false; private buffering: boolean = true; @@ -52,9 +52,10 @@ export default class CMCDController implements ComponentAPI { config.pLoader = this.createPlaylistLoader(); config.fLoader = this.createFragmentLoader(); - this.sid = cmcd.sessionId || CMCDController.uuid(); + this.sid = cmcd.sessionId || uuid(); this.cid = cmcd.contentId; this.useHeaders = cmcd.useHeaders === true; + this.includeKeys = cmcd.includeKeys; this.registerListeners(); } } @@ -129,10 +130,10 @@ export default class CMCDController implements ComponentAPI { /** * Create baseline CMCD data */ - private createData(): CMCD { + private createData(): Cmcd { return { - v: CMCDVersion, - sf: CMCDStreamingFormatHLS, + v: 1, + sf: CmcdStreamingFormat.HLS, sid: this.sid, cid: this.cid, pr: this.media?.playbackRate, @@ -143,14 +144,14 @@ export default class CMCDController implements ComponentAPI { /** * Apply CMCD data to a request. */ - private apply(context: LoaderContext, data: CMCD = {}) { + private apply(context: LoaderContext, data: Cmcd = {}) { // apply baseline data Object.assign(data, this.createData()); const isVideo = - data.ot === CMCDObjectType.INIT || - data.ot === CMCDObjectType.VIDEO || - data.ot === CMCDObjectType.MUXED; + data.ot === CmcdObjectType.INIT || + data.ot === CmcdObjectType.VIDEO || + data.ot === CmcdObjectType.MUXED; if (this.starved && isVideo) { data.bs = true; @@ -164,24 +165,22 @@ export default class CMCDController implements ComponentAPI { // TODO: Implement rtp, nrr, nor, dl - if (this.useHeaders) { - const headers = CMCDController.toHeaders(data); - if (!Object.keys(headers).length) { - return; - } + const { includeKeys } = this; + if (includeKeys) { + data = Object.keys(data).reduce((acc, key) => { + includeKeys.includes(key) && (acc[key] = data[key]); + return acc; + }, {}); + } + if (this.useHeaders) { if (!context.headers) { context.headers = {}; } - Object.assign(context.headers, headers); + appendCmcdHeaders(context.headers, data); } else { - const query = CMCDController.toQuery(data); - if (!query) { - return; - } - - context.url = CMCDController.appendQueryToUri(context.url, query); + context.url = appendCmcdQuery(context.url, data); } } @@ -191,7 +190,7 @@ export default class CMCDController implements ComponentAPI { private applyPlaylistData = (context: PlaylistLoaderContext) => { try { this.apply(context, { - ot: CMCDObjectType.MANIFEST, + ot: CmcdObjectType.MANIFEST, su: !this.initialized, }); } catch (error) { @@ -207,15 +206,15 @@ export default class CMCDController implements ComponentAPI { const fragment = context.frag; const level = this.hls.levels[fragment.level]; const ot = this.getObjectType(fragment); - const data: CMCD = { + const data: Cmcd = { d: fragment.duration * 1000, ot, }; if ( - ot === CMCDObjectType.VIDEO || - ot === CMCDObjectType.AUDIO || - ot == CMCDObjectType.MUXED + ot === CmcdObjectType.VIDEO || + ot === CmcdObjectType.AUDIO || + ot == CmcdObjectType.MUXED ) { data.br = level.bitrate / 1000; data.tb = this.getTopBandwidth(ot) / 1000; @@ -231,27 +230,27 @@ export default class CMCDController implements ComponentAPI { /** * The CMCD object type. */ - private getObjectType(fragment: Fragment): CMCDObjectType | undefined { + private getObjectType(fragment: Fragment): CmcdObjectType | undefined { const { type } = fragment; if (type === 'subtitle') { - return CMCDObjectType.TIMED_TEXT; + return CmcdObjectType.TIMED_TEXT; } if (fragment.sn === 'initSegment') { - return CMCDObjectType.INIT; + return CmcdObjectType.INIT; } if (type === 'audio') { - return CMCDObjectType.AUDIO; + return CmcdObjectType.AUDIO; } if (type === 'main') { if (!this.hls.audioTracks.length) { - return CMCDObjectType.MUXED; + return CmcdObjectType.MUXED; } - return CMCDObjectType.VIDEO; + return CmcdObjectType.VIDEO; } return undefined; @@ -260,12 +259,12 @@ export default class CMCDController implements ComponentAPI { /** * Get the highest bitrate. */ - private getTopBandwidth(type: CMCDObjectType) { + private getTopBandwidth(type: CmcdObjectType) { let bitrate: number = 0; let levels; const hls = this.hls; - if (type === CMCDObjectType.AUDIO) { + if (type === CmcdObjectType.AUDIO) { levels = hls.audioTracks; } else { const max = hls.maxAutoLevel; @@ -285,10 +284,10 @@ export default class CMCDController implements ComponentAPI { /** * Get the buffer length for a media type in milliseconds */ - private getBufferLength(type: CMCDObjectType) { + private getBufferLength(type: CmcdObjectType) { const media = this.hls.media; const buffer = - type === CMCDObjectType.AUDIO ? this.audioBuffer : this.videoBuffer; + type === CmcdObjectType.AUDIO ? this.audioBuffer : this.videoBuffer; if (!buffer || !media) { return NaN; @@ -386,153 +385,4 @@ export default class CMCDController implements ComponentAPI { } }; } - - /** - * Generate a random v4 UUI - * - * @returns {string} - */ - static uuid(): string { - const url = URL.createObjectURL(new Blob()); - const uuid = url.toString(); - URL.revokeObjectURL(url); - return uuid.slice(uuid.lastIndexOf('/') + 1); - } - - /** - * Serialize a CMCD data object according to the rules defined in the - * section 3.2 of - * [CTA-5004](https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf). - */ - static serialize(data: CMCD): string { - const results: string[] = []; - const isValid = (value: any) => - !Number.isNaN(value) && value != null && value !== '' && value !== false; - const toRounded = (value: number) => Math.round(value); - const toHundred = (value: number) => toRounded(value / 100) * 100; - const toUrlSafe = (value: string) => encodeURIComponent(value); - const formatters = { - br: toRounded, - d: toRounded, - bl: toHundred, - dl: toHundred, - mtp: toHundred, - nor: toUrlSafe, - rtp: toHundred, - tb: toRounded, - }; - - const keys = Object.keys(data || {}).sort(); - - for (const key of keys) { - let value = data[key]; - - // ignore invalid values - if (!isValid(value)) { - continue; - } - - // Version should only be reported if not equal to 1. - if (key === 'v' && value === 1) { - continue; - } - - // Playback rate should only be sent if not equal to 1. - if (key == 'pr' && value === 1) { - continue; - } - - // Certain values require special formatting - const formatter = formatters[key]; - if (formatter) { - value = formatter(value); - } - - // Serialize the key/value pair - const type = typeof value; - let result: string; - - if (key === 'ot' || key === 'sf' || key === 'st') { - result = `${key}=${value}`; - } else if (type === 'boolean') { - result = key; - } else if (type === 'number') { - result = `${key}=${value}`; - } else { - result = `${key}=${JSON.stringify(value)}`; - } - - results.push(result); - } - - return results.join(','); - } - - /** - * Convert a CMCD data object to request headers according to the rules - * defined in the section 2.1 and 3.2 of - * [CTA-5004](https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf). - */ - static toHeaders(data: CMCD): Partial { - const keys = Object.keys(data); - const headers = {}; - const headerNames = ['Object', 'Request', 'Session', 'Status']; - const headerGroups = [{}, {}, {}, {}]; - const headerMap = { - br: 0, - d: 0, - ot: 0, - tb: 0, - bl: 1, - dl: 1, - mtp: 1, - nor: 1, - nrr: 1, - su: 1, - cid: 2, - pr: 2, - sf: 2, - sid: 2, - st: 2, - v: 2, - bs: 3, - rtp: 3, - }; - - for (const key of keys) { - // Unmapped fields are mapped to the Request header - const index = headerMap[key] != null ? headerMap[key] : 1; - headerGroups[index][key] = data[key]; - } - - for (let i = 0; i < headerGroups.length; i++) { - const value = CMCDController.serialize(headerGroups[i]); - if (value) { - headers[`CMCD-${headerNames[i]}`] = value; - } - } - - return headers; - } - - /** - * Convert a CMCD data object to query args according to the rules - * defined in the section 2.2 and 3.2 of - * [CTA-5004](https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf). - */ - static toQuery(data: CMCD): string { - return `CMCD=${encodeURIComponent(CMCDController.serialize(data))}`; - } - - /** - * Append query args to a uri. - */ - static appendQueryToUri(uri, query) { - if (!query) { - return uri; - } - - const separator = uri.includes('?') ? '&' : '?'; - return `${uri}${separator}${query}`; - } } diff --git a/src/types/cmcd.ts b/src/types/cmcd.ts deleted file mode 100644 index 5d509ed6b4a..00000000000 --- a/src/types/cmcd.ts +++ /dev/null @@ -1,271 +0,0 @@ -/** - * CMCD spec version - */ -export const CMCDVersion = 1; - -/** - * CMCD Object Type - */ -export const enum CMCDObjectType { - MANIFEST = 'm', - AUDIO = 'a', - VIDEO = 'v', - MUXED = 'av', - INIT = 'i', - CAPTION = 'c', - TIMED_TEXT = 'tt', - KEY = 'k', - OTHER = 'o', -} - -/** - * CMCD Streaming Format - */ -export const CMCDStreamingFormatHLS = 'h'; - -/** - * CMCD Streaming Type - */ -const enum CMCDStreamType { - VOD = 'v', - LIVE = 'l', -} - -/** - * CMCD Headers - */ -export interface CMCDHeaders { - 'CMCD-Object': string; - 'CMCD-Request': string; - 'CMCD-Session': string; - 'CMCD-Status': string; -} - -/** - * CMCD - */ -export interface CMCD { - ///////////////// - // CMCD Object // - ///////////////// - - /** - * Encoded bitrate - * - * The encoded bitrate of the audio or video object being requested. This may not be known precisely by the player; however, - * it MAY be estimated based upon playlist/manifest declarations. If the playlist declares both peak and average bitrate values, - * the peak value should be transmitted. - * - * Integer kbps - */ - br?: number; - - /** - * Object duration - * - * The playback duration in milliseconds of the object being requested. If a partial segment is being requested, then this value - * MUST indicate the playback duration of that part and not that of its parent segment. This value can be an approximation of the - * estimated duration if the explicit value is not known. - * - * Integer milliseconds - */ - d?: number; - - /** - * Object type - * - * The media type of the current object being requested: - * - `m` = text file, such as a manifest or playlist - * - `a` = audio only - * - `v` = video only - * - `av` = muxed audio and video - * - `i` = init segment - * - `c` = caption or subtitle - * - `tt` = ISOBMFF timed text track - * - `k` = cryptographic key, license or certificate. - * - `o` = other - * - * If the object type being requested is unknown, then this key MUST NOT be used. - */ - ot?: CMCDObjectType; - - /** - * Top bitrate - * - * The highest bitrate rendition in the manifest or playlist that the client is allowed to play, given current codec, licensing and - * sizing constraints. - * - * Integer Kbps - */ - tb?: number; - - ////////////////// - // CMCD Request // - ////////////////// - /** - * Buffer length - * - * The buffer length associated with the media object being requested. This value MUST be rounded to the nearest 100 ms. This key SHOULD only be - * sent with an object type of ‘a’, ‘v’ or ‘av’. - * - * Integer milliseconds - */ - bl?: number; - - /** - * Deadline - * - * Deadline from the request time until the first sample of this Segment/Object needs to be available in order to not create a buffer underrun or - * any other playback problems. This value MUST be rounded to the nearest 100ms. For a playback rate of 1, this may be equivalent to the player’s - * remaining buffer length. - * - * Integer milliseconds - */ - dl?: number; - - /** - * Measured mtp CMCD throughput - * - * The throughput between client and server, as measured by the client and MUST be rounded to the nearest 100 kbps. This value, however derived, - * SHOULD be the value that the client is using to make its next Adaptive Bitrate switching decision. If the client is connected to multiple - * servers concurrently, it must take care to report only the throughput measured against the receiving server. If the client has multiple concurrent - * connections to the server, then the intent is that this value communicates the aggregate throughput the client sees across all those connections. - * - * Integer kbps - */ - mtp?: number; - - /** - * Next object request - * - * Relative path of the next object to be requested. This can be used to trigger pre-fetching by the CDN. This MUST be a path relative to the current - * request. This string MUST be URLEncoded. The client SHOULD NOT depend upon any pre-fetch action being taken - it is merely a request for such a - * pre-fetch to take place. - * - * String - */ - nor?: string; - - /** - * Next range request - * - * If the next request will be a partial object request, then this string denotes the byte range to be requested. If the ‘nor’ field is not set, then the - * object is assumed to match the object currently being requested. The client SHOULD NOT depend upon any pre-fetch action being taken – it is merely a - * request for such a pre-fetch to take place. Formatting is similar to the HTTP Range header, except that the unit MUST be ‘byte’, the ‘Range:’ prefix is - * NOT required and specifying multiple ranges is NOT allowed. Valid combinations are: - * - * - `"\-"` - * - `"\-\"` - * - `"-\"` - * - * String - */ - nrr?: string; - - /** - * Startup - * - * Key is included without a value if the object is needed urgently due to startup, seeking or recovery after a buffer-empty event. The media SHOULD not be - * rendering when this request is made. This key MUST not be sent if it is FALSE. - * - * Boolean - */ - su?: boolean; - - ////////////////// - // CMCD Session // - ////////////////// - - /** - * Content ID - * - * A unique string identifying the current content. Maximum length is 64 characters. This value is consistent across multiple different - * sessions and devices and is defined and updated at the discretion of the service provider. - * - * String - */ - cid?: string; - - /** - * Playback rate - * - * `1` if real-time, `2` if double speed, `0` if not playing. SHOULD only be sent if not equal to `1`. - * - * Decimal - */ - pr?: number; - - /** - * Streaming format - * - * The streaming format that defines the current request. - * - * - `d` = MPEG DASH - * - `h` = HTTP Live Streaming (HLS) - * - `s` = Smooth Streaming - * - `o` = other - * - * If the streaming format being requested is unknown, then this key MUST NOT be used. - */ - sf?: typeof CMCDStreamingFormatHLS; - - /** - * Session ID - * - * A GUID identifying the current playback session. A playback session typically ties together segments belonging to a single media asset. - * Maximum length is 64 characters. It is RECOMMENDED to conform to the UUID specification. - * - * String - */ - sid?: string; - - /** - * Stream type - * - `v` = all segments are available – e.g., VOD - * - `l` = segments become available over time – e.g., LIVE - */ - st?: CMCDStreamType; - - /** - * CMCD version - * - * The version of this specification used for interpreting the defined key names and values. If this key is omitted, the client and server MUST - * interpret the values as being defined by version 1. Client SHOULD omit this field if the version is 1. - * - * Integer - */ - v?: number; - - ///////////////// - // CMCD Status // - ///////////////// - - /** - * Buffer starvation - * - * Key is included without a value if the buffer was starved at some point between the prior request and this object request, - * resulting in the player being in a rebuffering state and the video or audio playback being stalled. This key MUST NOT be - * sent if the buffer was not starved since the prior request. - * - * If the object type `ot` key is sent along with this key, then the `bs` key refers to the buffer associated with the particular - * object type. If no object type is communicated, then the buffer state applies to the current session. - * - * Boolean - */ - bs?: boolean; - - /** - * Requested maximum throughput - * - * The requested maximum throughput that the client considers sufficient for delivery of the asset. Values MUST be rounded to the - * nearest 100kbps. For example, a client would indicate that the current segment, encoded at 2Mbps, is to be delivered at no more - * than 10Mbps, by using rtp=10000. - * - * Note: This can benefit clients by preventing buffer saturation through over-delivery and can also deliver a community benefit - * through fair-share delivery. The concept is that each client receives the throughput necessary for great performance, but no more. - * The CDN may not support the rtp feature. - * - * Integer kbps - */ - rtp?: number; -} diff --git a/tests/unit/controller/cmcd-controller.ts b/tests/unit/controller/cmcd-controller.ts index fa4d29c53dd..ac90fa45a1e 100644 --- a/tests/unit/controller/cmcd-controller.ts +++ b/tests/unit/controller/cmcd-controller.ts @@ -1,6 +1,7 @@ import CMCDController from '../../../src/controller/cmcd-controller'; import HlsMock from '../../mocks/hls.mock'; import type { CMCDControllerConfig } from '../../../src/config'; +import { CmcdHeaderField } from '@svta/common-media-library/cmcd/CmcdHeaderField'; import chai from 'chai'; @@ -9,88 +10,30 @@ const expect = chai.expect; let cmcdController; const uuidRegex = - '[A-F\\d]{8}-[A-F\\d]{4}-4[A-F\\d]{3}-[89AB][A-F\\d]{3}-[A-F\\d]{12}'; - -const data = { - sid: 'c936730c-031e-4a73-976f-92bc34039c60', - cid: 'xyz', - su: false, - nor: '../testing/3.m4v', - nrr: '0-99', - d: 6066.66, - mtp: 10049, - bs: true, - br: 52317, - v: 1, - pr: 1, - 'com.test-hello': 'world', - 'com.test-testing': 1234, - 'com.test-exists': true, - 'com.test-notExists': false, -}; + '[a-f\\d]{8}-[a-f\\d]{4}-4[a-f\\d]{3}-[89ab][a-f\\d]{3}-[a-f\\d]{12}'; -const setupEach = function (cmcd?: CMCDControllerConfig) { +const setupEach = (cmcd?: CMCDControllerConfig) => { cmcdController = new CMCDController(new HlsMock({ cmcd }) as any); }; -describe('CMCDController', function () { - describe('Query serialization', function () { - it('produces correctly serialized data', function () { - const query = CMCDController.toQuery(data); - const result = - 'CMCD=br%3D52317%2Cbs%2Ccid%3D%22xyz%22%2C' + - 'com.test-exists%2Ccom.test-hello%3D%22world%22%2C' + - 'com.test-testing%3D1234%2C' + - 'd%3D6067%2Cmtp%3D10000%2C' + - 'nor%3D%22..%252Ftesting%252F3.m4v%22%2C' + - 'nrr%3D%220-99%22%2C' + - 'sid%3D%22c936730c-031e-4a73-976f-92bc34039c60%22'; - expect(query).to.equal(result); - }); - - it('appends with ?', function () { - const result = CMCDController.appendQueryToUri( - 'http://test.com', - 'CMCD=d%3D6067', - ); - expect(result).to.equal('http://test.com?CMCD=d%3D6067'); - }); - - it('appends with &', function () { - const result = CMCDController.appendQueryToUri( - 'http://test.com?testing=123', - 'CMCD=d%3D6067', - ); - expect(result).to.equal('http://test.com?testing=123&CMCD=d%3D6067'); - }); - }); +const base = { + url: 'https://test.com/test.mpd', + headers: undefined, +}; - describe('Header serialization', function () { - it('produces all header shards', function () { - const header = CMCDController.toHeaders(data); - expect(header).to.deep.equal({ - 'CMCD-Object': 'br=52317,d=6067', - 'CMCD-Request': - 'com.test-exists,com.test-hello="world",' + - 'com.test-testing=1234,mtp=10000,' + - 'nor="..%2Ftesting%2F3.m4v",nrr="0-99"', - 'CMCD-Session': 'cid="xyz",sid="c936730c-031e-4a73-976f-92bc34039c60"', - 'CMCD-Status': 'bs', - }); - }); +const applyPlaylistData = (data = { frag: {} }) => { + const context = Object.assign(data, base); + cmcdController.applyPlaylistData(context); + return context; +}; - it('ignores empty shards', function () { - expect(CMCDController.toHeaders({ br: 200 })).to.deep.equal({ - 'CMCD-Object': 'br=200', - }); - }); - }); +const expectField = (result, expected) => { + const regex = new RegExp(expected); + expect(regex.test(result)).to.equal(true); +}; +describe('CMCDController', function () { describe('cmcdController instance', function () { - const context = { - url: 'https://test.com/test.mpd', - }; - describe('configuration', function () { it('does not modify requests when disabled', function () { setupEach(); @@ -100,14 +43,37 @@ describe('CMCDController', function () { expect(config.fLoader).to.equal(undefined); }); + it('uses the session id if provided', function () { + const sessionId = 'SESSION_ID'; + setupEach({ sessionId }); + + const { url } = applyPlaylistData(); + expectField(url, `sid%3D%22${sessionId}%22`); + }); + it('generates a session id if not provided', function () { setupEach({}); - const c = Object.assign({ frag: {} }, context); + const { url } = applyPlaylistData(); + expectField(url, `sid%3D%22${uuidRegex}%22`); + }); + + it('uses the content id if provided', function () { + const contentId = 'CONTENT_ID'; + setupEach({ contentId }); + + const { url } = applyPlaylistData(); + expectField(url, `cid%3D%22${contentId}%22`); + }); + + it('uses headers if configured', function () { + const contentId = 'CONTENT_ID'; + setupEach({ contentId, useHeaders: true }); + + const { url, headers = {} } = applyPlaylistData(); - cmcdController.applyPlaylistData(c); - const regex = new RegExp(`sid%3D%22${uuidRegex}%22`, 'i'); - expect(regex.test(c.url)).to.equal(true); + expect(url).to.equal(base.url); + expectField(headers[CmcdHeaderField.SESSION], `cid="${contentId}"`); }); }); });