diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..eade0fa6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,54 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [1.1.4] - 2023-04-26 + +### Fixed + +- Fixed an issue where a session could be created without a source. + +## [1.1.3] - 2023-04-20 + +### Changed + +- Made THEOplayer an external dependency. + +## [1.1.2] - 2023-04-14 + +### Fixed + +- Fixed passing content length for a live stream or on early error. + +## [1.1.1] - 2023-04-07 + +### Changed + +- Updated THEOplayer version to 5.X. + +## [1.1.0] - 2023-03-30 + +### Added + +- Added `setContentInfo` to pass video metadata during playback. +- Added `setAdInfo` to pass ad metadata during playback. +- Added `reportPlaybackFailed` to notify Conviva of non-video errors. +- Added `stopAndStartNewSession` to enable explicitly stopping the current session and starting a new one. +- Added visibility change reporting. + +### Changed + +- Updated THEOplayer version to 4.X. +- Improved error handling. +- Improved default metadata. + +### Fixed + +- Fixed handling a replay of the same source. + +## [1.0.0] - 2022-08-03 + +Initial release diff --git a/package-lock.json b/package-lock.json index d298b687..d81adc56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@theoplayer/conviva-connector-web", - "version": "1.0.0", + "version": "1.1.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@theoplayer/conviva-connector-web", - "version": "1.0.0", + "version": "1.1.4", "license": "MIT", "dependencies": { - "@convivainc/conviva-js-coresdk": "^4.5.8" + "@convivainc/conviva-js-coresdk": "^4.6.1" }, "devDependencies": { "@rollup/plugin-commonjs": "^22.0.1", @@ -32,7 +32,7 @@ "rollup": "^2.33.2", "rollup-plugin-dts": "^4.2.2", "rollup-plugin-terser": "^7.0.2", - "theoplayer": "^3.5.0", + "theoplayer": "^5.0.0", "ts-jest": "^28.0.5", "ts-node": "^10.8.2", "tslib": "^2.4.0", @@ -40,7 +40,7 @@ }, "peerDependencies": { "@theoplayer/yospace-connector-web": "1.2.0", - "theoplayer": "^3.5.0" + "theoplayer": "^5.0.0" }, "peerDependenciesMeta": { "@theoplayer/yospace-connector-web": { @@ -629,9 +629,9 @@ "dev": true }, "node_modules/@convivainc/conviva-js-coresdk": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/@convivainc/conviva-js-coresdk/-/conviva-js-coresdk-4.5.8.tgz", - "integrity": "sha512-MwezQbE0fqxUkQfChn0KyDojYfaACGEuw76KnY1lVXaxiQWOBRGMFHa1LgADiJd1Du+UAMGuMn7exPTPTozu/g==" + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@convivainc/conviva-js-coresdk/-/conviva-js-coresdk-4.6.1.tgz", + "integrity": "sha512-JFaSWexcuRd6Po/KEKduPIwqB7/YoLo6XkYYuYiVjJOhSQ2gb1gJRbvH944DjfTpl7/1PcUND0vAYkowkVfIIA==" }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", @@ -6000,9 +6000,9 @@ "dev": true }, "node_modules/theoplayer": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/theoplayer/-/theoplayer-3.6.1.tgz", - "integrity": "sha512-e10mBiIgsQBxN6bxIEOT83NC2KKSZiy9UOwqhHvipOBMruaBpfsJlzBrTj9LgqrZ9kc3nGzXqUjLJ3W5oxzi0g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/theoplayer/-/theoplayer-5.0.0.tgz", + "integrity": "sha512-T+Q70i+EieIfNuA72GqdfMnKf1mjGr+EVBw1Kx5ZGLeNhBRjHJhRyIJBRng/3GrgFld2X1B4v3I4JsWvBV9LKw==", "dev": true }, "node_modules/tiny-glob": { @@ -6933,9 +6933,9 @@ "dev": true }, "@convivainc/conviva-js-coresdk": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/@convivainc/conviva-js-coresdk/-/conviva-js-coresdk-4.5.8.tgz", - "integrity": "sha512-MwezQbE0fqxUkQfChn0KyDojYfaACGEuw76KnY1lVXaxiQWOBRGMFHa1LgADiJd1Du+UAMGuMn7exPTPTozu/g==" + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@convivainc/conviva-js-coresdk/-/conviva-js-coresdk-4.6.1.tgz", + "integrity": "sha512-JFaSWexcuRd6Po/KEKduPIwqB7/YoLo6XkYYuYiVjJOhSQ2gb1gJRbvH944DjfTpl7/1PcUND0vAYkowkVfIIA==" }, "@cspotcode/source-map-support": { "version": "0.8.1", @@ -10939,9 +10939,9 @@ "dev": true }, "theoplayer": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/theoplayer/-/theoplayer-3.6.1.tgz", - "integrity": "sha512-e10mBiIgsQBxN6bxIEOT83NC2KKSZiy9UOwqhHvipOBMruaBpfsJlzBrTj9LgqrZ9kc3nGzXqUjLJ3W5oxzi0g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/theoplayer/-/theoplayer-5.0.0.tgz", + "integrity": "sha512-T+Q70i+EieIfNuA72GqdfMnKf1mjGr+EVBw1Kx5ZGLeNhBRjHJhRyIJBRng/3GrgFld2X1B4v3I4JsWvBV9LKw==", "dev": true }, "tiny-glob": { diff --git a/package.json b/package.json index aef84abe..468f0b2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@theoplayer/conviva-connector-web", - "version": "1.0.0", + "version": "1.1.4", "description": "A connector implementing Conviva for web.", "main": "dist/conviva-connector.umd.js", "repository": "https://github.com/THEOplayer/conviva-connector-web", @@ -32,6 +32,7 @@ "license": "MIT", "files": [ "dist/", + "CHANGELOG.md", "README.md", "package.json" ], @@ -56,14 +57,14 @@ "rollup": "^2.33.2", "rollup-plugin-dts": "^4.2.2", "rollup-plugin-terser": "^7.0.2", - "theoplayer": "^3.5.0", + "theoplayer": "^5.0.0", "ts-jest": "^28.0.5", "ts-node": "^10.8.2", "tslib": "^2.4.0", "typescript": "^4.0.5" }, "peerDependencies": { - "theoplayer": "^3.5.0", + "theoplayer": "^5.0.0", "@theoplayer/yospace-connector-web": "1.2.0" }, "peerDependenciesMeta": { @@ -72,6 +73,6 @@ } }, "dependencies": { - "@convivainc/conviva-js-coresdk": "^4.5.8" + "@convivainc/conviva-js-coresdk": "^4.6.1" } } diff --git a/rollup.config.js b/rollup.config.js index dce1491b..321d85d8 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -35,8 +35,9 @@ const options = [ format: 'esm', indent: false, banner - } + }, ], + external: ['theoplayer'], plugins: [ nodeResolve({ extensions: ['.ts', '.js'] @@ -63,6 +64,7 @@ const options = [ footer: `export as namespace ${globalName};` } ], + external: ['theoplayer'], plugins: [ dts({ tsconfig: 'tsconfig.json' diff --git a/src/integration/ConvivaConnector.ts b/src/integration/ConvivaConnector.ts index e990b9b5..72ccbbc3 100644 --- a/src/integration/ConvivaConnector.ts +++ b/src/integration/ConvivaConnector.ts @@ -4,12 +4,9 @@ import { YospaceConnector } from '@theoplayer/yospace-connector-web'; import { ConvivaConfiguration, ConvivaHandler } from './ConvivaHandler'; export class ConvivaConnector { - private player: ChromelessPlayer; - private convivaHandler: ConvivaHandler; constructor(player: ChromelessPlayer, convivaMetadata: ConvivaMetadata, convivaConfig: ConvivaConfiguration) { - this.player = player; this.convivaHandler = new ConvivaHandler(player, convivaMetadata, convivaConfig); } @@ -17,10 +14,47 @@ export class ConvivaConnector { * Optionally connects the ConvivaConnector to the YospaceConnector to report SSAI. * @param connector the YospaceConnector */ - connect(connector: YospaceConnector) { + connect(connector: YospaceConnector): void { this.convivaHandler.connect(connector); } + /** + * Sets Conviva metadata on the Conviva video analytics. + * @param metadata object of key value pairs + */ + setContentInfo(metadata: ConvivaMetadata): void { + this.convivaHandler.setContentInfo(metadata); + } + + /** + * Sets Conviva metadata on the Conviva ad analytics. + * @param metadata object of key value pairs + */ + setAdInfo(metadata: ConvivaMetadata): void { + this.convivaHandler.setAdInfo(metadata); + } + + /** + * Reports an error to the Conviva session and closes the session. + * @param errorMessage string explaining what the error is. + */ + reportPlaybackFailed(errorMessage: string): void { + this.convivaHandler.reportPlaybackFailed(errorMessage); + } + + /** + * Explicitly stop the current session and start a new one. + * + * This can be used to manually mark the start of a new session during a live stream, + * for example when a new program starts. + * By default, new sessions are only started on play-out of a new source, or for an ad break. + * + * @param metadata object of key value pairs. + */ + stopAndStartNewSession(metadata: ConvivaMetadata): void { + this.convivaHandler.stopAndStartNewSession(metadata); + } + /** * Stops video and ad analytics and closes all sessions. */ diff --git a/src/integration/ConvivaHandler.ts b/src/integration/ConvivaHandler.ts index 21d4cb8a..e9fe8e5d 100644 --- a/src/integration/ConvivaHandler.ts +++ b/src/integration/ConvivaHandler.ts @@ -23,20 +23,24 @@ export class ConvivaHandler { private readonly player: ChromelessPlayer; private readonly convivaMetadata: ConvivaMetadata; private readonly convivaConfig: ConvivaConfiguration; + private customMetadata: ConvivaMetadata = {}; - private readonly convivaVideoAnalytics: VideoAnalytics; - private readonly convivaAdAnalytics: AdAnalytics; + private convivaVideoAnalytics: VideoAnalytics | undefined; + private convivaAdAnalytics: AdAnalytics | undefined; - private readonly adReporter: CsaiAdReporter | undefined; + private adReporter: CsaiAdReporter | undefined; private yospaceAdReporter: YospaceAdReporter | undefined; - private readonly verizonAdReporter: VerizonAdReporter | undefined; + private verizonAdReporter: VerizonAdReporter | undefined; private currentSource: SourceDescription | undefined; private playbackRequested: boolean = false; + private yospaceConnector: YospaceConnector | undefined; + constructor(player: ChromelessPlayer, convivaMetaData: ConvivaMetadata, config: ConvivaConfiguration) { this.player = player; this.convivaMetadata = convivaMetaData; + this.customMetadata = convivaMetaData; this.convivaConfig = config; this.currentSource = player.source; @@ -46,44 +50,82 @@ export class ConvivaHandler { CONVIVA_CALLBACK_FUNCTIONS, calculateConvivaOptions(this.convivaConfig) ); - // This object will be used throughout the entire application lifecycle to report video related events. + + this.addEventListeners(); + } + + private initializeSession(): void { this.convivaVideoAnalytics = Analytics.buildVideoAnalytics(); this.convivaVideoAnalytics.setPlayerInfo(collectPlayerInfo()); this.convivaVideoAnalytics.setCallback(this.convivaCallback); - // This object will be used throughout the entire application lifecycle to report ad related events. this.convivaAdAnalytics = Analytics.buildAdAnalytics(this.convivaVideoAnalytics); - if (player.ads !== undefined) { - this.adReporter = new CsaiAdReporter( - this.player, - this.convivaVideoAnalytics, - this.convivaAdAnalytics, - this.convivaMetadata - ); + if (this.player.ads !== undefined) { + this.adReporter = new CsaiAdReporter(this.player, this.convivaVideoAnalytics, this.convivaAdAnalytics); } - if (player.verizonMedia !== undefined) { + if (this.player.verizonMedia !== undefined) { this.verizonAdReporter = new VerizonAdReporter( this.player, this.convivaVideoAnalytics, - this.convivaAdAnalytics, - this.convivaMetadata + this.convivaAdAnalytics ); } - this.addEventListeners(); + if (this.yospaceConnector !== undefined) { + this.yospaceAdReporter = new YospaceAdReporter( + this.player, + this.convivaVideoAnalytics!, + this.convivaAdAnalytics!, + this.yospaceConnector + ); + } } - connect(connector: YospaceConnector) { + connect(connector: YospaceConnector): void { + if (!this.convivaVideoAnalytics) { + this.initializeSession(); + } this.yospaceAdReporter?.destroy(); this.yospaceAdReporter = new YospaceAdReporter( this.player, - this.convivaVideoAnalytics, - this.convivaAdAnalytics, - this.convivaMetadata, + this.convivaVideoAnalytics!, + this.convivaAdAnalytics!, connector ); + this.yospaceConnector = connector; + } + + setContentInfo(metadata: ConvivaMetadata): void { + if (!this.convivaVideoAnalytics) { + this.initializeSession(); + } + this.customMetadata = { ...this.customMetadata, ...metadata }; + this.convivaVideoAnalytics!.setContentInfo(metadata); + } + + setAdInfo(metadata: ConvivaMetadata): void { + if (!this.convivaVideoAnalytics) { + this.initializeSession(); + } + this.convivaAdAnalytics!.setAdInfo(metadata); + } + + reportPlaybackFailed(errorMessage: string): void { + this.convivaVideoAnalytics?.reportPlaybackFailed(errorMessage); + this.releaseSession(); + } + + stopAndStartNewSession(metadata: ConvivaMetadata): void { + this.maybeReportPlaybackEnded(); + this.maybeReportPlaybackRequested(); + this.setContentInfo(metadata); + if (this.player.paused) { + this.onPause(); + } else { + this.onPlaying(); + } } private addEventListeners(): void { @@ -102,6 +144,9 @@ export class ConvivaHandler { this.player.addEventListener('destroy', this.onDestroy); this.player.network.addEventListener('offline', this.onNetworkOffline); + + document.addEventListener('visibilitychange', this.onVisibilityChange); + window.addEventListener('beforeunload', this.onBeforeUnload); } private removeEventListeners(): void { @@ -120,16 +165,19 @@ export class ConvivaHandler { this.player.removeEventListener('destroy', this.onDestroy); this.player.network.removeEventListener('offline', this.onNetworkOffline); + + document.removeEventListener('visibilitychange', this.onVisibilityChange); + window.removeEventListener('beforeunload', this.onBeforeUnload); } private convivaCallback = () => { const currentTime = this.player.currentTime * 1000; - this.convivaVideoAnalytics.reportPlaybackMetric(Constants.Playback.PLAY_HEAD_TIME, currentTime); - this.convivaVideoAnalytics.reportPlaybackMetric( + this.convivaVideoAnalytics!.reportPlaybackMetric(Constants.Playback.PLAY_HEAD_TIME, currentTime); + this.convivaVideoAnalytics!.reportPlaybackMetric( Constants.Playback.BUFFER_LENGTH, calculateBufferLength(this.player) ); - this.convivaVideoAnalytics.reportPlaybackMetric( + this.convivaVideoAnalytics!.reportPlaybackMetric( Constants.Playback.RESOLUTION, this.player.videoWidth, this.player.videoHeight @@ -137,10 +185,13 @@ export class ConvivaHandler { const activeVideoTrack = this.player.videoTracks[0]; const activeQuality = activeVideoTrack?.activeQuality; if (activeQuality) { - this.convivaVideoAnalytics.reportPlaybackMetric(Constants.Playback.BITRATE, activeQuality.bandwidth / 1000); const frameRate = (activeQuality as VideoQuality).frameRate; + this.convivaVideoAnalytics!.reportPlaybackMetric( + Constants.Playback.BITRATE, + activeQuality.bandwidth / 1000 + ); if (frameRate) { - this.convivaVideoAnalytics.reportPlaybackMetric(Constants.Playback.RENDERED_FRAMERATE, frameRate); + this.convivaVideoAnalytics!.reportPlaybackMetric(Constants.Playback.RENDERED_FRAMERATE, frameRate); } } }; @@ -150,82 +201,123 @@ export class ConvivaHandler { }; private maybeReportPlaybackRequested() { - if (!this.playbackRequested) { + if (!this.playbackRequested && this.player.source !== undefined) { this.playbackRequested = true; - this.convivaVideoAnalytics.reportPlaybackRequested( + if (!this.convivaVideoAnalytics) { + this.initializeSession(); + } + this.convivaVideoAnalytics!.reportPlaybackRequested( collectContentMetadata(this.player, this.convivaMetadata) ); + this.reportMetadata(); } } private maybeReportPlaybackEnded() { if (this.playbackRequested) { - this.convivaVideoAnalytics.reportPlaybackEnded(); + this.convivaVideoAnalytics?.reportPlaybackEnded(); + this.releaseSession(); this.playbackRequested = false; } } + private reportMetadata() { + const src = this.player.src ?? ''; + const streamType = this.player.duration === Infinity ? Constants.StreamType.LIVE : Constants.StreamType.VOD; + const assetName = this.customMetadata[Constants.ASSET_NAME] ?? this.currentSource?.metadata?.title ?? 'NA'; + const playerName = this.customMetadata[Constants.PLAYER_NAME] ?? 'THEOplayer'; + const metadata = { + [Constants.STREAM_URL]: src, + [Constants.IS_LIVE]: streamType, + [Constants.ASSET_NAME]: assetName, + [Constants.PLAYER_NAME]: playerName + }; + this.setContentInfo(metadata); + } + private readonly onPlaying = () => { - this.convivaVideoAnalytics.reportPlaybackMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PLAYING); + this.convivaVideoAnalytics?.reportPlaybackMetric( + Constants.Playback.PLAYER_STATE, + Constants.PlayerState.PLAYING + ); }; private readonly onPause = () => { - this.convivaVideoAnalytics.reportPlaybackMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PAUSED); + this.convivaVideoAnalytics?.reportPlaybackMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PAUSED); }; private readonly onEmptied = () => { - this.convivaVideoAnalytics.reportPlaybackMetric( + this.convivaVideoAnalytics?.reportPlaybackMetric( Constants.Playback.PLAYER_STATE, Constants.PlayerState.BUFFERING ); }; private readonly onWaiting = () => { - this.convivaVideoAnalytics.reportPlaybackMetric( + this.convivaVideoAnalytics?.reportPlaybackMetric( Constants.Playback.PLAYER_STATE, Constants.PlayerState.BUFFERING ); }; private readonly onSeeking = () => { - this.convivaVideoAnalytics.reportPlaybackMetric(Constants.Playback.SEEK_STARTED); + this.convivaVideoAnalytics?.reportPlaybackMetric(Constants.Playback.SEEK_STARTED); }; private readonly onSeeked = () => { - this.convivaVideoAnalytics.reportPlaybackMetric(Constants.Playback.SEEK_ENDED); + this.convivaVideoAnalytics?.reportPlaybackMetric(Constants.Playback.SEEK_ENDED); }; private readonly onError = () => { - this.convivaVideoAnalytics.reportPlaybackFailed('Fatal error occured'); + const metadata: ConvivaMetadata = {}; + if (Number.isNaN(this.player.duration)) { + metadata[Constants.DURATION] = -1; + } + this.convivaVideoAnalytics?.reportPlaybackFailed( + this.player.errorObject?.message ?? 'Fatal error occurred', + metadata + ); + this.releaseSession(); }; private readonly onSegmentNotFound = () => { - this.convivaVideoAnalytics.reportPlaybackError( + this.convivaVideoAnalytics?.reportPlaybackError( 'A Video Playback Failure has occurred: Segment not found', Constants.ErrorSeverity.FATAL ); }; private readonly onNetworkOffline = () => { - this.convivaVideoAnalytics.reportPlaybackError( + this.convivaVideoAnalytics?.reportPlaybackError( 'A Video Playback Failure has occurred: Waiting for the manifest to come back online', Constants.ErrorSeverity.FATAL ); }; - private readonly onSourceChange = () => { - if (this.player.source === this.currentSource) { - return; + // eslint-disable-next-line class-methods-use-this + private readonly onVisibilityChange = () => { + if (document.visibilityState === 'visible') { + Analytics.reportAppForegrounded(); + } else { + Analytics.reportAppBackgrounded(); } + }; + + private readonly onBeforeUnload = () => { + this.maybeReportPlaybackEnded(); + }; + + private readonly onSourceChange = () => { this.maybeReportPlaybackEnded(); - this.reset(true); this.currentSource = this.player.source; }; private readonly onEnded = () => { - this.convivaVideoAnalytics.reportPlaybackMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.STOPPED); + this.convivaVideoAnalytics?.reportPlaybackMetric( + Constants.Playback.PLAYER_STATE, + Constants.PlayerState.STOPPED + ); this.maybeReportPlaybackEnded(); - this.reset(false); }; private readonly onDurationChange = () => { @@ -237,26 +329,32 @@ export class ConvivaHandler { contentInfo[Constants.IS_LIVE] = Constants.StreamType.VOD; contentInfo[Constants.DURATION] = duration; } - this.convivaVideoAnalytics.setContentInfo(contentInfo); + this.convivaVideoAnalytics?.setContentInfo(contentInfo); }; private readonly onDestroy = () => { this.destroy(); }; - private reset(resetSource: boolean = true): void { - if (resetSource) { - this.currentSource = undefined; - } - this.playbackRequested = false; + private releaseSession(): void { + this.adReporter?.destroy(); + this.verizonAdReporter?.destroy(); + this.yospaceAdReporter?.destroy(); + this.adReporter = undefined; + this.verizonAdReporter = undefined; + this.yospaceAdReporter = undefined; + + this.convivaAdAnalytics?.release(); + this.convivaVideoAnalytics?.release(); + this.convivaAdAnalytics = undefined; + this.convivaVideoAnalytics = undefined; + + this.customMetadata = {}; } destroy(): void { this.maybeReportPlaybackEnded(); this.removeEventListeners(); - this.adReporter?.destroy(); - this.convivaAdAnalytics.release(); - this.convivaVideoAnalytics.release(); Analytics.release(); } } diff --git a/src/integration/ads/CsaiAdReporter.ts b/src/integration/ads/CsaiAdReporter.ts index 2ef9c28a..9ec0769f 100644 --- a/src/integration/ads/CsaiAdReporter.ts +++ b/src/integration/ads/CsaiAdReporter.ts @@ -1,25 +1,21 @@ import { Ad, AdBreak, ChromelessPlayer, GoogleImaAd } from 'theoplayer'; -import { AdAnalytics, Constants, ConvivaMetadata, VideoAnalytics } from '@convivainc/conviva-js-coresdk'; -import { calculateCurrentAdBreakInfo, collectAdMetadata } from '../../utils/Utils'; +import { AdAnalytics, Constants, VideoAnalytics } from '@convivainc/conviva-js-coresdk'; +import { calculateCurrentAdBreakInfo, collectAdMetadata, collectPlayerInfo } from '../../utils/Utils'; export class CsaiAdReporter { private readonly player: ChromelessPlayer; private readonly convivaVideoAnalytics: VideoAnalytics; private readonly convivaAdAnalytics: AdAnalytics; - private readonly metadata: ConvivaMetadata; private currentAdBreak: AdBreak | undefined; + private adBreakCounter: number = 1; - constructor( - player: ChromelessPlayer, - videoAnalytics: VideoAnalytics, - adAnalytics: AdAnalytics, - metadata: ConvivaMetadata - ) { + constructor(player: ChromelessPlayer, videoAnalytics: VideoAnalytics, adAnalytics: AdAnalytics) { this.player = player; this.convivaVideoAnalytics = videoAnalytics; this.convivaAdAnalytics = adAnalytics; - this.metadata = metadata; + this.convivaAdAnalytics.setCallback(this.convivaAdCallback); + this.convivaAdAnalytics.setAdPlayerInfo(collectPlayerInfo()); this.addEventListeners(); } @@ -28,8 +24,9 @@ export class CsaiAdReporter { this.convivaVideoAnalytics.reportAdBreakStarted( Constants.AdType.CLIENT_SIDE, Constants.AdPlayer.CONTENT, - calculateCurrentAdBreakInfo(this.currentAdBreak) + calculateCurrentAdBreakInfo(this.currentAdBreak, this.adBreakCounter) ); + this.adBreakCounter++; }; private readonly onAdBreakEnd = () => { @@ -42,7 +39,7 @@ export class CsaiAdReporter { if (currentAd.type !== 'linear') { return; } - const adMetadata = collectAdMetadata(currentAd, this.metadata); + const adMetadata = collectAdMetadata(currentAd); this.convivaAdAnalytics.setAdInfo(adMetadata); this.convivaAdAnalytics.reportAdLoaded(adMetadata); this.convivaAdAnalytics.reportAdStarted(adMetadata); @@ -94,8 +91,12 @@ export class CsaiAdReporter { this.convivaAdAnalytics.reportAdMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PAUSED); }; + private convivaAdCallback = () => { + const currentTime = this.player.currentTime * 1000; + this.convivaAdAnalytics!.reportAdMetric(Constants.Playback.PLAY_HEAD_TIME, currentTime); + }; + private addEventListeners(): void { - this.player.addEventListener('play', this.onPlaying); this.player.addEventListener('playing', this.onPlaying); this.player.addEventListener('pause', this.onPause); if (this.player.ads === undefined) { @@ -112,7 +113,6 @@ export class CsaiAdReporter { } private removeEventListeners(): void { - this.player.removeEventListener('play', this.onPlaying); this.player.removeEventListener('playing', this.onPlaying); this.player.removeEventListener('pause', this.onPause); if (this.player.ads === undefined) { @@ -128,7 +128,14 @@ export class CsaiAdReporter { this.player.ads.removeEventListener('aderror', this.onAdError); } + reset(): void { + this.adBreakCounter = 0; + } + destroy(): void { + if (this.currentAdBreak) { + this.onAdBreakEnd(); + } this.removeEventListeners(); } } diff --git a/src/integration/ads/VerizonAdReporter.ts b/src/integration/ads/VerizonAdReporter.ts index 2c22989c..ba35cc83 100644 --- a/src/integration/ads/VerizonAdReporter.ts +++ b/src/integration/ads/VerizonAdReporter.ts @@ -7,27 +7,22 @@ import { VerizonMediaRemoveAdBreakEvent, VideoQuality } from 'theoplayer'; -import { AdAnalytics, Constants, ConvivaMetadata, VideoAnalytics } from '@convivainc/conviva-js-coresdk'; -import { calculateVerizonAdBreakInfo, collectVerizonAdMetadata } from '../../utils/Utils'; +import { AdAnalytics, Constants, VideoAnalytics } from '@convivainc/conviva-js-coresdk'; +import { calculateVerizonAdBreakInfo, collectPlayerInfo, collectVerizonAdMetadata } from '../../utils/Utils'; export class VerizonAdReporter { private readonly player: ChromelessPlayer; private readonly convivaVideoAnalytics: VideoAnalytics; private readonly convivaAdAnalytics: AdAnalytics; - private readonly metadata: ConvivaMetadata; private currentAdBreak: VerizonMediaAdBreak | undefined; + private adBreakCounter: number = 1; - constructor( - player: ChromelessPlayer, - videoAnalytics: VideoAnalytics, - adAnalytics: AdAnalytics, - metadata: ConvivaMetadata - ) { + constructor(player: ChromelessPlayer, videoAnalytics: VideoAnalytics, adAnalytics: AdAnalytics) { this.player = player; this.convivaVideoAnalytics = videoAnalytics; this.convivaAdAnalytics = adAnalytics; - this.metadata = metadata; + this.convivaAdAnalytics.setAdPlayerInfo(collectPlayerInfo()); this.addEventListeners(); } @@ -36,8 +31,9 @@ export class VerizonAdReporter { this.convivaVideoAnalytics.reportAdBreakStarted( Constants.AdType.SERVER_SIDE, Constants.AdPlayer.CONTENT, - calculateVerizonAdBreakInfo(this.currentAdBreak) + calculateVerizonAdBreakInfo(this.currentAdBreak, this.adBreakCounter) ); + this.adBreakCounter++; }; private onAdBreakEnd = () => { @@ -52,7 +48,7 @@ export class VerizonAdReporter { }; private onAdBegin = (event: VerizonMediaAdBeginEvent) => { - const adMetadata = collectVerizonAdMetadata(event.ad, this.metadata); + const adMetadata = collectVerizonAdMetadata(event.ad); this.convivaAdAnalytics.setAdInfo(adMetadata); this.convivaAdAnalytics.reportAdStarted(adMetadata); this.convivaAdAnalytics.reportAdMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PLAYING); diff --git a/src/integration/ads/YospaceAdReporter.ts b/src/integration/ads/YospaceAdReporter.ts index 8b52bb20..d1de643c 100644 --- a/src/integration/ads/YospaceAdReporter.ts +++ b/src/integration/ads/YospaceAdReporter.ts @@ -1,13 +1,12 @@ import { ChromelessPlayer, VideoQuality } from 'theoplayer'; -import { AdAnalytics, Constants, ConvivaMetadata, VideoAnalytics } from '@convivainc/conviva-js-coresdk'; +import { AdAnalytics, Constants, VideoAnalytics } from '@convivainc/conviva-js-coresdk'; import { AdBreak, AdVert, AnalyticEventObserver, YospaceConnector } from '@theoplayer/yospace-connector-web'; -import { collectYospaceAdMetadata } from '../../utils/Utils'; +import { collectPlayerInfo, collectYospaceAdMetadata } from '../../utils/Utils'; export class YospaceAdReporter { private readonly player: ChromelessPlayer; private readonly convivaAdAnalytics: AdAnalytics; private readonly convivaVideoAnalytics: VideoAnalytics; - private readonly metadata: ConvivaMetadata; private readonly yospaceConnector: YospaceConnector; private readonly observer: AnalyticEventObserver; @@ -18,13 +17,11 @@ export class YospaceAdReporter { player: ChromelessPlayer, videoAnalytics: VideoAnalytics, adAnalytics: AdAnalytics, - metadata: ConvivaMetadata, yospace: YospaceConnector ) { this.player = player; this.convivaVideoAnalytics = videoAnalytics; this.convivaAdAnalytics = adAnalytics; - this.metadata = metadata; this.yospaceConnector = yospace; this.observer = { onAnalyticUpdate: () => {}, @@ -40,6 +37,7 @@ export class YospaceAdReporter { console.log('session initialized'); this.yospaceConnector.registerAnalyticEventObserver(this.observer); }); + this.convivaAdAnalytics.setAdPlayerInfo(collectPlayerInfo()); this.addEventListeners(); } diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 965ddd70..b558a815 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -1,32 +1,32 @@ import { Constants, + ConvivaAdBreakInfo, ConvivaDeviceMetadata, ConvivaMetadata, ConvivaOptions, ConvivaPlayerInfo } from '@convivainc/conviva-js-coresdk'; import { AdVert } from '@theoplayer/yospace-connector-web'; -import { Ad, AdBreak, ChromelessPlayer, GoogleImaAd, VerizonMediaAd, VerizonMediaAdBreak } from 'theoplayer'; +import { Ad, AdBreak, ChromelessPlayer, GoogleImaAd, VerizonMediaAd, VerizonMediaAdBreak, version } from 'theoplayer'; import { ConvivaConfiguration } from '../integration/ConvivaHandler'; export function collectDeviceMetadata(): ConvivaDeviceMetadata { + // Most device metadata is auto-collected by Conviva. return { [Constants.DeviceMetadata.CATEGORY]: Constants.DeviceCategory.WEB }; } type AdBreakPosition = 'preroll' | 'midroll' | 'postroll'; -let adBreakCounter = 1; -export function calculateVerizonAdBreakInfo(adBreak: VerizonMediaAdBreak): object | undefined { - // TODO improve types +export function calculateVerizonAdBreakInfo(adBreak: VerizonMediaAdBreak, adBreakIndex: number): ConvivaAdBreakInfo { return { [Constants.POD_DURATION]: adBreak.duration!, - [Constants.POD_INDEX]: adBreakCounter++ + [Constants.POD_INDEX]: adBreakIndex }; } -export function calculateCurrentAdBreakInfo(adBreak: AdBreak): object | undefined { +export function calculateCurrentAdBreakInfo(adBreak: AdBreak, adBreakIndex: number): ConvivaAdBreakInfo { const currentAdBreakTimeOffset = adBreak.timeOffset; let currentAdBreakPosition: AdBreakPosition; if (currentAdBreakTimeOffset === 0) { @@ -36,12 +36,11 @@ export function calculateCurrentAdBreakInfo(adBreak: AdBreak): object | undefine } else { currentAdBreakPosition = 'midroll'; } - const currentAdBreakIndex = adBreakCounter++; - // TODO improve types + return { [Constants.POD_POSITION]: currentAdBreakPosition, [Constants.POD_DURATION]: adBreak.maxDuration!, - [Constants.POD_INDEX]: currentAdBreakIndex + [Constants.POD_INDEX]: adBreakIndex }; } @@ -59,9 +58,8 @@ export function calculateConvivaOptions(config: ConvivaConfiguration): ConvivaOp export function collectPlayerInfo(): ConvivaPlayerInfo { return { - [Constants.FRAMEWORK_NAME]: 'THEOplayer HTML5', - // Not applicable for HTML5 - [Constants.FRAMEWORK_VERSION]: 'NaForHTML5' + [Constants.FRAMEWORK_NAME]: 'THEOplayer', + [Constants.FRAMEWORK_VERSION]: version }; } @@ -69,12 +67,15 @@ export function collectContentMetadata( player: ChromelessPlayer, configuredContentMetadata: ConvivaMetadata ): ConvivaMetadata { + const contentInfo: ConvivaMetadata = {}; + const duration = player.duration; + if (!Number.isNaN(duration) && duration !== Infinity) { + contentInfo[Constants.DURATION] = duration; + } // @ts-ignore return { ...configuredContentMetadata, - [Constants.STREAM_URL]: player.src, - [Constants.PLAYER_NAME]: 'THEOplayer', - [Constants.DURATION]: player.duration + ...contentInfo }; } @@ -83,7 +84,6 @@ export function collectYospaceAdMetadata(player: ChromelessPlayer, ad: AdVert): [Constants.ASSET_NAME]: ad.getProperty('AdTitle')?.getValue(), [Constants.STREAM_URL]: player.src!, [Constants.DURATION]: (ad.getDuration() / 1000) as any, - [Constants.IS_LIVE]: Constants.StreamType.VOD, 'c3.ad.technology': Constants.AdType.SERVER_SIDE, 'c3.ad.id': ad.getIdentifier(), 'c3.ad.system': ad.getProperty('AdSystem')?.getValue(), @@ -97,12 +97,9 @@ export function collectYospaceAdMetadata(player: ChromelessPlayer, ad: AdVert): }; } -export function collectVerizonAdMetadata(ad: VerizonMediaAd, metadata: ConvivaMetadata): ConvivaMetadata { +export function collectVerizonAdMetadata(ad: VerizonMediaAd): ConvivaMetadata { const adMetadata: ConvivaMetadata = { - [Constants.PLAYER_NAME]: 'THEOplayer', - [Constants.DURATION]: ad.duration as any, - [Constants.IS_LIVE]: Constants.StreamType.VOD, - [Constants.VIEWER_ID]: metadata[Constants.VIEWER_ID]! + [Constants.DURATION]: ad.duration as any }; const assetName = ad.creative; if (assetName) { @@ -112,12 +109,9 @@ export function collectVerizonAdMetadata(ad: VerizonMediaAd, metadata: ConvivaMe return adMetadata; } -export function collectAdMetadata(ad: Ad, metadata: ConvivaMetadata): ConvivaMetadata { +export function collectAdMetadata(ad: Ad): ConvivaMetadata { const adMetadata: ConvivaMetadata = { - [Constants.PLAYER_NAME]: 'THEOplayer', - [Constants.DURATION]: ad.duration as any, - [Constants.IS_LIVE]: Constants.StreamType.VOD, - [Constants.VIEWER_ID]: metadata[Constants.VIEWER_ID]! + [Constants.DURATION]: ad.duration as any }; const streamUrl = (ad as GoogleImaAd).mediaUrl! || ad.resourceURI; if (streamUrl) { diff --git a/test/pages/main_esm.html b/test/pages/main_esm.html index 445d8d57..8fd06d34 100644 --- a/test/pages/main_esm.html +++ b/test/pages/main_esm.html @@ -23,10 +23,6 @@ // Set up the ConvivaConnector. const convivaMetadata = { - ['Conviva.assetName']: 'Main page', - ['Conviva.streamUrl']: srcUrl, - ['Conviva.streamType']: 'VOD', - ['Conviva.applicationName']: 'THEOplayer', ['Conviva.viewerId']: 'your_viewer_id' }; @@ -38,6 +34,17 @@ const convivaIntegration = new ConvivaConnector(player, convivaMetadata, convivaConfig); + const onSourceChange = () => { + const metadata = { + ['Conviva.assetName']: `Main page ${(new Date()).toLocaleString()}`, + ['customTag1']: "customValue1", + ['customTag2']: "customValue2", + } + convivaIntegration.setContentInfo(metadata); + } + + player.addEventListener('sourcechange', onSourceChange); + player.source = { sources: [{src: srcUrl}] }; diff --git a/test/pages/main_umd.html b/test/pages/main_umd.html index 9d81393c..d3146abf 100644 --- a/test/pages/main_umd.html +++ b/test/pages/main_umd.html @@ -6,6 +6,7 @@ +
@@ -23,10 +24,6 @@ // Set up the ConvivaConnector. const convivaMetadata = { - ['Conviva.assetName']: 'Main page', - ['Conviva.streamUrl']: srcUrl, - ['Conviva.streamType']: 'VOD', - ['Conviva.applicationName']: 'THEOplayer', ['Conviva.viewerId']: 'your_viewer_id' }; @@ -42,6 +39,17 @@ convivaConfig ); + const onSourceChange = () => { + const metadata = { + ['Conviva.assetName']: `Main page ${(new Date()).toLocaleString()}`, + ['customTag1']: "customValue1", + ['customTag2']: "customValue2", + } + convivaIntegration.setContentInfo(metadata); + } + + player.addEventListener('sourcechange', onSourceChange); + player.source = { sources: [{src: srcUrl}] };