diff --git a/.changeset/ten-zoos-sing.md b/.changeset/ten-zoos-sing.md
new file mode 100644
index 00000000..c05c0c19
--- /dev/null
+++ b/.changeset/ten-zoos-sing.md
@@ -0,0 +1,5 @@
+---
+"@theoplayer/adscript-connector-web": minor
+---
+
+Initial release.
diff --git a/.idea/prettier.xml b/.idea/prettier.xml
index b0c1c68f..0c83ac4e 100644
--- a/.idea/prettier.xml
+++ b/.idea/prettier.xml
@@ -2,5 +2,6 @@
+
\ No newline at end of file
diff --git a/.idea/web-connectors.iml b/.idea/web-connectors.iml
index 447c428e..c2d8285e 100644
--- a/.idea/web-connectors.iml
+++ b/.idea/web-connectors.iml
@@ -10,6 +10,7 @@
+
diff --git a/README.md b/README.md
index 8ef22b61..e30f04b7 100644
--- a/README.md
+++ b/README.md
@@ -9,13 +9,14 @@ Using the available connectors allows you to augment the features delivered thro
## Available Connectors
-| Connector | npm package | Source code |
-|:----------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------|
-| CMCD | [![@theoplayer/cmcd-connector-web](https://img.shields.io/npm/v/%40theoplayer%2Fcmcd-connector-web?label=%40theoplayer%2Fcmcd-connector-web)](https://npmjs.com/package/@theoplayer/cmcd-connector-web) | [cmcd](https://github.com/THEOplayer/web-connectors/tree/main/cmcd) |
-| Comscore | [![@theoplayer/comscore-connector-web](https://img.shields.io/npm/v/%40theoplayer%2Fcomscore-connector-web?label=%40theoplayer%2Fcomscore-connector-web)](https://npmjs.com/package/@theoplayer/comscore-connector-web) | [comscore](https://github.com/THEOplayer/web-connectors/tree/main/comscore) |
-| Conviva | [![@theoplayer/conviva-connector-web](https://img.shields.io/npm/v/%40theoplayer%2Fconviva-connector-web?label=%40theoplayer%2Fconviva-connector-web)](https://npmjs.com/package/@theoplayer/conviva-connector-web) | [conviva](https://github.com/THEOplayer/web-connectors/tree/main/conviva) |
-| Nielsen | [![@theoplayer/nielsen-connector-web](https://img.shields.io/npm/v/%40theoplayer%2Fnielsen-connector-web?label=%40theoplayer%2Fnielsen-connector-web)](https://npmjs.com/package/@theoplayer/nielsen-connector-web) | [nielsen](https://github.com/THEOplayer/web-connectors/tree/main/nielsen) |
-| Yospace | [![@theoplayer/yospace-connector-web](https://img.shields.io/npm/v/%40theoplayer%2Fyospace-connector-web?label=%40theoplayer%2Fyospace-connector-web)](https://npmjs.com/package/@theoplayer/yospace-connector-web) | [yospace](https://github.com/THEOplayer/web-connectors/tree/main/yospace) |
+| Connector | npm package | Source code |
+|:----------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------|
+| AdScript | [![@theoplayer/adscript-connector-web](https://img.shields.io/npm/v/%40theoplayer%2Fadscript-connector-web?label=%40theoplayer%2Fadscript-connector-web)](https://npmjs.com/package/@theoplayer/adscript-connector-web) | [adscript](https://github.com/THEOplayer/web-connectors/tree/main/adscript) |
+| CMCD | [![@theoplayer/cmcd-connector-web](https://img.shields.io/npm/v/%40theoplayer%2Fcmcd-connector-web?label=%40theoplayer%2Fcmcd-connector-web)](https://npmjs.com/package/@theoplayer/cmcd-connector-web) | [cmcd](https://github.com/THEOplayer/web-connectors/tree/main/cmcd) |
+| Comscore | [![@theoplayer/comscore-connector-web](https://img.shields.io/npm/v/%40theoplayer%2Fcomscore-connector-web?label=%40theoplayer%2Fcomscore-connector-web)](https://npmjs.com/package/@theoplayer/comscore-connector-web) | [comscore](https://github.com/THEOplayer/web-connectors/tree/main/comscore) |
+| Conviva | [![@theoplayer/conviva-connector-web](https://img.shields.io/npm/v/%40theoplayer%2Fconviva-connector-web?label=%40theoplayer%2Fconviva-connector-web)](https://npmjs.com/package/@theoplayer/conviva-connector-web) | [conviva](https://github.com/THEOplayer/web-connectors/tree/main/conviva) |
+| Nielsen | [![@theoplayer/nielsen-connector-web](https://img.shields.io/npm/v/%40theoplayer%2Fnielsen-connector-web?label=%40theoplayer%2Fnielsen-connector-web)](https://npmjs.com/package/@theoplayer/nielsen-connector-web) | [nielsen](https://github.com/THEOplayer/web-connectors/tree/main/nielsen) |
+| Yospace | [![@theoplayer/yospace-connector-web](https://img.shields.io/npm/v/%40theoplayer%2Fyospace-connector-web?label=%40theoplayer%2Fyospace-connector-web)](https://npmjs.com/package/@theoplayer/yospace-connector-web) | [yospace](https://github.com/THEOplayer/web-connectors/tree/main/yospace) |
## License
diff --git a/adscript/.gitignore b/adscript/.gitignore
new file mode 100644
index 00000000..92b3d00f
--- /dev/null
+++ b/adscript/.gitignore
@@ -0,0 +1,10 @@
+# Node artifact files
+node_modules/
+lib/
+dist/
+
+# Generated by MacOS
+.DS_Store
+
+# Generated by Windows
+Thumbs.db
diff --git a/adscript/README.md b/adscript/README.md
new file mode 100644
index 00000000..99d4adf4
--- /dev/null
+++ b/adscript/README.md
@@ -0,0 +1,100 @@
+# adscript-connector-web
+
+The AdScript connector provides an AdScript integration for THEOplayer.
+
+## Installation
+
+Install using your favorite package manager for Node (such as `npm` or `yarn`):
+
+### npm
+
+```bash
+npm install @theoplayer/adscript-connector-web
+```
+
+### yarn
+
+```bash
+yarn add @theoplayer/adscript-connector-web
+```
+
+## Usage
+
+First you need to add the AdScript connector to your app :
+
+* Add as a regular script
+
+```html
+
+
+
+```
+
+* Add as an ES2015 module
+
+```html
+
+
+```
+
+## Updating metadata
+
+If the metadata has changed during playback, you can update it with:
+
+```javascript
+adScriptConnector.updateMetadata(newMetadata);
+```
+
+## Updating userInfo
+
+If the user info has changed during playback, you can update it with:
+
+```javascript
+adScriptConnector.updateUser(i12n);
+```
\ No newline at end of file
diff --git a/adscript/package.json b/adscript/package.json
new file mode 100644
index 00000000..ef07ea4c
--- /dev/null
+++ b/adscript/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "@theoplayer/adscript-connector-web",
+ "version": "0.0.1",
+ "description": "A connector implementing AdScript with THEOplayer",
+ "main": "dist/adscript-connector.umd.js",
+ "module": "dist/adscript-connector.esm.js",
+ "types": "dist/types/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/types/index.d.ts",
+ "import": "./dist/adscript-connector.esm.js",
+ "require": "./dist/adscript-connector.umd.js"
+ },
+ "./dist/*": "./dist/*",
+ "./package": "./package.json",
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "clean": "rimraf lib dist",
+ "bundle": "rollup -c rollup.config.mjs",
+ "watch": "npm run bundle -- --watch",
+ "build": "npm run clean && npm run bundle",
+ "serve": "http-server ./.. -o /adscript/test/pages/main_esm.html",
+ "test": "echo \"No tests yet\""
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/THEOplayer/web-connectors.git",
+ "directory": "adscript"
+ },
+ "author": "THEO Technologies NV",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/THEOplayer/web-connectors/issues"
+ },
+ "homepage": "https://github.com/THEOplayer/web-connectors/tree/main/adscript#readme",
+ "files": [
+ "dist/",
+ "CHANGELOG.md",
+ "README.md",
+ "LICENSE.md",
+ "package.json"
+ ],
+ "peerDependencies": {
+ "theoplayer": "^7.0.0"
+ }
+}
diff --git a/adscript/rollup.config.mjs b/adscript/rollup.config.mjs
new file mode 100644
index 00000000..49f0009a
--- /dev/null
+++ b/adscript/rollup.config.mjs
@@ -0,0 +1,15 @@
+import fs from "node:fs";
+import {getSharedBuildConfiguration} from "../tools/build.mjs";
+
+
+const {version} = JSON.parse(fs.readFileSync("./package.json", "utf8"));
+
+const fileName = "adscript-connector";
+const globalName = "THEOplayerAdScriptConnector";
+
+const banner = `
+/**
+ * THEOplayer AdScript Web Connector v${version}
+ */`.trim();
+
+export default getSharedBuildConfiguration({fileName, globalName, banner});
diff --git a/adscript/src/adscript/AdScript.d.ts b/adscript/src/adscript/AdScript.d.ts
new file mode 100644
index 00000000..e64ae1b0
--- /dev/null
+++ b/adscript/src/adscript/AdScript.d.ts
@@ -0,0 +1,57 @@
+export type JHMTApiProtocol = 'https:' | 'http:' | 'file:';
+
+interface I12n {
+ i1: string;
+ i2: string;
+ i3: string;
+ i4: string;
+ i5: string;
+}
+
+export type MainVideoContentType = 'content';
+export type EmbeddedContentType = 'preroll' | 'midroll' | 'postroll';
+export type StaticContentType = 'static';
+
+interface StaticContentMetadata {
+ assetid: string;
+ type: StaticContentType;
+ sec1: string;
+ sec2: string;
+ sec3: string;
+ sec4: string;
+ ref: string;
+}
+
+export interface PlayerState {
+ muted: number;
+ volume: number;
+ triggeredByUser: number;
+ normalSpeed: number;
+ fullscreen: number;
+ visibility: number;
+ width: number;
+ height: number;
+}
+
+interface JHMTArray extends Array {
+ i12n: I12n;
+ contentMetadata: ContentMetadata;
+ playerState: PlayerState;
+ push: (item: any) => number; // Type of the push function
+}
+
+declare global {
+ interface Window {
+ JHMT: JHMTArray;
+ JHMTApi: typeof JHMTApi;
+ JHMTApiProtocol: JHMTApiProtocol;
+ }
+}
+
+export interface JHMTApi {
+ setI12n(i12n: I12n);
+
+ setContentMetadata(contentMetadata: ContentMetadata);
+
+ setPlayerState(playerState: PlayerState);
+}
diff --git a/adscript/src/index.ts b/adscript/src/index.ts
new file mode 100644
index 00000000..da9e700c
--- /dev/null
+++ b/adscript/src/index.ts
@@ -0,0 +1,2 @@
+export { AdScriptConnector } from './integration/AdScriptConnector';
+export * from './integration/AdScriptConfiguration';
diff --git a/adscript/src/integration/AdScriptConfiguration.ts b/adscript/src/integration/AdScriptConfiguration.ts
new file mode 100644
index 00000000..4c0581be
--- /dev/null
+++ b/adscript/src/integration/AdScriptConfiguration.ts
@@ -0,0 +1,62 @@
+import type { Ad } from 'theoplayer';
+
+/**
+ * The main content information settings.
+ * For more information, see the [main content information settings](https://adscript.admosphere.cz/en_adScript_browser.html) section in the AdScript documentation.
+ */
+export interface MainVideoContentMetadata {
+ assetid: string;
+ type: 'content';
+ program: string;
+ title: string;
+ length: string;
+ crossId: string;
+ livestream: string;
+ channelId: string;
+ attributes: string;
+}
+
+/**
+ * The embedded content metadata, about the currently playing ad.
+ */
+export interface EmbeddedContentMetadata {
+ assetid: string;
+ type: 'preroll' | 'midroll' | 'postroll';
+ length: string;
+ title: string;
+ asmea: string;
+ attributes: string;
+}
+
+/**
+ * The configuration for the AdScript Connector.
+ */
+export interface AdScriptConfiguration {
+ /**
+ * Integration ID you received from your Nielsen representative.
+ */
+ implementationId: string;
+
+ /**
+ * The initial main content information settings.
+ * Metadata of the main video content needs to be set before the first measured event occurs.
+ * For more information, see the [main content information settings](https://adscript.admosphere.cz/en_adScript_browser.html) section in the AdScript documentation.
+ */
+ metadata: MainVideoContentMetadata;
+
+ /**
+ * Additional information about the logged-in user (customerID, deviceID, profileID) from the client´s database.
+ * For more information, see the [Additional Information Settings](https://adscript.admosphere.cz/en_adScript_browser.html) section.
+ */
+ i12n?: { [key: string]: string };
+
+ /**
+ * An optional advertisement processor to receive metadata about the Ad.
+ */
+ adProcessor?: (ad: Ad) => EmbeddedContentMetadata;
+
+ /**
+ * Whether the connector should log all actions.
+ */
+ debug?: boolean;
+}
diff --git a/adscript/src/integration/AdScriptConnector.ts b/adscript/src/integration/AdScriptConnector.ts
new file mode 100644
index 00000000..31d61ae8
--- /dev/null
+++ b/adscript/src/integration/AdScriptConnector.ts
@@ -0,0 +1,86 @@
+import type { ChromelessPlayer } from 'theoplayer';
+import { AdScriptTHEOIntegration } from './AdScriptTHEOIntegration';
+import { AdScriptConfiguration, MainVideoContentMetadata } from './AdScriptConfiguration';
+import { loadAdScriptSDK } from './LoadAdScriptSDK';
+
+export class AdScriptConnector {
+ private readonly player: ChromelessPlayer;
+ private readonly initialLoadTime: number;
+ private readonly configuration: AdScriptConfiguration;
+
+ private metadata: MainVideoContentMetadata;
+ private i12n: { [key: string]: string } | undefined;
+
+ private adScriptIntegration: AdScriptTHEOIntegration | undefined;
+ private destroyed = false;
+
+ /**
+ * Constructor for the THEOplayer AdScript connector.
+ * @param player a THEOplayer instance reference
+ * @param configuration a configuration object for the AdScript connector
+ * @returns
+ */
+ constructor(player: ChromelessPlayer, configuration: AdScriptConfiguration) {
+ this.player = player;
+ this.configuration = configuration;
+ this.metadata = configuration.metadata;
+ this.i12n = configuration.i12n;
+
+ // This loads the external AdScript SDK script. This is not immediately available, so we start a timer.
+ this.initialLoadTime = new Date().getTime();
+ loadAdScriptSDK(configuration.implementationId);
+
+ this.createAdScriptIntegrationWhenApiIsAvailable();
+ }
+
+ private readonly createAdScriptIntegrationWhenApiIsAvailable = () => {
+ if (this.destroyed) {
+ // The connector was destroyed before the API became available.
+ // Don't bother creating the integration.
+ return;
+ }
+ if (new Date().getTime() > this.initialLoadTime + 5_000) {
+ console.error('JHMT API not found, make sure you included the script to initialize AdScript Measurement.');
+ return;
+ }
+ if (typeof window.JHMTApi === 'object') {
+ this.adScriptIntegration = new AdScriptTHEOIntegration(this.player, this.configuration);
+ if (this.i12n) {
+ this.adScriptIntegration.updateUser(this.i12n);
+ }
+ this.adScriptIntegration.updateMetadata(this.metadata);
+ this.adScriptIntegration.start();
+ return;
+ }
+ setTimeout(this.createAdScriptIntegrationWhenApiIsAvailable, 20);
+ };
+
+ /**
+ * Update the main content information settings.
+ * This method must be called every time the main video content on the currently displayed page changes.
+ * For more information, see the [main content information settings](https://adscript.admosphere.cz/en_adScript_browser.html) section in the AdScript documentation.
+ * @param metadata The MainVideoContentMetadata.
+ */
+ updateMetadata(metadata: MainVideoContentMetadata): void {
+ this.metadata = metadata;
+ this.adScriptIntegration?.updateMetadata(metadata);
+ }
+
+ /**
+ * Updates the additional information about the logged-in user (customerID, deviceID, profileID, ...) from the client´s database.
+ * For more information, see the [Additional Information Settings](https://adscript.admosphere.cz/en_adScript_browser.html) section.
+ * @param i12n The Additional Information
+ */
+ updateUser(i12n: { [key: string]: string }): void {
+ this.i12n = i12n;
+ this.adScriptIntegration?.updateUser(i12n);
+ }
+
+ /**
+ * Destroy the connector.
+ */
+ destroy(): void {
+ this.destroyed = true;
+ this.adScriptIntegration?.destroy();
+ }
+}
diff --git a/adscript/src/integration/AdScriptTHEOIntegration.ts b/adscript/src/integration/AdScriptTHEOIntegration.ts
new file mode 100644
index 00000000..7360a7ff
--- /dev/null
+++ b/adscript/src/integration/AdScriptTHEOIntegration.ts
@@ -0,0 +1,303 @@
+import type {
+ Ad,
+ AdBreakEvent,
+ AdEvent,
+ ChromelessPlayer,
+ DurationChangeEvent,
+ EndedEvent,
+ Event,
+ GoogleImaAd,
+ PlayEvent,
+ RateChangeEvent,
+ SourceChangeEvent,
+ TimeUpdateEvent,
+ VolumeChangeEvent
+} from 'theoplayer';
+import { AdScriptConfiguration, EmbeddedContentMetadata, MainVideoContentMetadata } from './AdScriptConfiguration';
+import { EmbeddedContentType } from './../adscript/AdScript';
+import { Logger } from '../utils/Logger';
+
+interface LogPoint {
+ offset: number;
+ name: string;
+ reported: boolean;
+}
+
+export class AdScriptTHEOIntegration {
+ private player: ChromelessPlayer;
+ private logger: Logger;
+ private readonly adProcessor: ((ad: Ad) => EmbeddedContentMetadata) | undefined;
+ private mainContentMetadata: MainVideoContentMetadata | undefined;
+ private mainContentLogPoints: LogPoint[] = [];
+ private mainContentDuration: number | undefined;
+ private currentAdMetadata: EmbeddedContentMetadata | undefined;
+ private currentAdLogPoints: LogPoint[] = [];
+
+ private JHMTApi = window.JHMTApi;
+ private JHMT = window.JHMT;
+
+ constructor(player: ChromelessPlayer, configuration: AdScriptConfiguration) {
+ this.player = player;
+ this.logger = new Logger(Boolean(configuration.debug));
+ this.adProcessor = configuration?.adProcessor;
+ }
+
+ public start() {
+ if (this.mainContentMetadata === undefined) {
+ throw Error('Metadata of the main video content needs to be set before the first measured event occurs.');
+ }
+ this.reportPlayerState();
+ this.addListeners();
+ }
+
+ public updateMetadata(metadata: MainVideoContentMetadata) {
+ this.mainContentMetadata = metadata;
+ this.logger.onSetMainVideoContentMetadata(this.mainContentMetadata);
+ this.JHMTApi.setContentMetadata(this.mainContentMetadata);
+ }
+
+ public updateUser(i12n: { [key: string]: string }): void {
+ // Set the additional information about the logged user.
+ for (const id in i12n) {
+ this.logger.onSetI12N(id, i12n[id]);
+ this.JHMTApi.setI12n(id, i12n[id]);
+ }
+ }
+
+ public destroy() {
+ this.removeListeners();
+ }
+
+ private addListeners(): void {
+ this.player.addEventListener('playing', this.onFirstMainContentPlaying);
+ this.player.addEventListener('durationchange', this.onDurationChange);
+ this.player.addEventListener('sourcechange', this.onSourceChange);
+ this.player.addEventListener('timeupdate', this.onTimeUpdate);
+ this.player.addEventListener('play', this.onPlay); // TODO
+ this.player.addEventListener('ended', this.onEnded); // TODO
+ this.player.addEventListener('volumechange', this.onVolumeChange);
+ this.player.addEventListener('ratechange', this.onRateChange);
+ this.player.addEventListener('presentationmodechange', this.onPresentationModeChange);
+ window.addEventListener('resize', this.reportPlayerState);
+ window.addEventListener('blur', this.reportPlayerState);
+ window.addEventListener('focus', this.reportPlayerState);
+ document.addEventListener('scroll', this.reportPlayerState);
+ document.addEventListener('visibilitychange', this.reportPlayerState);
+ if (this.player.ads) {
+ this.player.ads.addEventListener('adbreakend', this.onAdBreakEnd); //TODO
+ this.player.ads.addEventListener('adbegin', this.onAdBegin); //TODO
+ this.player.ads.addEventListener('adfirstquartile', this.onAdFirstQuartile);
+ this.player.ads.addEventListener('admidpoint', this.onAdMidpoint);
+ this.player.ads.addEventListener('adthirdquartile', this.onAdTirdQuartile);
+ this.player.ads.addEventListener('adend', this.onAdEnd);
+ }
+ }
+
+ private removeListeners(): void {
+ this.player.removeEventListener('playing', this.onFirstMainContentPlaying);
+ this.player.removeEventListener('durationchange', this.onDurationChange);
+ this.player.removeEventListener('sourcechange', this.onSourceChange);
+ this.player.removeEventListener('timeupdate', this.onTimeUpdate); // TODO
+ this.player.removeEventListener('play', this.onPlay); // TODO
+ this.player.removeEventListener('ended', this.onEnded); // TODO
+ this.player.removeEventListener('volumechange', this.onVolumeChange);
+ this.player.removeEventListener('ratechange', this.onRateChange);
+ this.player.removeEventListener('presentationmodechange', this.onPresentationModeChange);
+ window.removeEventListener('resize', this.reportPlayerState);
+ window.removeEventListener('blur', this.reportPlayerState);
+ window.removeEventListener('focus', this.reportPlayerState);
+ document.removeEventListener('scroll', this.reportPlayerState);
+ document.removeEventListener('visibilitychange', this.reportPlayerState);
+ if (this.player.ads) {
+ this.player.ads.removeEventListener('adbreakend', this.onAdBreakEnd); //TODO
+ this.player.ads.removeEventListener('adbegin', this.onAdBegin); //TODO
+ this.player.ads.removeEventListener('adfirstquartile', this.onAdFirstQuartile);
+ this.player.ads.removeEventListener('admidpoint', this.onAdMidpoint);
+ this.player.ads.removeEventListener('adthirdquartile', this.onAdTirdQuartile);
+ this.player.ads.removeEventListener('adend', this.onAdEnd);
+ }
+ }
+
+ private onDurationChange = (event: DurationChangeEvent) => {
+ if (this.player.ads?.playing || this.mainContentLogPoints.length) return;
+ const { duration } = event;
+ if (isNaN(duration)) return;
+ const firstSecondOfMainContent = this.player.ads?.dai?.streamTimeForContentTime(1);
+ const useDAITimeline = firstSecondOfMainContent && firstSecondOfMainContent !== 1;
+ this.mainContentDuration = duration;
+ if (duration === Infinity) {
+ this.mainContentLogPoints = [{ reported: false, offset: this.player.currentTime + 1, name: 'progress1' }];
+ } else {
+ this.mainContentLogPoints = [
+ { reported: false, offset: duration * 0.75, name: 'thirdQuartile' },
+ { reported: false, offset: duration * 0.5, name: 'midpoint' },
+ { reported: false, offset: duration * 0.25, name: 'firstQuartile' },
+ { reported: false, offset: useDAITimeline ? firstSecondOfMainContent : 1, name: 'progress1' }
+ ];
+ }
+ };
+
+ private onSourceChange = (event: SourceChangeEvent) => {
+ this.logger.onEvent(event);
+ this.player.removeEventListener('playing', this.onFirstMainContentPlaying);
+ this.player.addEventListener('playing', this.onFirstMainContentPlaying);
+ this.mainContentLogPoints = [];
+ this.currentAdLogPoints = [];
+ this.currentAdMetadata = undefined;
+ };
+
+ private onTimeUpdate = (event: TimeUpdateEvent) => {
+ const { currentTime } = event;
+ if (this.currentAdMetadata) {
+ this.maybeReportLogPoint(currentTime, this.currentAdMetadata, this.currentAdLogPoints);
+ } else {
+ this.maybeReportLogPoint(currentTime, this.mainContentMetadata!, this.mainContentLogPoints);
+ }
+ };
+
+ private onFirstMainContentPlaying = () => {
+ const isBeforePreroll = this.player.ads?.scheduledAdBreaks.find((adBreak) => adBreak.timeOffset === 0);
+ if (this.player.ads?.playing || isBeforePreroll) return;
+ this.logger.onAdScriptEvent('start', this.mainContentMetadata);
+ this.JHMT.push(['start', this.mainContentMetadata]);
+ this.player.removeEventListener('playing', this.onFirstMainContentPlaying);
+ };
+
+ private onPlay = (event: PlayEvent) => {
+ this.logger.onEvent(event);
+ this.reportPlayerState();
+ };
+
+ private onEnded = (event: EndedEvent) => {
+ this.logger.onEvent(event);
+ this.logger.onAdScriptEvent('complete', this.mainContentMetadata);
+ this.JHMT.push(['complete', this.mainContentMetadata]);
+ };
+
+ private onVolumeChange = (event: VolumeChangeEvent) => {
+ this.logger.onEvent(event);
+ this.reportPlayerState();
+ };
+
+ private onRateChange = (event: RateChangeEvent) => {
+ this.logger.onEvent(event);
+ this.reportPlayerState();
+ };
+
+ private onPresentationModeChange = (event: Event) => {
+ this.logger.onEvent(event);
+ this.reportPlayerState();
+ };
+
+ private onAdBreakEnd = (event: AdBreakEvent<'adbreakend'>) => {
+ this.logger.onEvent(event);
+ const { adBreak } = event;
+ const { timeOffset, integration } = adBreak;
+ this.currentAdLogPoints = [];
+ this.currentAdMetadata = undefined;
+ if (integration === 'google-dai' && timeOffset === 0) {
+ this.onFirstMainContentPlaying();
+ }
+ };
+
+ private onAdFirstQuartile = (event: AdEvent<'adfirstquartile'>) => {
+ this.logger.onEvent(event);
+ this.logger.onAdScriptEvent('firstquartile', this.currentAdMetadata);
+ this.JHMT.push(['firstquartile', this.currentAdMetadata]);
+ };
+ private onAdMidpoint = (event: AdEvent<'admidpoint'>) => {
+ this.logger.onEvent(event);
+ this.logger.onAdScriptEvent('midpoint', this.currentAdMetadata);
+ this.JHMT.push(['midpoint', this.currentAdMetadata]);
+ };
+ private onAdTirdQuartile = (event: AdEvent<'adthirdquartile'>) => {
+ this.logger.onEvent(event);
+ this.logger.onAdScriptEvent('thirdquartile', this.currentAdMetadata);
+ this.JHMT.push(['thirdquartile', this.currentAdMetadata]);
+ };
+ private onAdEnd = (event: AdEvent<'adend'>) => {
+ this.logger.onEvent(event);
+ this.logger.onAdScriptEvent('complete', this.currentAdMetadata);
+ this.JHMT.push(['complete', this.currentAdMetadata]);
+ };
+
+ private onAdBegin = (event: AdEvent<'adbegin'>) => {
+ this.logger.onEvent(event);
+ if (event.ad.type !== 'linear') return;
+ this.currentAdMetadata = this.buildAdMetadataObject(event);
+ this.currentAdLogPoints = this.buildAdLogPoints(event.ad);
+ this.logger.onAdScriptEvent('start', this.currentAdMetadata);
+ this.JHMT.push(['start', this.currentAdMetadata]);
+ };
+
+ private buildAdLogPoints = (ad: Ad) => {
+ const { duration } = ad;
+ if (ad.adBreak.integration === 'theo' && duration) {
+ return [
+ { reported: false, offset: duration * 0.75, name: 'thirdQuartile' },
+ { reported: false, offset: duration * 0.5, name: 'midpoint' },
+ { reported: false, offset: duration * 0.25, name: 'firstQuartile' },
+ { reported: false, offset: 1, name: 'progress1' }
+ ];
+ }
+ return [{ reported: false, offset: 1, name: 'progress1' }];
+ };
+
+ private buildAdMetadataObject = (event: AdEvent<'adbegin'>): EmbeddedContentMetadata => {
+ const { ad } = event;
+ const { adBreak } = ad;
+ if (this.adProcessor) {
+ return {
+ ...this.adProcessor(ad),
+ type: this.getAdType(adBreak.timeOffset, this.player.duration, adBreak.integration),
+ length: ad.duration?.toString() ?? ''
+ };
+ }
+ return {
+ assetid: ad.id ?? '',
+ type: this.getAdType(adBreak.timeOffset, this.player.duration, adBreak.integration),
+ length: ad.duration?.toString() ?? '',
+ title: ad.integration?.includes('google') ? (ad as GoogleImaAd).title ?? '' : '',
+ asmea: '',
+ attributes: ''
+ };
+ };
+
+ private getAdType = (offset: number, duration: number, integration: string | undefined): EmbeddedContentType => {
+ if (offset === 0) return 'preroll';
+ if (offset === -1) return 'postroll';
+ if (duration - offset < 1 && integration === 'google-dai') return 'postroll';
+ if (this.mainContentDuration && this.mainContentDuration - offset < 1) return 'postroll';
+ return 'midroll';
+ };
+
+ private reportPlayerState = () => {
+ const playerState = {
+ muted: this.player.muted ? 1 : 0,
+ volume: this.player.volume * 100,
+ triggeredByUser: this.player.autoplay ? 1 : 0,
+ normalSpeed: this.player.playbackRate === 1 ? 1 : 0,
+ fullscreen: this.player.presentation.currentMode === 'fullscreen' ? 1 : 0,
+ visibility: this.player.visibility.ratio * 100,
+ width: this.player.element.clientWidth,
+ height: this.player.element.clientHeight
+ };
+ this.logger.onPlayerStateChange(playerState);
+ this.JHMTApi.setPlayerState(playerState);
+ };
+
+ private maybeReportLogPoint = (
+ currentTime: number,
+ metadata: MainVideoContentMetadata | EmbeddedContentMetadata,
+ logPoints: LogPoint[]
+ ) => {
+ logPoints.forEach((logPoint) => {
+ const { reported, offset, name } = logPoint;
+ if (!reported && currentTime >= offset && currentTime < offset + 1) {
+ logPoint.reported = true;
+ this.logger.onAdScriptEvent(name, metadata);
+ this.JHMT.push([name, metadata]);
+ }
+ });
+ };
+}
diff --git a/adscript/src/integration/LoadAdScriptSDK.ts b/adscript/src/integration/LoadAdScriptSDK.ts
new file mode 100644
index 00000000..06eb224b
--- /dev/null
+++ b/adscript/src/integration/LoadAdScriptSDK.ts
@@ -0,0 +1,55 @@
+// @ts-nocheck
+
+/**
+ * This function loads the AdScript SDK.
+ */
+export function loadAdScriptSDK(implementationId: string) {
+ loadAdScriptInternal(window, document, implementationId);
+}
+
+/**
+ * This comes directly from the AdScript documentation: https://adscript.admosphere.cz/en_adScript_browser.html
+ * It has minimal changes to not use arguments.callee.
+ */
+function loadAdScriptInternal(j, h, m, t, c, z) {
+ c = c || 'JHMT';
+ j[c] = j[c] || [];
+ j['JHMTApiProtocol'] = 'https:';
+ z = z || 3;
+
+ var i = (z % 3) + 1,
+ b = h.createElement('script');
+
+ (b.async = !0),
+ b.readyState
+ ? (b.onreadystatechange = function () {
+ ('loaded' !== b.readyState && 'complete' !== b.readyState) ||
+ ((b.onreadystataechange = null), j.JHMTApi.init(c, m, t));
+ })
+ : (b.onload = function () {
+ j.JHMTApi.init(c, m, t);
+ }),
+ (b.src = j['JHMTApiProtocol'] + '//cm' + i + '.jhmt.cz/api.js'),
+ (b.onerror = function () {
+ b.parentNode.removeChild(b);
+ z++;
+ i = (z % 3) + 1;
+ loadAdScriptInternal(j, h, m, t, c, i);
+ }),
+ h.getElementsByTagName('head')[0].appendChild(b);
+
+ try {
+ var it = setInterval(function () {
+ if (typeof j.JHMTApi !== 'undefined') {
+ clearInterval(it);
+ } else {
+ b.parentNode.removeChild(b);
+ z++;
+ i = (z % 3) + 1;
+ loadAdScriptInternal(j, h, m, t, c, i);
+ }
+ }, 1e3);
+ } catch (e) {
+ console.log('JHMT: ' + e);
+ }
+}
diff --git a/adscript/src/utils/Logger.ts b/adscript/src/utils/Logger.ts
new file mode 100644
index 00000000..0bfc23d8
--- /dev/null
+++ b/adscript/src/utils/Logger.ts
@@ -0,0 +1,31 @@
+import type { Event } from 'theoplayer';
+import { PlayerState } from '../adscript/AdScript';
+import { EmbeddedContentMetadata, MainVideoContentMetadata } from '../integration/AdScriptConfiguration';
+
+export class Logger {
+ private readonly debug: boolean;
+
+ constructor(debug: boolean = false) {
+ this.debug = debug;
+ }
+
+ onEvent = (event: Event) => {
+ if (this.debug) console.log(`[ADSCRIPT - THEOplayer EVENTS] ${event.type} event`);
+ };
+
+ onPlayerStateChange = (playerState: PlayerState) => {
+ if (this.debug) console.log(`[ADSCRIPT - setPlayerState]`, playerState);
+ };
+
+ onSetMainVideoContentMetadata = (metadata: MainVideoContentMetadata) => {
+ if (this.debug) console.log(`[ADSCRIPT - setContentMetadata]`, metadata);
+ };
+
+ onAdScriptEvent = (name: string, metadata: MainVideoContentMetadata | EmbeddedContentMetadata | undefined) => {
+ if (this.debug) console.log(`[ADSCRIPT - EVENT] ${name}`, metadata);
+ };
+
+ onSetI12N = (id: string, value: string) => {
+ if (this.debug) console.log(`[ADSCRIPT - SET I12N] ${id}: ${value}`);
+ };
+}
diff --git a/adscript/test/pages/main_esm.html b/adscript/test/pages/main_esm.html
new file mode 100644
index 00000000..de856e8e
--- /dev/null
+++ b/adscript/test/pages/main_esm.html
@@ -0,0 +1,106 @@
+
+
+
+
+ Connector test page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/adscript/test/pages/main_umd.html b/adscript/test/pages/main_umd.html
new file mode 100644
index 00000000..243c972d
--- /dev/null
+++ b/adscript/test/pages/main_umd.html
@@ -0,0 +1,96 @@
+
+
+
+
+ Connector test page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/adscript/test/pages/preroll-empty.xml b/adscript/test/pages/preroll-empty.xml
new file mode 100644
index 00000000..6850c951
--- /dev/null
+++ b/adscript/test/pages/preroll-empty.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/adscript/test/pages/test-assets.json b/adscript/test/pages/test-assets.json
new file mode 100644
index 00000000..8dcf0ba5
--- /dev/null
+++ b/adscript/test/pages/test-assets.json
@@ -0,0 +1,225 @@
+[
+ {
+ "label": "VOD (DASH) - VAST (IMA)",
+ "source": {
+ "sources": [
+ {
+ "src": "https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd",
+ "useCredentials": false
+ }
+ ],
+ "ads": [
+ {
+ "integration": "google-ima",
+ "timeOffset": "start",
+ "sources": "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_preroll_skippable&sz=640x480&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator="
+ }
+ ]
+ },
+ "metadata": {
+ "assetid": "v0000001",
+ "type": "content",
+ "program": "Big Buck Bunny",
+ "title": "Sample Video - Extended",
+ "length": "635",
+ "crossId": "000 111 22222",
+ "livestream": "0",
+ "channelId": "",
+ "attribute": "1"
+ }
+ },
+ {
+ "label": "VOD (HLS)",
+ "source": {
+ "sources": [
+ {
+ "src": "https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny.m3u8",
+ "type": "application/x-mpegurl"
+ }
+ ]
+ },
+ "metadata": {
+ "assetid": "v0000002",
+ "type": "content",
+ "program": "Big Buck Bunny",
+ "title": "Sample Video",
+ "length": "596",
+ "crossId": "000 111 22222",
+ "livestream": "0",
+ "channelId": "",
+ "attribute": "1"
+ }
+ },
+ {
+ "label": "VOD (HLS) - VMAP (IMA)",
+ "source": {
+ "sources": [
+ {
+ "src": "https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny.m3u8",
+ "type": "application/x-mpegurl"
+ }
+ ],
+ "ads": [
+ {
+ "integration": "google-ima",
+ "sources": "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpostpod&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator="
+ }
+ ]
+ },
+ "metadata": {
+ "assetid": "v0000002",
+ "type": "content",
+ "program": "Big Buck Bunny",
+ "title": "Sample Video",
+ "length": "596",
+ "crossId": "000 111 22222",
+ "livestream": "0",
+ "channelId": "",
+ "attribute": "1"
+ }
+ },
+ {
+ "label": "VOD (HLS) - VMAP (THEOAds)",
+ "source": {
+ "sources": [
+ {
+ "src": "https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny.m3u8",
+ "type": "application/x-mpegurl"
+ }
+ ],
+ "ads": [
+ {
+ "integration": "theo",
+ "sources": "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpostpod&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator="
+ }
+ ]
+ },
+ "metadata": {
+ "assetid": "v0000002",
+ "type": "content",
+ "program": "Big Buck Bunny",
+ "title": "Sample Video",
+ "length": "596",
+ "crossId": "000 111 22222",
+ "livestream": "0",
+ "channelId": "",
+ "attribute": "1"
+ }
+ },
+ {
+ "label": "LIVE (DASH) - VAST pre-roll (IMA)",
+ "source": {
+ "sources": [
+ {
+ "src": "https://livesim2.dashif.org/livesim2/testpic_2s/Manifest.mpd",
+ "useCredentials": false
+ }
+ ],
+ "ads": [
+ {
+ "integration": "google-ima",
+ "timeOffset": "start",
+ "sources": "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_preroll_skippable&sz=640x480&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator="
+ }
+ ]
+ },
+ "metadata": {
+ "assetid": "v0000003",
+ "type": "content",
+ "program": "DASHIF Test Assets",
+ "title": "Livesim",
+ "length": "86400",
+ "crossId": "000 111 333333",
+ "livestream": "1",
+ "channelId": "DASHIF1",
+ "attribute": "1"
+ }
+ },
+ {
+ "label": "LIVE (DASH) - empty VAST pre-roll (IMA)",
+ "source": {
+ "sources": [
+ {
+ "src": "https://livesim2.dashif.org/livesim2/testpic_2s/Manifest.mpd",
+ "useCredentials": false
+ }
+ ],
+ "ads": [
+ {
+ "integration": "google-ima",
+ "timeOffset": "start",
+ "sources": "http://localhost:8081/adscript/test/pages/preroll-empty.xml"
+ }
+ ]
+ },
+ "metadata": {
+ "assetid": "v0000003",
+ "type": "content",
+ "program": "DASHIF Test Assets",
+ "title": "Livesim",
+ "length": "86400",
+ "crossId": "000 111 333333",
+ "livestream": "1",
+ "channelId": "DASHIF1",
+ "attribute": "1"
+ }
+ },
+ {
+ "label": "LIVE (DASH) - VAST pre-roll (THEOAds)",
+ "source": {
+ "sources": [
+ {
+ "src": "https://livesim2.dashif.org/livesim2/testpic_2s/Manifest.mpd",
+ "useCredentials": false
+ }
+ ],
+ "ads": [
+ {
+ "integration": "theo",
+ "timeOffset": "start",
+ "sources": "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_preroll_skippable&sz=640x480&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator="
+ }
+ ]
+ },
+ "metadata": {
+ "assetid": "v0000003",
+ "type": "content",
+ "program": "DASHIF Test Assets",
+ "title": "Livesim",
+ "length": "86400",
+ "crossId": "000 111 333333",
+ "livestream": "1",
+ "channelId": "DASHIF1",
+ "attribute": "1"
+ }
+ },
+ {
+ "label": "VOD - Google DAI",
+ "source": {
+ "sources": [
+ {
+ "type": "application/x-mpegurl",
+ "ssai": {
+ "integration": "google-dai",
+ "availabilityType": "vod",
+ "contentSourceID": "2548831",
+ "videoID": "tears-of-steel",
+ "assetKey": "",
+ "apiKey": ""
+ }
+ }
+ ]
+ },
+ "metadata": {
+ "assetid": "v0000004",
+ "type": "content",
+ "program": "DAI VOD Samples",
+ "title": "Tears Of Steel",
+ "length": "784",
+ "crossId": "000 111 333333",
+ "livestream": "0",
+ "channelId": "",
+ "attribute": "1"
+ }
+ }
+]
\ No newline at end of file
diff --git a/adscript/tsconfig.json b/adscript/tsconfig.json
new file mode 100644
index 00000000..c9804acc
--- /dev/null
+++ b/adscript/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist",
+ "declarationDir": "dist/types"
+ },
+ "include": [
+ "src/**/*"
+ ]
+}
\ No newline at end of file
diff --git a/adscript/typedoc.json b/adscript/typedoc.json
new file mode 100644
index 00000000..0d320987
--- /dev/null
+++ b/adscript/typedoc.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://typedoc.org/schema.json",
+ "extends": [
+ "../typedoc.base.json"
+ ],
+ "entryPoints": [
+ "src/index.ts"
+ ],
+ "tsconfig": "tsconfig.json",
+ "readme": "README.md",
+ "name": "AdScript Connector"
+}
diff --git a/package-lock.json b/package-lock.json
index 2d017d80..4babe749 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,8 @@
"conviva",
"nielsen",
"cmcd",
- "comscore"
+ "comscore",
+ "adscript"
],
"devDependencies": {
"@changesets/cli": "^2.27.1",
@@ -42,6 +43,14 @@
"typescript-eslint": "^7.5.0"
}
},
+ "adscript": {
+ "name": "@theoplayer/adscript-connector-web",
+ "version": "0.0.1",
+ "license": "MIT",
+ "peerDependencies": {
+ "theoplayer": "^7.0.0"
+ }
+ },
"cmcd": {
"name": "@theoplayer/cmcd-connector-web",
"version": "1.0.2",
@@ -2396,6 +2405,10 @@
"@sinonjs/commons": "^3.0.0"
}
},
+ "node_modules/@theoplayer/adscript-connector-web": {
+ "resolved": "adscript",
+ "link": true
+ },
"node_modules/@theoplayer/cmcd-connector-web": {
"resolved": "cmcd",
"link": true
diff --git a/package.json b/package.json
index 14a91e11..b56f2b8f 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,8 @@
"conviva",
"nielsen",
"cmcd",
- "comscore"
+ "comscore",
+ "adscript"
],
"scripts": {
"changeset:version": "changeset version && node .changeset/post-process.js",