diff --git a/.changeset/blue-gorillas-lie.md b/.changeset/blue-gorillas-lie.md new file mode 100644 index 00000000..1f24a6b4 --- /dev/null +++ b/.changeset/blue-gorillas-lie.md @@ -0,0 +1,5 @@ +--- +"@theoplayer/comscore-connector-web": patch +--- + +Fixed an issue where DVR window length and offsets were incorrectly reported. diff --git a/.changeset/chilled-lamps-happen.md b/.changeset/chilled-lamps-happen.md new file mode 100644 index 00000000..3e245cf3 --- /dev/null +++ b/.changeset/chilled-lamps-happen.md @@ -0,0 +1,5 @@ +--- +"@theoplayer/comscore-connector-web": patch +--- + +Fixed an issue where playhead positions or content/ad durations were not reported in (rounded) milliseconds. diff --git a/.changeset/kind-pugs-grin.md b/.changeset/kind-pugs-grin.md new file mode 100644 index 00000000..e36f5682 --- /dev/null +++ b/.changeset/kind-pugs-grin.md @@ -0,0 +1,5 @@ +--- +"@theoplayer/comscore-connector-web": patch +--- + +Fixed an issue where only one ad in an adbreak would be reported. diff --git a/.changeset/metal-ties-draw.md b/.changeset/metal-ties-draw.md new file mode 100644 index 00000000..9310f4a4 --- /dev/null +++ b/.changeset/metal-ties-draw.md @@ -0,0 +1,5 @@ +--- +"@theoplayer/comscore-connector-web": patch +--- + +Fixed an issue where playback of the main content wouldn't get reported if Google IMA returned an empty pre-roll ad break. diff --git a/.changeset/rare-cycles-give.md b/.changeset/rare-cycles-give.md new file mode 100644 index 00000000..329b7c9d --- /dev/null +++ b/.changeset/rare-cycles-give.md @@ -0,0 +1,5 @@ +--- +"@theoplayer/comscore-connector-web": patch +--- + +Fixed an issue where no content metadata was reported during a pre-roll ad. diff --git a/.changeset/wise-vans-begin.md b/.changeset/wise-vans-begin.md new file mode 100644 index 00000000..0c2f4f27 --- /dev/null +++ b/.changeset/wise-vans-begin.md @@ -0,0 +1,5 @@ +--- +"@theoplayer/comscore-connector-web": minor +--- + +Add the option to inform the ComScore library of the environment it is running in through the `setPlatformAPI`. diff --git a/comscore/README.md b/comscore/README.md index 773e446b..83e18ab8 100644 --- a/comscore/README.md +++ b/comscore/README.md @@ -27,6 +27,7 @@ const comscoreConfig = { publisherId: '', applicationName: 'Test App', userConsent: '1', + platformApi: ns_.analytics.PlatformAPIs.WebBrowser, debug: true }; @@ -47,6 +48,43 @@ const comscoreMetadata = { const comscoreConnector = new ComscoreConnector(player, comscoreConfig, comscoreMetadata); ``` +### Optional ComscoreConfiguration properties + +#### `usagePropertiesAutoUpdateMode` + +When omitted this wil default to foregroundOnly. + +#### `skeleton` + +Pass an interface object with target platform specific implementations for the necessary Platform APIs. E.g. + +```js +analytics.PlatformApi.setPlatformApi(analytics.PlatformApi.PlatformApis.Skeleton, { + onDataFetch: function (onSuccessCallback, onErrorCallback) { + // Execute a function with platform-specific code to retrieve up-to-date information. + runPlatformSpecificCodeToRetrieveValues(onSuccessCallback, onErrorCallback); + } + + // Other overridden PlatformAPI methods, as needed. +}); +``` + +For more information, please consult the [Skeleton PlatformAPI Implementation Guide](https://mymetrix-support.comscore.com/hc/en-us/article_attachments/19635711827867) + +Note that if the skeleton property is defined, the connector will always use `setPlatformAPI(ns_.analytics.PlatformAPIs.Skeleton)`. + +#### `platformApi` + +Pass a valid value from `ns_.analytics.PlatformAPIs`. When omitted, the connector will report `setPlatformAPI(ns_.analytics.PlatformAPIs.html5)`. + +#### `adIdProcessor` + +Pass a function with the following signature if you require custom ad id handling: `(ad: Ad) => string`. When omitted, the connector will use `(ad) => ad.id`. Consult THEOplayer's types for more info about the `Ad` interface. + +#### `debug` + +A flag to enable verbose logging. + ### Passing metadata dynamically The connector allows updating the current asset's metadata at any time. Do it when setting a new source to the player. diff --git a/comscore/package.json b/comscore/package.json index 2f1ab53d..6658fbb8 100644 --- a/comscore/package.json +++ b/comscore/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 ./.. -o /comscore/test/pages/main.html", + "serve": "http-server ./.. -o /comscore/test/pages/main_umd.html", "test": "echo \"No tests yet\"" }, "repository": { diff --git a/comscore/src/api/ComscoreConfiguration.ts b/comscore/src/api/ComscoreConfiguration.ts index 9d06e5a6..5d0b0b15 100644 --- a/comscore/src/api/ComscoreConfiguration.ts +++ b/comscore/src/api/ComscoreConfiguration.ts @@ -12,6 +12,24 @@ export enum ComscoreUsagePropertiesAutoUpdateMode { disabled = "disabled" } +export enum ComscorePlatformAPIs { + SmartTV = 0, + Netcast = 1, + Cordova = 2, + Trilithium = 3, + AppleTV = 4, + Chromecast = 5, + Xbox = 6, + webOS = 7, + tvOS = 8, + nodejs = 9, + html5 = 10, + JSMAF = 11, + Skeleton = 12, + WebBrowser = 13, + SamsungTizenTV = 14 +} + export interface ComscoreConfiguration { /** * Also known as the c2 value @@ -24,6 +42,10 @@ export interface ComscoreConfiguration { */ usagePropertiesAutoUpdateMode?: ComscoreUsagePropertiesAutoUpdateMode; skeleton?: any; + /** + * Defaults to ns_.analytics.PlatformAPIs.html5 if no skeleton is provided or ns_.analytics.PlatformAPIs.Skeleton if a skeleton is provided. + */ + platformApi?: ComscorePlatformAPIs; adIdProcessor?: (ad: Ad) => string; debug?: boolean; } diff --git a/comscore/src/api/ComscoreConnector.ts b/comscore/src/api/ComscoreConnector.ts index a79338f1..58480f00 100644 --- a/comscore/src/api/ComscoreConnector.ts +++ b/comscore/src/api/ComscoreConnector.ts @@ -1,5 +1,5 @@ import { ChromelessPlayer } from 'theoplayer'; -import type { ComscoreConfiguration, ComscoreUserConsent } from './ComscoreConfiguration'; +import { ComscoreConfiguration, ComscorePlatformAPIs, ComscoreUserConsent } from './ComscoreConfiguration'; import type { ComscoreMetadata } from './ComscoreMetadata'; import { ComscoreStreamingAnalyticsTHEOIntegration } from '../integration/ComscoreStreamingAnalyticsTHEOIntegration'; @@ -28,7 +28,10 @@ export class ComscoreConnector { // Set platform API if (this.configuration.skeleton) { this.analytics.PlatformApi.setPlatformAPI(this.analytics.PlatformAPIs.Skeleton, this.configuration.skeleton) - } else { + } else if (this.configuration.platformApi){ + this.analytics.PlatformApi.setPlatformAPI(mapPlatformAPI(this.configuration.platformApi)) + if (this.configuration.debug) console.log(`[COMSCORE] Set the Platform API to ${this.configuration.platformApi}`) + } else { this.analytics.PlatformApi.setPlatformAPI(this.analytics.PlatformAPIs.html5) } @@ -96,4 +99,41 @@ export class ComscoreConnector { destroy(): void { this.streamingAnalyticsIntegration.destroy(); } -} \ No newline at end of file +} + +function mapPlatformAPI(platformApi: ComscorePlatformAPIs): ns_.analytics.PlatformAPIs { + switch (platformApi) { + case ComscorePlatformAPIs.SmartTV: + return ns_.analytics.PlatformAPIs.SmartTV; + case ComscorePlatformAPIs.Netcast: + return ns_.analytics.PlatformAPIs.Netcast; + case ComscorePlatformAPIs.Cordova: + return ns_.analytics.PlatformAPIs.Cordova; + case ComscorePlatformAPIs.Trilithium: + return ns_.analytics.PlatformAPIs.Trilithium; + case ComscorePlatformAPIs.AppleTV: + return ns_.analytics.PlatformAPIs.AppleTV; + case ComscorePlatformAPIs.Chromecast: + return ns_.analytics.PlatformAPIs.Chromecast; + case ComscorePlatformAPIs.Xbox: + return ns_.analytics.PlatformAPIs.Xbox; + case ComscorePlatformAPIs.webOS: + return ns_.analytics.PlatformAPIs.webOS; + case ComscorePlatformAPIs.tvOS: + return ns_.analytics.PlatformAPIs.tvOS; + case ComscorePlatformAPIs.nodejs: + return ns_.analytics.PlatformAPIs.nodejs; + case ComscorePlatformAPIs.html5: + return ns_.analytics.PlatformAPIs.html5; + case ComscorePlatformAPIs.JSMAF: + return ns_.analytics.PlatformAPIs.JSMAF; + case ComscorePlatformAPIs.Skeleton: + return ns_.analytics.PlatformAPIs.Skeleton; + case ComscorePlatformAPIs.WebBrowser: + return ns_.analytics.PlatformAPIs.WebBrowser; + case ComscorePlatformAPIs.SamsungTizenTV: + return ns_.analytics.PlatformAPIs.SamsungTizenTV; + default: + return ns_.analytics.PlatformAPIs.html5; + } +} diff --git a/comscore/src/comscore/ComScore.d.ts b/comscore/src/comscore/ComScore.d.ts index de717661..9af99abd 100644 --- a/comscore/src/comscore/ComScore.d.ts +++ b/comscore/src/comscore/ComScore.d.ts @@ -1,265 +1,264 @@ declare namespace ns_ { namespace analytics { - enum ConnectivityType { - UNKNOWN, - UNAVAILABLE, - DISCONNECTED, - CONNECTED, - ETHERNET, - WIFI, - WWAN, - BLUETOOTH, - EMULATOR - } - - enum PlatformAPIs { - SmartTV, - Netcast, - Cordova, - Trilithium, - AppleTV, - Chromecast, - Xbox, - webOS, - tvOS, - nodejs, - html5, - JSMAF, - Skeleton, - WebBrowser - } - - namespace PlatformApi { - function setPlatformAPI(platformApi: PlatformAPIs): void; - function setPlatformAPI(platformApi: PlatformAPIs, interfaceObject: unknown): void; - function setPlatformApi(platformApi: PlatformAPIs, interfaceObject: unknown): void; - } - - class StreamingAnalytics { - setMediaPlayerName(name: string): void; - setMediaPlayerVersion(version: string): void; - createPlaybackSession(): void - getPlaybackSessionId(): void - loopPlaybackSession(): void - notifyBufferStart(): void - notifyBufferStop(): void - notifyChangePlaybackRate(rate: number): void; - notifyEnd(): void - notifyPause(): void - notifyPlay(): void - notifySeekStart(): void - setDvrWindowLength(length: number): void; - setImplementationId(id: string): void; - setMetadata(metadata: any): void; - setProjectId(id: string): void; - startFromDvrWindowOffset(offset: number): void; - startFromPosition(position: number): void; - startFromSegment(segment: any): void; - constructor(); - } - - namespace StreamingAnalytics { - namespace AdvertisementMetadata { - export enum AdvertisementType { - ON_DEMAND_PRE_ROLL, - ON_DEMAND_MID_ROLL, - ON_DEMAND_POST_ROLL, - LIVE, - BRANDED_ON_DEMAND_PRE_ROLL, - BRANDED_ON_DEMAND_MID_ROLL, - BRANDED_ON_DEMAND_POST_ROLL, - BRANDED_AS_CONTENT, - BRANDED_DURING_LIVE, - OTHER, - } + enum ConnectivityType { + UNKNOWN, + UNAVAILABLE, + DISCONNECTED, + CONNECTED, + ETHERNET, + WIFI, + WWAN, + BLUETOOTH, + EMULATOR + } - export enum AdvertisementDeliveryType { - NATIONAL, - LOCAL, - SYNDICATION - } - } - - class AdvertisementMetadata { - addCustomLabels(labels: any): void; - classifyAsAudioStream(isAudio: boolean): void; - setCallToActionUrl(url: string): void; - setClipUrl(url: string): void; - setDeliveryType(type: StreamingAnalytics.AdvertisementMetadata.AdvertisementDeliveryType): void; - setLength(length: number): void; - setMediaType(type: StreamingAnalytics.AdvertisementMetadata.AdvertisementType): void; - setOwner(owner: string): void; - setPlacementId(id: string): void; - setRelatedContentMetadata(metadata: any): void; - setServer(server: string): void; - setServerCampaignId(id: string): void; - setSiteId(id: string): void; - setTitle(title: string): void; - setUniqueId(id: string): void; - setVideoDimensions(width: number, height: number): void; - - getMetadataLabels(): any; - - - constructor(); - } - - namespace ContentMetadata { - - export enum ContentDeliveryAdvertisementCapability { - NONE, - DYNAMIC_LOAD, - DYNAMIC_REPLACEMENT, - LINEAR_1DAY, - LINEAR_2DAY, - LINEAR_3DAY, - LINEAR_4DAY, - LINEAR_5DAY, - LINEAR_6DAY, - LINEAR_7DAY - } - - export enum ContentDeliveryComposition { - CLEAN, - EMBED - } - - export enum ContentDeliveryMode { - LINEAR, - ON_DEMAND - } - - export enum ContentDeliverySubscriptionType { - ADVERTISING, - PREMIUM, - SUBSCRIPTION, - TRADITIONAL_MVPD, - TRANSACTIONAL, - VIRTUAL_MVPD, - } - - export enum ContentDistributionModel { - EXCLUSIVELY_ONLINE, - TV_AND_ONLINE - } - - export enum ContentFeedType { - EAST_HD, - EAST_SD, - OTHER, - WEST_HD, - WEST_SD - } - - export enum ContentMediaFormat { - EXTRA_EPISODE, - EXTRA_GENERIC, - EXTRA_MOVIE, - FULL_CONTENT_EPISODE, - FULL_CONTENT_GENERIC, - FULL_CONTENT_MOVIE, - PARTIAL_CONTENT_EPISODE, - PARTIAL_CONTENT_GENERIC, - PARTIAL_CONTENT_MOVIE, - PREVIEW_EPISODE, - PREVIEW_GENERIC, - PREVIEW_MOVIE - } - - export enum ContentType { - LONG_FORM_ON_DEMAND, - SHORT_FORM_ON_DEMAND, - LIVE, - USER_GENERATED_SHORT_FORM_ON_DEMAND, - USER_GENERATED_LONG_FORM_ON_DEMAND, - USER_GENERATED_LIVE, - BUMPER, - OTHER, - } + enum PlatformAPIs { + SmartTV, + Netcast, + Cordova, + Trilithium, + AppleTV, + Chromecast, + Xbox, + webOS, + tvOS, + nodejs, + html5, + JSMAF, + Skeleton, + WebBrowser, + SamsungTizenTV + } + + namespace PlatformApi { + function setPlatformAPI(platformApi: PlatformAPIs): void; + function setPlatformAPI(platformApi: PlatformAPIs, interfaceObject: unknown): void; + function setPlatformApi(platformApi: PlatformAPIs, interfaceObject: unknown): void; } - - class ContentMetadata { - addCustomLabels(labels: any): void; - carryTvAdvertisementLoad(carriesTvAdvertisementLoad: boolean): void; - classifyAsAudioStream(audioStream: boolean): void; - classifyAsCompleteEpisode(completeEpisode: boolean): void; - setClipUrl(url: string): void; - setDateOfDigitalAiring(year: number, month: number, day: number): void; - setDateOfProduction(year: number, month: number, day: number): void; - setDateOfTvAiring(year: number, month: number, day: number): void; - setDeliveryAdvertisementCapability(value: StreamingAnalytics.ContentMetadata.ContentDeliveryAdvertisementCapability): void; - setDeliveryComposition(value: StreamingAnalytics.ContentMetadata.ContentDeliveryComposition): void; - setDeliveryMode(value: StreamingAnalytics.ContentMetadata.ContentDeliveryMode): void; - setDeliverySubscriptionType(value: StreamingAnalytics.ContentMetadata.ContentDeliverySubscriptionType): void; - setDictionaryClassificationC3(value: string): void; - setDictionaryClassificationC4(value: string): void; - setDictionaryClassificationC6(value: string): void; - setDistributionModel(value: StreamingAnalytics.ContentMetadata.ContentDistributionModel): void; - setEpisodeId(id: string): void; - setEpisodeNumber(episodeNumber: string): void; - setEpisodeSeasonNumber(seasonNumber: string): void; - setEpisodeTitle(title: string): void; - setFeedType(value: StreamingAnalytics.ContentMetadata.ContentFeedType): void; - setGenreId(id: string): void; - setGenreName(name: string): void; - setLength(length: number): void; - setMediaFormat(value: StreamingAnalytics.ContentMetadata.ContentMediaFormat): void; - setMediaType(value: StreamingAnalytics.ContentMetadata.ContentType): void; - setNetworkAffiliate(code: string): void; - setPlaylistTitle(title: string): void; - setProgramId(id: string): void; - setProgramTitle(title: string): void; - setPublisherName(name: string): void; - setStationCode(code: string): void; - setStationTitle(title: string): void; - setTimeOfDigitalAiring(hours: number, minutes: number): void; - setTimeOfProduction(hours: number, minutes: number): void; - setTimeOfTvAiring(hours: number, minutes: number): void; - setTotalSegments(total: number): void; - setUniqueId(id: string): void; - setVideoDimensions(width: number, height: number): void; - - getMetadataLabels(): any; - + + class StreamingAnalytics { + setMediaPlayerName(name: string): void; + setMediaPlayerVersion(version: string): void; + createPlaybackSession(): void; + getPlaybackSessionId(): void; + loopPlaybackSession(): void; + notifyBufferStart(): void; + notifyBufferStop(): void; + notifyChangePlaybackRate(rate: number): void; + notifyEnd(): void; + notifyPause(): void; + notifyPlay(): void; + notifySeekStart(): void; + setDvrWindowLength(length: number): void; + setImplementationId(id: string): void; + setMetadata(metadata: any): void; + setProjectId(id: string): void; + startFromDvrWindowOffset(offset: number): void; + startFromPosition(position: number): void; + startFromSegment(segment: any): void; constructor(); + } + + namespace StreamingAnalytics { + namespace AdvertisementMetadata { + export enum AdvertisementType { + ON_DEMAND_PRE_ROLL, + ON_DEMAND_MID_ROLL, + ON_DEMAND_POST_ROLL, + LIVE, + BRANDED_ON_DEMAND_PRE_ROLL, + BRANDED_ON_DEMAND_MID_ROLL, + BRANDED_ON_DEMAND_POST_ROLL, + BRANDED_AS_CONTENT, + BRANDED_DURING_LIVE, + OTHER + } + + export enum AdvertisementDeliveryType { + NATIONAL, + LOCAL, + SYNDICATION + } + } + + class AdvertisementMetadata { + addCustomLabels(labels: any): void; + classifyAsAudioStream(isAudio: boolean): void; + setCallToActionUrl(url: string): void; + setClipUrl(url: string): void; + setDeliveryType(type: StreamingAnalytics.AdvertisementMetadata.AdvertisementDeliveryType): void; + setLength(length: number): void; + setMediaType(type: StreamingAnalytics.AdvertisementMetadata.AdvertisementType): void; + setOwner(owner: string): void; + setPlacementId(id: string): void; + setRelatedContentMetadata(metadata: any): void; + setServer(server: string): void; + setServerCampaignId(id: string): void; + setSiteId(id: string): void; + setTitle(title: string): void; + setUniqueId(id: string): void; + setVideoDimensions(width: number, height: number): void; + + getMetadataLabels(): any; + + constructor(); + } + + namespace ContentMetadata { + export enum ContentDeliveryAdvertisementCapability { + NONE, + DYNAMIC_LOAD, + DYNAMIC_REPLACEMENT, + LINEAR_1DAY, + LINEAR_2DAY, + LINEAR_3DAY, + LINEAR_4DAY, + LINEAR_5DAY, + LINEAR_6DAY, + LINEAR_7DAY + } + + export enum ContentDeliveryComposition { + CLEAN, + EMBED + } + + export enum ContentDeliveryMode { + LINEAR, + ON_DEMAND + } + + export enum ContentDeliverySubscriptionType { + ADVERTISING, + PREMIUM, + SUBSCRIPTION, + TRADITIONAL_MVPD, + TRANSACTIONAL, + VIRTUAL_MVPD + } + + export enum ContentDistributionModel { + EXCLUSIVELY_ONLINE, + TV_AND_ONLINE + } + + export enum ContentFeedType { + EAST_HD, + EAST_SD, + OTHER, + WEST_HD, + WEST_SD + } + export enum ContentMediaFormat { + EXTRA_EPISODE, + EXTRA_GENERIC, + EXTRA_MOVIE, + FULL_CONTENT_EPISODE, + FULL_CONTENT_GENERIC, + FULL_CONTENT_MOVIE, + PARTIAL_CONTENT_EPISODE, + PARTIAL_CONTENT_GENERIC, + PARTIAL_CONTENT_MOVIE, + PREVIEW_EPISODE, + PREVIEW_GENERIC, + PREVIEW_MOVIE + } + + export enum ContentType { + LONG_FORM_ON_DEMAND, + SHORT_FORM_ON_DEMAND, + LIVE, + USER_GENERATED_SHORT_FORM_ON_DEMAND, + USER_GENERATED_LONG_FORM_ON_DEMAND, + USER_GENERATED_LIVE, + BUMPER, + OTHER + } + } + + class ContentMetadata { + addCustomLabels(labels: any): void; + carryTvAdvertisementLoad(carriesTvAdvertisementLoad: boolean): void; + classifyAsAudioStream(audioStream: boolean): void; + classifyAsCompleteEpisode(completeEpisode: boolean): void; + setClipUrl(url: string): void; + setDateOfDigitalAiring(year: number, month: number, day: number): void; + setDateOfProduction(year: number, month: number, day: number): void; + setDateOfTvAiring(year: number, month: number, day: number): void; + setDeliveryAdvertisementCapability( + value: StreamingAnalytics.ContentMetadata.ContentDeliveryAdvertisementCapability + ): void; + setDeliveryComposition(value: StreamingAnalytics.ContentMetadata.ContentDeliveryComposition): void; + setDeliveryMode(value: StreamingAnalytics.ContentMetadata.ContentDeliveryMode): void; + setDeliverySubscriptionType( + value: StreamingAnalytics.ContentMetadata.ContentDeliverySubscriptionType + ): void; + setDictionaryClassificationC3(value: string): void; + setDictionaryClassificationC4(value: string): void; + setDictionaryClassificationC6(value: string): void; + setDistributionModel(value: StreamingAnalytics.ContentMetadata.ContentDistributionModel): void; + setEpisodeId(id: string): void; + setEpisodeNumber(episodeNumber: string): void; + setEpisodeSeasonNumber(seasonNumber: string): void; + setEpisodeTitle(title: string): void; + setFeedType(value: StreamingAnalytics.ContentMetadata.ContentFeedType): void; + setGenreId(id: string): void; + setGenreName(name: string): void; + setLength(length: number): void; + setMediaFormat(value: StreamingAnalytics.ContentMetadata.ContentMediaFormat): void; + setMediaType(value: StreamingAnalytics.ContentMetadata.ContentType): void; + setNetworkAffiliate(code: string): void; + setPlaylistTitle(title: string): void; + setProgramId(id: string): void; + setProgramTitle(title: string): void; + setPublisherName(name: string): void; + setStationCode(code: string): void; + setStationTitle(title: string): void; + setTimeOfDigitalAiring(hours: number, minutes: number): void; + setTimeOfProduction(hours: number, minutes: number): void; + setTimeOfTvAiring(hours: number, minutes: number): void; + setTotalSegments(total: number): void; + setUniqueId(id: string): void; + setVideoDimensions(width: number, height: number): void; + + getMetadataLabels(): any; + + constructor(); + } } - } - - namespace configuration { - function setApplicationName(name: string): void; - - function setApplicationVersion(name: string): void; - - function addClient(config: PublisherConfiguration): void; - - function getPublisherConfiguration(id: string): any; - - function setPersistentLabel(name: string, value: any): void; - - function addPersistentLabels(labels: any): void; - - function enableImplementationValidationMode(): void; - - function enableChildDirectedApplicationMode(): void; - - class PublisherConfiguration { - publisherId: string; - constructor({ }: any) + namespace configuration { + function setApplicationName(name: string): void; + + function setApplicationVersion(name: string): void; + + function addClient(config: PublisherConfiguration): void; + + function getPublisherConfiguration(id: string): any; + + function setPersistentLabel(name: string, value: any): void; + + function addPersistentLabels(labels: any): void; + + function enableImplementationValidationMode(): void; + + function enableChildDirectedApplicationMode(): void; + + class PublisherConfiguration { + publisherId: string; + constructor({}: any); + } } - } - - function notifyHiddenEvent(): void; - - function notifyEnterForeground(): void; - - function notifyExitForeground(): void; - - function close(): void; - - function start(): void; + function notifyHiddenEvent(): void; + + function notifyEnterForeground(): void; + + function notifyExitForeground(): void; + + function close(): void; + + function start(): void; } - } - \ No newline at end of file +} diff --git a/comscore/src/integration/ComscoreStreamingAnalyticsTHEOIntegration.ts b/comscore/src/integration/ComscoreStreamingAnalyticsTHEOIntegration.ts index 352905db..ccfaf1bf 100644 --- a/comscore/src/integration/ComscoreStreamingAnalyticsTHEOIntegration.ts +++ b/comscore/src/integration/ComscoreStreamingAnalyticsTHEOIntegration.ts @@ -18,6 +18,7 @@ import { import { ComscoreConfiguration } from '../api/ComscoreConfiguration'; import { ComscoreMetadata } from '../api/ComscoreMetadata'; import { buildContentMetadata } from './ComscoreContentMetadata'; +import { toMilliSeconds } from './Utils'; const LOG_STATE_CHANGES = true; const LOG_THEOPLAYER_EVENTS = true; @@ -56,6 +57,10 @@ export class ComscoreStreamingAnalyticsTHEOIntegration { // Copy of main content's ContentMetadata private contentMetadata: ns_.analytics.StreamingAnalytics.ContentMetadata | null = null; + // Main content related fields for use outside of event handlers + private dvrWindowLengthMs: number | undefined = undefined; + private dvrWindowOffsetMs: number | undefined = undefined; + // Advertisement related fields for use outside of ad event handlers private inAd: boolean = false; private lastAdId: string | undefined = undefined; @@ -98,6 +103,7 @@ export class ComscoreStreamingAnalyticsTHEOIntegration { if (this.player.ads) { this.player.ads.addEventListener('adbegin', this.onAdBegin); + this.player.ads.addEventListener('adend', this.onAdEnd); this.player.ads.addEventListener('adbreakend', this.onAdBreakEnd); } } @@ -117,6 +123,7 @@ export class ComscoreStreamingAnalyticsTHEOIntegration { if (this.player.ads) { this.player.ads.removeEventListener('adbegin', this.onAdBegin); + this.player.ads.removeEventListener('adend', this.onAdEnd); this.player.ads.removeEventListener('adbreakend', this.onAdBreakEnd); } } @@ -150,7 +157,9 @@ export class ComscoreStreamingAnalyticsTHEOIntegration { adMetadata.setUniqueId(adId); adMetadata.setLength(adDuration); - if (!this.contentMetadata) buildContentMetadata(this.metadata); + if (!this.contentMetadata) { + this.contentMetadata = buildContentMetadata(this.metadata); + } adMetadata.setRelatedContentMetadata(this.contentMetadata); if (this.configuration.debug && LOG_STREAMINGANALYTICS) { console.log(`[COMSCORE - StreamingAnalytics] setMetadata (advertisement)`, adMetadata.getMetadataLabels()); @@ -168,6 +177,7 @@ export class ComscoreStreamingAnalyticsTHEOIntegration { console.log(`[COMSCORE - STATE] State change ${this.state} -> VIDEO`); this.state = ComscoreState.VIDEO; this.setContentMetadata(); + this.maybeReportDvrFields(); break; case ComscoreState.ADVERTISEMENT: case ComscoreState.ADVERTISEMENT_PAUSED: @@ -177,6 +187,7 @@ export class ComscoreStreamingAnalyticsTHEOIntegration { console.log(`[COMSCORE - STATE] State change ${this.state} -> VIDEO`); this.state = ComscoreState.VIDEO; this.setContentMetadata(); + this.maybeReportDvrFields(); break; case ComscoreState.VIDEO_PAUSED: if (this.configuration.debug && LOG_STATE_CHANGES) @@ -283,6 +294,8 @@ export class ComscoreStreamingAnalyticsTHEOIntegration { console.log(`[COMSCORE - THEOplayer EVENTS] ${event.type} event`); this.state = ComscoreState.INITIALIZED; this.contentMetadata = null; + this.dvrWindowLengthMs = undefined; + this.dvrWindowOffsetMs = undefined; this.streamingAnalytics.createPlaybackSession(); if (this.configuration.debug && LOG_STREAMINGANALYTICS) console.log(`[COMSCORE - StreamingAnalytics] createPlaybackSession`); @@ -365,11 +378,9 @@ export class ComscoreStreamingAnalyticsTHEOIntegration { const dvrWindowStart = seekable.start(0); const dvrWindowLengthInSeconds = dvrWindowEnd - dvrWindowStart; if (dvrWindowLengthInSeconds) { - this.streamingAnalytics.setDvrWindowLength(dvrWindowLengthInSeconds * 1000); - if (this.configuration.debug && LOG_STREAMINGANALYTICS) - console.log( - `[COMSCORE - StreamingAnalytics] setDvrWindowLength ${dvrWindowLengthInSeconds * 1000}` - ); + this.dvrWindowLengthMs = toMilliSeconds(dvrWindowLengthInSeconds); + if (this.state === ComscoreState.VIDEO || this.state === ComscoreState.VIDEO_PAUSED) + this.streamingAnalytics.setDvrWindowLength(this.dvrWindowLengthMs); } else if (this.configuration.debug) console.log(`[COMSCORE] DVR window length was not > 0`); } } catch (error) { @@ -407,16 +418,18 @@ export class ComscoreStreamingAnalyticsTHEOIntegration { } const dvrWindowEnd = seekable.end(seekable.length - 1); const dvrWindowOffsetInSeconds = dvrWindowEnd - currentTime; - this.streamingAnalytics.startFromDvrWindowOffset(dvrWindowOffsetInSeconds * 1000); - if (this.configuration.debug && LOG_STREAMINGANALYTICS) - console.log( - `[COMSCORE - StreamingAnalytics] startFromDvrWindowOffset ${dvrWindowOffsetInSeconds * 1000}` - ); + this.dvrWindowOffsetMs = toMilliSeconds(dvrWindowOffsetInSeconds); + if (this.state === ComscoreState.VIDEO || this.state === ComscoreState.VIDEO_PAUSED) { + this.streamingAnalytics.startFromDvrWindowOffset(this.dvrWindowOffsetMs); + if (this.configuration.debug && LOG_STREAMINGANALYTICS) + console.log(`[COMSCORE - StreamingAnalytics] startFromDvrWindowOffset ${this.dvrWindowOffsetMs}`); + } } else { if (this.configuration.debug) console.log(`[COMSCORE] seeked in a VOD stream`); - this.streamingAnalytics.startFromPosition(currentTime * 1000); + const currentTimeInMilliSeconds = toMilliSeconds(currentTime); + this.streamingAnalytics.startFromPosition(currentTimeInMilliSeconds); if (this.configuration.debug && LOG_STREAMINGANALYTICS) - console.log(`[COMSCORE - StreamingAnalytics] startFromPosition ${currentTime * 1000}`); + console.log(`[COMSCORE - StreamingAnalytics] startFromPosition ${currentTimeInMilliSeconds}`); } }; @@ -439,7 +452,7 @@ export class ComscoreStreamingAnalyticsTHEOIntegration { ad.adBreak.integration ?? '' ); this.lastAdId = adIdProcessor ? adIdProcessor(ad) : ad.id; - this.lastAdDuration = ad.duration; + this.lastAdDuration = toMilliSeconds(ad.duration ?? 0); if (!this.lastAdDuration && this.configuration.debug) { console.log('[COMSCORE] AD_BEGIN event with an ad duration of 0 found. Please check the ad configuration'); } @@ -454,6 +467,12 @@ export class ComscoreStreamingAnalyticsTHEOIntegration { } }; + private onAdEnd = (event: AdEvent<'adend'>) => { + if (this.configuration.debug && LOG_THEOPLAYER_EVENTS) + console.log(`[COMSCORE - THEOplayer EVENTS] ${event.type} event`); + this.transitionToStopped(); + }; + private onAdBreakEnd = (event: AdBreakEvent<'adbreakend'>) => { if (this.configuration.debug && LOG_THEOPLAYER_EVENTS) console.log(`[COMSCORE - THEOplayer EVENTS] ${event.type} event`); @@ -482,6 +501,17 @@ export class ComscoreStreamingAnalyticsTHEOIntegration { } }; + private maybeReportDvrFields = () => { + if (this.dvrWindowLengthMs) { + this.streamingAnalytics.setDvrWindowLength(this.dvrWindowLengthMs); + if (this.configuration.debug && LOG_STREAMINGANALYTICS) + console.log(`[COMSCORE - StreamingAnalytics] setDvrWindowLength ${this.dvrWindowLengthMs}`); + this.streamingAnalytics.startFromDvrWindowOffset(this.dvrWindowOffsetMs ?? 0); + if (this.configuration.debug && LOG_STREAMINGANALYTICS) + console.log(`[COMSCORE - StreamingAnalytics] startFromDvrWindowOffset ${this.dvrWindowOffsetMs}`); + } + }; + private findAdBreakType = (offset: number, maxDuration: number, integration: string): AdBreakType => { if (offset === 0) { if (this.configuration.debug) console.log('[COMSCORE] Mark as PRE_ROLL'); @@ -512,8 +542,16 @@ export class ComscoreStreamingAnalyticsTHEOIntegration { } }; - private isBeforePreRoll = (): boolean => - this.player.ads?.scheduledAdBreaks.length ? this.player.ads?.scheduledAdBreaks[0].timeOffset === 0 : false; + private isBeforePreRoll = (): boolean => { + if (!this.player.ads) return false; + const hasScheduledAdBreaks = this.player.ads?.scheduledAdBreaks.length !== 0; + const firstScheduledAdBreakIsPreroll = + hasScheduledAdBreaks && this.player.ads?.scheduledAdBreaks[0].timeOffset === 0; + const hasScheduledPrerollWithAds = + firstScheduledAdBreakIsPreroll && this.player.ads?.scheduledAdBreaks[0].ads?.length !== 0; + return hasScheduledPrerollWithAds; + }; + // private isAfterPostRoll = () => this.lastAdBreakOffset && this.lastAdBreakOffset < 0 && this.player.duration - this.player.currentTime < 1 private isAfterPostRoll = (): boolean => this.lastAdBreakType ? this.lastAdBreakType === AdBreakType.POST_ROLL : false; diff --git a/comscore/src/integration/Utils.ts b/comscore/src/integration/Utils.ts new file mode 100644 index 00000000..0f059459 --- /dev/null +++ b/comscore/src/integration/Utils.ts @@ -0,0 +1,3 @@ +export const toMilliSeconds = (seconds: number) => { + return Math.round(seconds * 1000) +} \ No newline at end of file diff --git a/comscore/test/pages/main_esm.html b/comscore/test/pages/main_esm.html index 6f481529..3775d96e 100644 --- a/comscore/test/pages/main_esm.html +++ b/comscore/test/pages/main_esm.html @@ -58,6 +58,7 @@ applicationName: 'Test App', userConsent: "1", adIdProcessor: (ad) => ad.id, + platformApi: ns_.analytics.PlatformAPIs.WebBrowser, debug: true };