diff --git a/.changeset/chilly-yaks-bake.md b/.changeset/chilly-yaks-bake.md new file mode 100644 index 00000000..8c9db543 --- /dev/null +++ b/.changeset/chilly-yaks-bake.md @@ -0,0 +1,5 @@ +--- +"@theoplayer/nielsen-connector-web": minor +--- + +Add DCR support (CZ and US). diff --git a/nielsen/README.md b/nielsen/README.md index d598a24f..3b97a5a2 100644 --- a/nielsen/README.md +++ b/nielsen/README.md @@ -12,39 +12,79 @@ npm install @theoplayer/nielsen-connector-web ### Configuring the connector -Create the connector by providing the `THEOplayer` instance, the Nielsen App ID, the channelName for the asset -and optionally some Nielsen configuration. +Create the connector by providing the following mandatory parameters: + +- The `THEOplayer` instance +- the Nielsen App ID +- the channelName for the asset + +and optionally the following parameters: + +- a `NielsenOptions` object +- a `NielsenConfiguration` object (if none is provided, the default configuration disables DCR, enables DTVR and sets the country to US) ```js -import {NielsenConnector} from "../../dist/THEOplayerNielsenConnector"; +import { NielsenConnector } from '../../dist/THEOplayerNielsenConnector'; const appId = ''; const channelName = ''; -// Optional + +// Non-mandatory options const options: NielsenOptions = { - containerId: 'THEOplayer', + containerId: 'THEOplayer', optout: false -} +}; + +// Non-mandatory configuration (e.g. for DCR tracking with the Czech Republic SDK) +const configuration: NielsenConfiguration = { + country: NielsenCountry.CZ, + enableDTVR: false, + enableDCR: true +}; const nielsenConnector = new NielsenConnector(player, appId, channelName, options); ``` The `NielsenOptions` can have the following fields: | Key | Value | -|-----------------|-----------------------------------------------------------------| -| ` containerId ` | HTML DOM element id of the player container. | +| --------------- | --------------------------------------------------------------- | +| `containerId` | HTML DOM element id of the player container. | | ` nol_sdkDebug` | Enables Debug Mode which allows output to be viewed in console. | -| ` optout ` | Whether to opt-out of Nielsen Measurement. | +| `optout` | Whether to opt-out of Nielsen Measurement. | -### Passing metadata dynamically +### Passing metadata dynamically (DTVR) -The connector allows updating the current asset's metadata at any time: +The connector allows updating the current asset's metadata at any time. Note that Nielsen's [documentation]() prohibits updating of the values for `type`, `vidtype` or `assetid` parameters ```js const metadata = { ['channelName']: 'newChannelName', ['customTag1']: 'customValue1', - ['customTag2']: 'customValue2' -} + ['customTag2']: 'customValue2' +}; nielsenConnector.updateMetadata(metadata); ``` + +### Passing metadata when setting a source to the player (DCR) + +This can be achieved through the `updateDCRContentMetadata` method, e.g.: + +```js +const metadata: NielsenDCRContentMetadataCZ = { + assetid: 'cz-500358-98731568435405', + program: 'Animated Test Content', + title: 'Big Buck Bunny', + length: '596', + airdate: '20230620 20:00:00', + isfullepisode: true, + crossId1: '915 954 39504', + c2: '651678089925925', + segB: '011', + adloadtype: AdLoadType.linear, + hasAds: HasAds.supports_ads +}; + +nielsenConnector.updateDCRContentMetadata(metadata); +``` + +Note that types are included in the package: `NielsenDCRContentMetadataUS`, `NielsenDCRContentMetadataCZ`. Please contact your THEO Technologies representative if you need support for another International DCR SDK. diff --git a/nielsen/package.json b/nielsen/package.json index f1fe65b0..da1ccbd4 100644 --- a/nielsen/package.json +++ b/nielsen/package.json @@ -20,7 +20,7 @@ "bundle": "rollup -c rollup.config.mjs", "watch": "npm run bundle -- --watch", "build": "npm run clean && npm run bundle", - "serve": "http-server", + "serve": "http-server ./.. -o /nielsen/test/pages/main.html", "test": "jest" }, "author": "THEO Technologies NV", diff --git a/nielsen/src/index.ts b/nielsen/src/index.ts index ee3d9c68..9cf6bf32 100644 --- a/nielsen/src/index.ts +++ b/nielsen/src/index.ts @@ -1,2 +1,11 @@ export { NielsenConnector } from './integration/NielsenConnector'; -export { NielsenOptions } from './nielsen/Types'; +export { + NielsenOptions, + NielsenDCRContentMetadata, + NielsenDCRContentMetadataCZ, + NielsenDCRContentMetadataUS, + NielsenConfiguration, + NielsenCountry, + AdLoadType, + HasAds +} from './nielsen/Types'; diff --git a/nielsen/src/integration/NielsenConnector.ts b/nielsen/src/integration/NielsenConnector.ts index 371816bc..23c4177b 100644 --- a/nielsen/src/integration/NielsenConnector.ts +++ b/nielsen/src/integration/NielsenConnector.ts @@ -1,5 +1,5 @@ import type { ChromelessPlayer } from 'theoplayer'; -import { NielsenOptions } from '../nielsen/Types'; +import { NielsenConfiguration, NielsenDCRContentMetadata, NielsenOptions } from '../nielsen/Types'; import { NielsenHandler } from './NielsenHandler'; export class NielsenConnector { @@ -13,14 +13,24 @@ export class NielsenConnector { * @param instanceName User-defined string value for describing the player/site. * @param options Additional options. */ - constructor(player: ChromelessPlayer, appId: string, instanceName: string, options?: NielsenOptions) { - this.nielsenHandler = new NielsenHandler(player, appId, instanceName, options); + constructor( + player: ChromelessPlayer, + appId: string, + instanceName: string, + options?: NielsenOptions, + configuration?: NielsenConfiguration + ) { + this.nielsenHandler = new NielsenHandler(player, appId, instanceName, options, configuration); } updateMetadata(metadata: { [key: string]: string }): void { this.nielsenHandler.updateMetadata(metadata); } + updateDCRContentMetadata(metadata: NielsenDCRContentMetadata): void { + this.nielsenHandler.updateDCRContentMetadata(metadata); + } + destroy() { this.nielsenHandler.destroy(); } diff --git a/nielsen/src/integration/NielsenHandler.ts b/nielsen/src/integration/NielsenHandler.ts index 335b432c..9d14a7c8 100644 --- a/nielsen/src/integration/NielsenHandler.ts +++ b/nielsen/src/integration/NielsenHandler.ts @@ -1,14 +1,26 @@ import type { Ad, + AdBreakEvent, + AdEvent, AddTrackEvent, ChromelessPlayer, + DurationChangeEvent, TextTrack, TextTrackEnterCueEvent, + TimeUpdateEvent, VolumeChangeEvent } from 'theoplayer'; import { loadNielsenLibrary } from '../nielsen/NOLBUNDLE'; -import { AdMetadata, ContentMetadata, NielsenOptions } from '../nielsen/Types'; -import { getAdType } from '../utils/Util'; +import { + AdMetadata, + DCRContentMetadata, + DTVRContentMetadata, + NielsenConfiguration, + NielsenCountry, + NielsenDCRContentMetadata, + NielsenOptions +} from '../nielsen/Types'; +import { buildDCRAdMetadata, buildDCRContentMetadata, getAdType } from '../utils/Util'; const EMSG_PRIV_SUFFIX = 'PRIV{'; const EMSG_PAYLOAD_SUFFIX = 'payload='; @@ -16,6 +28,15 @@ const EMSG_PAYLOAD_SUFFIX = 'payload='; export class NielsenHandler { private player: ChromelessPlayer; + private debug: boolean; + + private dcrEnabled: boolean; + private dtvrEnabled: boolean; + private country: NielsenCountry = NielsenCountry.US; + + private metadata: DCRContentMetadata | undefined; + private lastReportedPlayheadPosition: number | undefined; + private nSdkInstance: any; private sessionInProgress: boolean = false; @@ -24,29 +45,59 @@ export class NielsenHandler { private decoder = new TextDecoder('utf-8'); - constructor(player: ChromelessPlayer, appId: string, instanceName: string, options?: NielsenOptions) { + constructor( + player: ChromelessPlayer, + appId: string, + instanceName: string, + options?: NielsenOptions, + configuration?: NielsenConfiguration + ) { this.player = player; - this.nSdkInstance = loadNielsenLibrary(appId, instanceName, options); + this.debug = options?.nol_sdkDebug === 'debug' ? true : false; + this.dcrEnabled = configuration?.enableDCR ?? false; + this.dtvrEnabled = configuration?.enableDTVR ?? true; + this.country = configuration?.country ?? NielsenCountry.US; + this.nSdkInstance = loadNielsenLibrary(appId, instanceName, options, this.country); this.addEventListeners(); } updateMetadata(metadata: { [key: string]: string }): void { - this.nSdkInstance.ggPM('updateMetadata', metadata); + switch (this.country) { + case NielsenCountry.US: { + const { type, vidtype, assetid, ...updateableParameters } = metadata; + if (this.debug) + console.log(`[NIELSEN] updateMetadata: ${{ type, vidtype, assetid }} will not be updated`); + this.nSdkInstance.ggPM('updateMetadata', updateableParameters); + break; + } + case NielsenCountry.CZ: + default: + } + } + + updateDCRContentMetadata(metadata: NielsenDCRContentMetadata): void { + if (!this.dcrEnabled) return; + this.metadata = buildDCRContentMetadata(metadata, this.country); } private addEventListeners(): void { this.player.addEventListener('play', this.onPlay); + this.player.addEventListener('pause', this.onInterrupt); + this.player.addEventListener('waiting', this.onInterrupt); this.player.addEventListener('ended', this.onEnd); this.player.addEventListener('sourcechange', this.onSourceChange); this.player.addEventListener('volumechange', this.onVolumeChange); this.player.addEventListener('loadedmetadata', this.onLoadMetadata); this.player.addEventListener('durationchange', this.onDurationChange); + this.player.addEventListener('timeupdate', this.onTimeUpdate); this.player.textTracks.addEventListener('addtrack', this.onAddTrack); if (this.player.ads) { this.player.ads.addEventListener('adbegin', this.onAdBegin); + this.player.ads.addEventListener('adend', this.onAdEnd); + this.player.ads.addEventListener('adbreakbegin', this.onAdBreakBegin); } window.addEventListener('beforeunload', this.onEnd); @@ -54,16 +105,21 @@ export class NielsenHandler { private removeEventListeners(): void { this.player.removeEventListener('play', this.onPlay); + this.player.removeEventListener('pause', this.onInterrupt); + this.player.removeEventListener('waiting', this.onInterrupt); this.player.removeEventListener('ended', this.onEnd); this.player.removeEventListener('sourcechange', this.onSourceChange); this.player.removeEventListener('volumechange', this.onVolumeChange); this.player.removeEventListener('loadedmetadata', this.onLoadMetadata); this.player.removeEventListener('durationchange', this.onDurationChange); + this.player.removeEventListener('timeupdate', this.onTimeUpdate); this.player.textTracks.removeEventListener('addtrack', this.onAddTrack); if (this.player.ads) { this.player.ads.removeEventListener('adbegin', this.onAdBegin); + this.player.ads.removeEventListener('adend', this.onAdEnd); + this.player.ads.removeEventListener('adbreakbegin', this.onAdBreakBegin); } window.removeEventListener('beforeunload', this.onEnd); @@ -73,7 +129,13 @@ export class NielsenHandler { this.maybeSendPlayEvent(); }; + private onInterrupt = () => { + if (!this.dcrEnabled) return; + this.nSdkInstance.ggPM('stop', this.getPlayHeadPosition()); + }; + private onEnd = () => { + if (this.dcrEnabled && this.player.ads?.playing) this.nSdkInstance.ggPM('stop', this.getPlayHeadPosition()); this.endSession(); }; @@ -83,17 +145,28 @@ export class NielsenHandler { }; private onVolumeChange = (event: VolumeChangeEvent) => { + if (!this.dtvrEnabled) return; const volumeLevel = this.player.muted ? 0 : event.volume * 100; this.nSdkInstance.ggPM('setVolume', volumeLevel); }; - private onDurationChange = () => { + private onDurationChange = ({ duration }: DurationChangeEvent) => { + if (isNaN(duration)) return; this.duration = this.player.duration; this.maybeSendPlayEvent(); }; + private onTimeUpdate = ({ currentTime }: TimeUpdateEvent) => { + if (!this.dcrEnabled) return; + const currentTimeFloor = Math.floor(currentTime); + if (currentTimeFloor === this.lastReportedPlayheadPosition) return; + this.lastReportedPlayheadPosition = currentTimeFloor; + this.nSdkInstance.ggPM('setPlayheadPosition', currentTimeFloor); + }; + private onLoadMetadata = () => { - const data: ContentMetadata = { + if (!this.dtvrEnabled) return; + const data: DTVRContentMetadata = { type: 'content', adModel: '1' // Always '1' for DTVR }; @@ -101,6 +174,7 @@ export class NielsenHandler { }; private onAddTrack = (event: AddTrackEvent) => { + if (!this.dtvrEnabled) return; if (event.track.kind === 'metadata') { const track = event.track as TextTrack; if (track.type === 'id3' || track.type === 'emsg') { @@ -164,25 +238,54 @@ export class NielsenHandler { } }; - private onAdBegin = () => { - const currentAd = this.player.ads!.currentAds.filter((ad: Ad) => ad.type === 'linear'); - const type = getAdType(this.player.ads!.currentAdBreak!); - const adMetadata: AdMetadata = { - type, - assetid: currentAd[0].id! - }; - this.nSdkInstance.ggPM('loadMetadata', adMetadata); + private onAdBegin = ({ ad }: AdEvent<'adbegin'>) => { + if (ad.type !== 'linear') return; + const { adBreak } = ad; + const { timeOffset } = adBreak; + const offset = this.player.ads?.dai?.contentTimeForStreamTime(timeOffset) ?? timeOffset; + const duration = this.player.ads?.dai?.contentTimeForStreamTime(this.duration) ?? this.duration; + const type = getAdType(offset, duration); + if (this.dtvrEnabled) { + const dtvrAdMetadata: AdMetadata = { + type, + assetid: ad.id! + }; + this.nSdkInstance.ggPM('loadMetadata', dtvrAdMetadata); + } + if (this.dcrEnabled) { + const dcrAdMetadata = buildDCRAdMetadata(ad, this.country, this.duration); + this.nSdkInstance.ggPM('loadMetadata', dcrAdMetadata); + } + }; + + private onAdEnd = () => { + if (!this.dcrEnabled) return; + this.nSdkInstance.ggPM('stop', this.getPlayHeadPosition()); + }; + + private onAdBreakBegin = ({ adBreak }: AdBreakEvent<'adbreakbegin'>) => { + if (!this.dcrEnabled) return; + const { timeOffset } = adBreak; + const offset = this.player.ads?.dai?.contentTimeForStreamTime(timeOffset) ?? timeOffset; + const duration = this.player.ads?.dai?.contentTimeForStreamTime(this.duration) ?? this.duration; + const isPostroll = getAdType(offset, duration) === 'postroll'; + if (!isPostroll) return; + this.endSession(); }; private maybeSendPlayEvent(): void { - if (!this.sessionInProgress && !Number.isNaN(this.duration)) { - this.sessionInProgress = true; + if (this.sessionInProgress || Number.isNaN(this.duration)) return; + this.sessionInProgress = true; + if (this.dtvrEnabled) { const metadataObject = { channelName: this.player.src, length: this.duration }; this.nSdkInstance.ggPM('play', metadataObject); } + if (this.dcrEnabled) { + this.nSdkInstance.ggPM('loadMetadata', this.metadata); + } } private endSession(): void { diff --git a/nielsen/src/nielsen/NOLBUNDLE.ts b/nielsen/src/nielsen/NOLBUNDLE.ts index 87d67c1a..83a2fa5c 100644 --- a/nielsen/src/nielsen/NOLBUNDLE.ts +++ b/nielsen/src/nielsen/NOLBUNDLE.ts @@ -1,40 +1,104 @@ /* eslint-disable */ -import { NielsenOptions } from './Types'; +import { NielsenCountry, NielsenOptions } from './Types'; -export function loadNielsenLibrary(appId: string, instanceName: string, options?: NielsenOptions) { - // https://engineeringportal.nielsen.com/docs/DTVR_Browser_SDK - // Add Static Queue Snippet to initialize a Nielsen SDK Instance. - // @ts-ignore - !(function (t: Window, n: any) { - t[n] = t[n] || { - nlsQ: function (e: any, o: any, c?: any, r?: any, s?: any, i?: any) { - // @ts-ignore - return ( - (s = t.document), - (r = s.createElement('script')), - (r.async = 1), - (r.src = - ('http:' === t.location.protocol ? 'http:' : 'https:') + - '//cdn-gl.imrworldwide.com/conf/' + - e + - '.js#name=' + - o + - '&ns=' + - n), - (i = s.getElementsByTagName('script')[0]), - i.parentNode.insertBefore(r, i), - (t[n][o] = t[n][o] || { - g: c || {}, - ggPM: function (e: any, c: any, r?: any, s?: any, i?: any) { - // @ts-ignore - (t[n][o].q = t[n][o].q || []).push([e, c, r, s, i]); - } - }), - t[n][o] - ); +export function loadNielsenLibrary( + appId: string, + instanceName: string, + options?: NielsenOptions, + country?: NielsenCountry +) { + if (country == NielsenCountry.CZ) { + // https://engineeringportal.nielsen.com/wiki/DCR_Czech_Video_Browser_SDK + // Step 1: Configure SDK + // @ts-ignore + !(function (e: any, n: any) { + function t(e: any) { + return 'object' == typeof e ? JSON.parse(JSON.stringify(e)) : e; } - }; - })(window, 'NOLBUNDLE'); + e[n] = e[n] || { + nlsQ: function (o: any, r: any, c: any) { + //@ts-ignore + var s = e.document, + a = s.createElement('script'); + (a.async = 1), + (a.src = + ('http:' === e.location.protocol ? 'http:' : 'https:') + + '//cdn-gl.imrworldwide.com/conf/' + + o + + '.js#name=' + + r + + '&ns=' + + n); + var i = s.getElementsByTagName('script')[0]; + return ( + i.parentNode.insertBefore(a, i), + (e[n][r] = e[n][r] || { + g: c || {}, + ggPM: function (o: any, c: any, s: any, a: any, i: any) { + // @ts-ignore + e[n][r].q = e[n][r].q || []; + try { + var l = t([o, c, s, a, i]); + e[n][r].q.push(l); + } catch (e) { + console && + console.log && + console.log('Error: Cannot register event in Nielsen SDK queue.'); + } + }, + trackEvent: function (o: any) { + // @ts-ignore + e[n][r].te = e[n][r].te || []; + try { + var c = t(o); + e[n][r].te.push(c); + } catch (e) { + console && + console.log && + console.log('Error: Cannot register event in Nielsen SDK queue.'); + } + } + }), + e[n][r] + ); + } + }; + })(window, 'NOLBUNDLE'); + } else { + // https://engineeringportal.nielsen.com/docs/DTVR_Browser_SDK + // Add Static Queue Snippet to initialize a Nielsen SDK Instance. + // @ts-ignore + !(function (t: Window, n: any) { + t[n] = t[n] || { + nlsQ: function (e: any, o: any, c?: any, r?: any, s?: any, i?: any) { + // @ts-ignore + return ( + (s = t.document), + (r = s.createElement('script')), + (r.async = 1), + (r.src = + ('http:' === t.location.protocol ? 'http:' : 'https:') + + '//cdn-gl.imrworldwide.com/conf/' + + e + + '.js#name=' + + o + + '&ns=' + + n), + (i = s.getElementsByTagName('script')[0]), + i.parentNode.insertBefore(r, i), + (t[n][o] = t[n][o] || { + g: c || {}, + ggPM: function (e: any, c: any, r?: any, s?: any, i?: any) { + // @ts-ignore + (t[n][o].q = t[n][o].q || []).push([e, c, r, s, i]); + } + }), + t[n][o] + ); + } + }; + })(window, 'NOLBUNDLE'); + } if (options) { return (window as any).NOLBUNDLE.nlsQ(appId, instanceName, options); diff --git a/nielsen/src/nielsen/Types.ts b/nielsen/src/nielsen/Types.ts index f2ffe88a..049fba98 100644 --- a/nielsen/src/nielsen/Types.ts +++ b/nielsen/src/nielsen/Types.ts @@ -1,5 +1,11 @@ export type AdType = 'preroll' | 'midroll' | 'postroll' | 'ad'; +export type NielsenConfiguration = { + country: NielsenCountry; + enableDTVR: boolean; + enableDCR: boolean; +}; + export type NielsenOptions = { // HTML DOM element id of the player container containerId?: string; @@ -11,10 +17,201 @@ export type NielsenOptions = { optout?: boolean; }; +export type DCRContentMetadata = { + /* + * A fixed dial specifying the type of measured content + */ + type: string; + /* + * Unique identifier for the video content (video file). Any label according to the needs of the TV company, which will ensure the identification of the same content across platforms. It can also be used for chaining several pieces of information from internal systems. At the beginning, the possibility of adding a client ID (to ensure uniqueness across clients) + */ + assetid: string; + /* + * Content description (name of the show, channel name, etc.) + */ + program: string; + /* + * Detailed description of content. + * [VOD] episode title + * [LIVE] name of the program (if it is available and can be dynamically changed along with the change of the program. Otherwise, fill in only the name of the channel)) + */ + title: string; + /* + * The length of the broadcast video content. It is also used to uniquely distinguish VOD and live broadcasts. This is the length of the currently playing content/file. If the content is, for example, only a part of the program, the length of this played part of the program is indicated. Use 86400 for live content. + */ + length: string; + /* + * Broadcast date, if you cannot fill in the correct value, please use the constant "19700101 00:00:01". + * [VOD] The date and time the video content was exposed online YYYYMMDD HH24:MI:SS. + * [LIVE] Midnight of the current day in YYYYMMDD 00:00:00 format + */ + airdate: string; + /* + * Indication of whether the video content being played is the entire episode or only part of it. Always use 'y' for live. + */ + isfullepisode: string; + /* + * CMS tag helper item. The method of recording ads insertion: 1. Linear – corresponds to TV insertion of ads 2. Dynamic – Dynamic Ad Insertion (DAI) + */ + adloadtype: string; +}; + +export enum AdLoadType { + linear = '1', + dynamic = '2' +} + +export type NielsenDCRContentMetadata = { + /* + * Unique identifier for the video content (video file). Any label according to the needs of the TV company, which will ensure the identification of the same content across platforms. It can also be used for chaining several pieces of information from internal systems. At the beginning, the possibility of adding a client ID (to ensure uniqueness across clients) + */ + assetid: string; + /* + * Content description (name of the show, channel name, etc.) + */ + program: string; + /* + * Detailed description of content. + * [VOD] episode title + * [LIVE] name of the program (if it is available and can be dynamically changed along with the change of the program. Otherwise, fill in only the name of the channel)) + */ + title: string; + /* + * The length of the broadcast video content. It is also used to uniquely distinguish VOD and live broadcasts. This is the length of the currently playing content/file. If the content is, for example, only a part of the program, the length of this played part of the program is indicated. Use 86400 for live content. + */ + length: string; + /* + * Broadcast date, if left empty, "19700101 00:00:01" will be reported. + * [VOD] The date and time the video content was exposed online YYYYMMDD HH24:MI:SS. + * [LIVE] Midnight of the current day in YYYYMMDD 00:00:00 format + */ + airdate?: string; + /* + * Indication of whether the video content being played is the entire episode or only part of it. Always reported as true for live. + */ + isfullepisode: boolean; + /* + * CMS tag helper item. The method of recording ads insertion: 1. Linear – corresponds to TV insertion of ads 2. Dynamic – Dynamic Ad Insertion (DAI) + */ + adloadtype: AdLoadType; +}; + +export enum HasAds { + no_ads = '0', + supports_ads = '1', + unknown = '2' +} + +export type NielsenDCRContentMetadataCZ = NielsenDCRContentMetadata & { + /* + * IDEC type identifier + */ + crossId1: string; + /* + * More detailed categorization of video content. + * [VOD] Program type (codes according to the TV code list). + * [LIVE] The program type of the content being played, if available and can be changed dynamically with the program change. Otherwise, send an empty string. + */ + segB: string; + /* + * More detailed categorization of video content. + */ + segC?: ''; + /* + * Live TV station code + * [VOD] Keep this empty and pass "nol_c1":"p1,". + * [LIVE] implementation : "nol_c1":"p1,value" where value = Live station code used in the output data of the TV audience measurement project. see current appendix of the reference manual "Appendix RP 13 - List of stations.xlsx" If the live broadcast does not correspond to any TV station, use the code 9999. + */ + c1?: string; + /* + * TV Identity for VOD. + * [VOD] Pass "nol_c2" : "p2,value" where value = To ensure the best possible harmonization of PEM D online measurement data with the data of the TV meter project, it is recommended to use TV IDENT as another online content identifier. If TVIDENT is not available at the time the content is brought online, c2 remains blank (and can be filled in later). TVIdent - same as in broadcast protocols of TV stations. + * [LIVE] Keep empty : "nol_c2":"p2," + */ + c2?: string; + /* + * CMS tag helper item. Indication of whether the content being played supports the insertion of advertisements. “0” – No ads “1” – Supports ads “2” – Don't know (default). + */ + hasAds: HasAds; +}; + +export type DCRContentMetadataCZ = DCRContentMetadata & { + /* + * IDEC type identifier + */ + crossId1: string; + /* + * More detailed categorization of video content. + * [VOD] Program type (codes according to the TV code list). + * [LIVE] The program type of the content being played, if available and can be changed dynamically with the program change. Otherwise, send an empty string. + */ + segB: string; + /* + * More detailed categorization of video content. + */ + segC: ''; + /* + * Live TV station code + * [VOD] Keep this empty and pass "nol_c1":"p1,". + * [LIVE] implementation : "nol_c1":"p1,value" where value = Live station code used in the output data of the TV audience measurement project. see current appendix of the reference manual "Appendix RP 13 - List of stations.xlsx" If the live broadcast does not correspond to any TV station, use the code 9999. + */ + nol_c1?: string; + /* + * TV Identity for VOD. + * [VOD] Pass "nol_c2" : "p2,value" where value = To ensure the best possible harmonization of PEM D online measurement data with the data of the TV meter project, it is recommended to use TV IDENT as another online content identifier. If TVIDENT is not available at the time the content is brought online, c2 remains blank (and can be filled in later). TVIdent - same as in broadcast protocols of TV stations. + * [LIVE] Keep empty : "nol_c2":"p2," + */ + nol_c2?: string; + /* + * CMS tag helper item. Indication of whether the content being played supports the insertion of advertisements. “0” – No ads “1” – Supports ads “2” – Don't know (default). + */ + hasAds: '0' | '1' | '2'; +}; + +export type NielsenDCRContentMetadataUS = NielsenDCRContentMetadata & { + /* + * Gracenote TMS ID (If available) should be passed for all telecasted content for clients using the Gracenote solution for proper matching purposes + * Note: The TMS ID will be a 14 character string. Normally leading with 2 alpha characters ('EP', 'MV', 'SH' or 'SP'), followed by 12 numbers. This should be provided to you by Nielsen + */ + crossId1?: string; + /* + * Populated by content distributor to contribute viewing from that distributor to the given content originator. For a full list of acceptable values, please contact your Nielsen representative. + */ + crossId2?: string; + /* + * One of two custom segments for clients' granular reporting within a brand. (Examples: Genre - horror, comedy, etc. ; Timeslot - primetime, daytime, etc. ; News type - breakingnews, weather, etc.) + */ + segB?: string; + /* + * One of two custom segments for clients' granular reporting within a brand. (Examples: Genre - horror, comedy, etc. ; Timeslot - primetime, daytime, etc. ; News type - breakingnews, weather, etc.) + */ + segC?: string; +}; + +export type DCRContentMetadataUS = DCRContentMetadata & { + /* + * Gracenote TMS ID (If available) should be passed for all telecasted content for clients using the Gracenote solution for proper matching purposes + * Note: The TMS ID will be a 14 character string. Normally leading with 2 alpha characters ('EP', 'MV', 'SH' or 'SP'), followed by 12 numbers. This should be provided to you by Nielsen + */ + crossId1?: string; + /* + * Populated by content distributor to contribute viewing from that distributor to the given content originator. For a full list of acceptable values, please contact your Nielsen representative. + */ + crossId2?: string; + /* + * One of two custom segments for clients' granular reporting within a brand. (Examples: Genre - horror, comedy, etc. ; Timeslot - primetime, daytime, etc. ; News type - breakingnews, weather, etc.) + */ + segB?: string; + /* + * One of two custom segments for clients' granular reporting within a brand. (Examples: Genre - horror, comedy, etc. ; Timeslot - primetime, daytime, etc. ; News type - breakingnews, weather, etc.) + */ + segC?: string; +}; + /** * adModel: 1) - Linear – matches TV ad load * 2) Dynamic – Dynamic Ad Insertion (DAI) */ -export type ContentMetadata = { +export type DTVRContentMetadata = { type: 'content'; adModel: '1' | '2'; } & { [key: string]: string }; @@ -23,3 +220,34 @@ export type AdMetadata = { type: AdType; assetid: any; // TODO string? or can be anything? } & { [key: string]: string }; + +export type DCRAdMetadataCZ = AdMetadata & { + /* + * An item in the CMS tag reserved for an identifier enabling the connection of an advertisement description from the RTVK system, similarly to PEM TV data. + */ + nol_c4: string; + /* + * More detailed categorization of video content + */ + nol_c5: string; + /* + * Ad type (same value as in the "type" item) + */ + nol_c6: string; + /* + * Ad description. Possible use for cases where the AKA code is not available. RTB - designation of the advertising supplier (e.g. if there is no AKA code or more detailed description of the advertisement). + */ + title?: string; + /* + * Length of broadcast ad in seconds. (So that the length indicator is available even in cases where the AKA code is not available.) + */ + length: string; +}; + +/* + * Countries for which (1) Nielsen provides DCR Browser SDKs and (2) the corresponding SDK was tested with this integration. + */ +export enum NielsenCountry { + US = 'US', + CZ = 'CZ' +} diff --git a/nielsen/src/utils/Util.ts b/nielsen/src/utils/Util.ts index 56006989..a0d5da2c 100644 --- a/nielsen/src/utils/Util.ts +++ b/nielsen/src/utils/Util.ts @@ -1,15 +1,91 @@ -import type { AdBreak } from 'theoplayer'; -import { AdType } from '../nielsen/Types'; +import type { Ad, AdBreak, GoogleImaAd } from 'theoplayer'; +import { + AdMetadata, + AdType, + DCRAdMetadataCZ, + DCRContentMetadata, + DCRContentMetadataCZ, + DCRContentMetadataUS, + NielsenCountry, + NielsenDCRContentMetadata, + NielsenDCRContentMetadataCZ, + NielsenDCRContentMetadataUS +} from '../nielsen/Types'; -export function getAdType(adBreak: AdBreak): AdType { - const currentAdBreakTimeOffset = adBreak.timeOffset; +export function getAdType(offset: number, duration: number): AdType { let currentAdBreakPosition: AdType = 'ad'; - if (currentAdBreakTimeOffset === 0) { + if (offset === 0) { currentAdBreakPosition = 'preroll'; - } else if (currentAdBreakTimeOffset < 0) { + } else if (offset < 0) { currentAdBreakPosition = 'postroll'; - } else if (currentAdBreakTimeOffset > 0) { + } else if (duration - offset < 1) { + currentAdBreakPosition = 'postroll'; // For DAI ads, the offset and duration will be converted from stream time to content time and coincide more or less + } else if (offset > 0) { currentAdBreakPosition = 'midroll'; } return currentAdBreakPosition; } + +export function buildDCRContentMetadata( + metadata: NielsenDCRContentMetadata, + country: NielsenCountry +): DCRContentMetadata | DCRContentMetadataUS | DCRContentMetadataCZ { + const dcrContentMetadata = { + type: 'content', + assetid: metadata.assetid, + program: metadata.program, + title: metadata.title, + length: metadata.length, + airdate: metadata?.airdate ?? '19700101 00:00:01', + isfullepisode: metadata.isfullepisode ? 'y' : 'n', + adloadtype: metadata.adloadtype + }; + if (country === NielsenCountry.CZ) { + const { crossId1, segB, segC, c1, c2, hasAds } = metadata as NielsenDCRContentMetadataCZ; + const dcrContentMetadataCZ: DCRContentMetadataCZ = { + ...dcrContentMetadata, + ['crossId1']: crossId1, + ['nol_c2']: `p2,${c2 ?? ''}`, + segB: segB, + segC: segC ?? '', + hasAds: hasAds + }; + if (c1) dcrContentMetadataCZ['nol_c1'] = `p1,${c1}`; + return dcrContentMetadataCZ; + } + if (country === NielsenCountry.US) { + const { crossId1, crossId2, segB, segC } = metadata as NielsenDCRContentMetadataUS; + const dcrContentMetadataUS: DCRContentMetadataUS = { + ...dcrContentMetadata + }; + if (crossId1) dcrContentMetadataUS['crossId1'] = crossId1; + if (crossId2) dcrContentMetadataUS['crossId2'] = crossId2; + if (segB) dcrContentMetadataUS['segB'] = segB; + if (segC) dcrContentMetadataUS['segC'] = segC; + return dcrContentMetadataUS; + } + return dcrContentMetadata; +} + +export function buildDCRAdMetadata(ad: Ad, country: NielsenCountry, duration: number): AdMetadata { + const adMetadata = { + assetid: ad.id ?? '', + type: getAdType(ad.adBreak.timeOffset, duration) + }; + if (country == NielsenCountry.US) { + return adMetadata; + } + if (country == NielsenCountry.CZ) { + const dcrAdMetadataCZ: DCRAdMetadataCZ = { + ...adMetadata, + ['nol_c4']: 'PLACEHOLDER', + ['nol_c5']: '2', // 1. regular show, 2. advertising, 3. trailer, 4. divide, 5. komerce (teleshopping,sponsor) TODO: provide API to control this + ['nol_c6']: `p6,${adMetadata.type}`, + title: (ad as GoogleImaAd).title ?? '', + length: ad.duration?.toString() ?? '0' + }; + return dcrAdMetadataCZ; + } + console.error('[NIELSEN - Error] No NielsenCountry was provided - sending only assetid and type'); + return adMetadata; +} diff --git a/nielsen/test/pages/main.html b/nielsen/test/pages/main.html index 910e458c..8cb95a65 100644 --- a/nielsen/test/pages/main.html +++ b/nielsen/test/pages/main.html @@ -3,9 +3,9 @@ Connector test page - - - + + +
@@ -15,29 +15,54 @@ ui: { fluid: true }, - libraryLocation: "/node_modules/theoplayer/" + libraryLocation: "./../../../node_modules/theoplayer/" }); // // Add your connector setup logic here // - const appId = "P77E3B909-D4B5-4E5C-9B5F-77B0E8FE27F5"; + const appId = "XXXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX"; const instanceName = "instanceName"; + const contentMetadataObjectCZ = { + assetid: 'cz-500358-98731568435405', + program: 'Animated Test Content', + title: 'Big Buck Bunny', + length: '596', + airdate: '20230620 20:00:00', + isfullepisode: true, + crossId1: '915 954 39504', + c2: '651678089925925', + segB: '011', + adloadtype: "2", + hasAds: "2" + }; + const instanceName = "exampleInstanceName"; const options = { // containerId: 'THEOplayer', nol_sdkDebug: "debug" }; + const configuration = { + country: "CZ", + enableDTVR: false, + enableDCR: true + } const nielsenConnector = new THEOplayerNielsenConnector.NielsenConnector( player, appId, instanceName, - options + options, + configuration ); // Set a source that works with your connector + // player.source = {sources: [{ src: "https://www.nielseninternet.com/DTVR/RTVOD_(PC-FD)_C3/prog_index.m3u8" }]} + player.source = { - sources: [{ src: "https://www.nielseninternet.com/DTVR/RTVOD_(PC-FD)_C3/prog_index.m3u8" }] + sources: [{ src: "https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny.m3u8" }] }; + + nielsenConnector.updateDCRContentMetadata(contentMetadataObjectCZ) +