diff --git a/.changeset/sweet-candles-warn.md b/.changeset/sweet-candles-warn.md
new file mode 100644
index 00000000..8eda3adb
--- /dev/null
+++ b/.changeset/sweet-candles-warn.md
@@ -0,0 +1,5 @@
+---
+"@theoplayer/gemius-connector-web": minor
+---
+
+Initial release.
diff --git a/README.md b/README.md
index e30f04b7..2b8e841d 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ Using the available connectors allows you to augment the features delivered thro
| 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) |
+| Gemius | [![@theoplayer/gemius-connector-web](https://img.shields.io/npm/v/%40theoplayer%2Fgemius-connector-web?label=%40theoplayer%2Fgemius-connector-web)](https://npmjs.com/package/@theoplayer/gemius-connector-web) | [gemius](https://github.com/THEOplayer/web-connectors/tree/main/gemius) |
| 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) |
diff --git a/gemius/.gitignore b/gemius/.gitignore
new file mode 100644
index 00000000..92b3d00f
--- /dev/null
+++ b/gemius/.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/gemius/README.md b/gemius/README.md
new file mode 100644
index 00000000..d164f6ae
--- /dev/null
+++ b/gemius/README.md
@@ -0,0 +1,133 @@
+# gemius-connector-web
+
+The Gemius connector provides a Gemius integration for THEOplayer.
+
+## Installation
+
+```sh
+npm install @theoplayer/gemius-connector-web
+```
+
+Load the gplayer.js library from Gemius. There are two options to to this: either you do it synchronously:
+
+```html
+
+
+```
+
+... or asynchronously
+
+```html
+
+
+```
+
+* Add as an ES2015 module
+
+```html
+
+```
+
+## Updating program parameters
+
+If the program parameters changed during playback, you can update it with:
+
+```javascript
+const newProgramParameters = { ... };
+
+gemiusConnector.update(newProgramParameters);
+```
diff --git a/gemius/package.json b/gemius/package.json
new file mode 100644
index 00000000..7f989521
--- /dev/null
+++ b/gemius/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "@theoplayer/gemius-connector-web",
+ "version": "0.0.1",
+ "description": "A connector implementing Gemius with THEOplayer",
+ "main": "dist/gemius-connector.umd.js",
+ "module": "dist/gemius-connector.esm.js",
+ "types": "dist/types/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/types/index.d.ts",
+ "import": "./dist/gemius-connector.esm.js",
+ "require": "./dist/gemius-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 /gemius/test/pages/main_esm.html",
+ "test": "echo \"No tests yet\""
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/THEOplayer/web-connectors.git",
+ "directory": "gemius"
+ },
+ "author": "THEO Technologies NV",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/THEOplayer/web-connectors/issues"
+ },
+ "homepage": "https://github.com/THEOplayer/web-connectors/tree/main/gemius#readme",
+ "files": [
+ "dist/",
+ "CHANGELOG.md",
+ "README.md",
+ "LICENSE.md",
+ "package.json"
+ ],
+ "peerDependencies": {
+ "theoplayer": "^7.0.0"
+ }
+}
diff --git a/gemius/rollup.config.mjs b/gemius/rollup.config.mjs
new file mode 100644
index 00000000..be0c2f8d
--- /dev/null
+++ b/gemius/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 = "gemius-connector";
+const globalName = "THEOplayerGemiusConnector";
+
+const banner = `
+/**
+ * THEOplayer Gemius Web Connector v${version}
+ */`.trim();
+
+export default getSharedBuildConfiguration({fileName, globalName, banner});
diff --git a/gemius/src/gemius/Gemius.d.ts b/gemius/src/gemius/Gemius.d.ts
new file mode 100644
index 00000000..e07a457b
--- /dev/null
+++ b/gemius/src/gemius/Gemius.d.ts
@@ -0,0 +1,108 @@
+import {
+ NewAdAdditionalParameters,
+ NewProgramAdditionalParameters,
+ PlayAdEventAdditionalParameters,
+ PlayProgramEventAdditionalParameters,
+ PlayerAdditionalParameters,
+ ListEventAdditionalParameters,
+ ChangeResolutionEventAddtionalParameters,
+ ChangeVolumeEventAddtionalParameters,
+ ChangeQualityEventAddtionalParameters
+} from '../integration/GemiusParameters';
+import {
+ PlayEvent,
+ ListEvent,
+ ChangeResolutionEvent,
+ ChangeVolumeEvent,
+ ChangeQualityEvent,
+ BreakEvent,
+ BasicEvent
+} from '../integration/GemiusEvents';
+
+declare global {
+ interface Window {
+ GemiusPlayer: typeof GemiusPlayer;
+ }
+}
+
+export class GemiusPlayer {
+ constructor(playerID: string, gemiusID: string, additionalParameters?: PlayerAdditionalParameters);
+ newProgram(programID: string, additionalParameters: NewProgramAdditionalParameters);
+ newAd(adId: string, additionalParameters?: NewAdAdditionalParameters);
+
+ // List event
+ programEvent(
+ programID: string,
+ offset: number,
+ event: ListEvent,
+ additionalParameters?: ListEventAdditionalParameters
+ );
+
+ // Change resolution events
+ programEvent(
+ programID: string,
+ offset: number,
+ event: ChangeResolutionEvent,
+ additionalParameters?: ChangeResolutionEventAddtionalParameters
+ );
+ adEvent(
+ programID: string,
+ offset: number,
+ event: ChangeResolutionEvent,
+ additionalParameters?: ChangeResolutionEventAddtionalParameters
+ );
+
+ // Change volume events
+ programEvent(
+ programID: string,
+ offset: number,
+ event: ChangeVolumeEvent,
+ additionalParameters?: ChangeVolumeEventAddtionalParameters
+ );
+ adEvent(
+ programID: string,
+ adID: string,
+ offset: number,
+ event: ChangeVolumeEvent,
+ additionalParameters?: ChangeVolumeEventAddtionalParameters
+ );
+
+ // Change quality events
+ programEvent(
+ programID: string,
+ offset: number,
+ event: ChangeQualityEvent,
+ additionalParameters?: ChangeQualityEventAddtionalParameters
+ );
+ adEvent(
+ programID: string,
+ adID: string,
+ offset: number,
+ event: ChangeQualityEvent,
+ additionalParameters?: ChangeQualityEventAddtionalParameters
+ );
+
+ // Play event
+ adEvent(
+ programID: string,
+ adID: string,
+ offset: number,
+ event: PlayEvent,
+ additionalParameters: PlayAdEventAdditionalParameters
+ );
+ programEvent(
+ programID: string,
+ offset: number,
+ event: PlayEvent,
+ additionalParameters: PlayProgramEventAdditionalParameters
+ );
+
+ // Break event (program only)
+ programEvent(programID: string, offset: number, event: BreakEvent);
+
+ // Basic events for both program and ads that don't require additional parameters
+ adEvent(programID: string, adID: string, offset: number, event: BasicEvent);
+ programEvent(programID: string, offset: number, event: BasicEvent);
+
+ dispose();
+}
diff --git a/gemius/src/index.ts b/gemius/src/index.ts
new file mode 100644
index 00000000..09a8e697
--- /dev/null
+++ b/gemius/src/index.ts
@@ -0,0 +1,3 @@
+export { GemiusConnector } from './integration/GemiusConnector';
+export * from './integration/GemiusConfiguration';
+export * from './integration/GemiusParameters';
diff --git a/gemius/src/integration/GemiusConfiguration.ts b/gemius/src/integration/GemiusConfiguration.ts
new file mode 100644
index 00000000..677a3e07
--- /dev/null
+++ b/gemius/src/integration/GemiusConfiguration.ts
@@ -0,0 +1,4 @@
+export interface GemiusConfiguration {
+ gemiusID: string;
+ debug?: boolean;
+}
diff --git a/gemius/src/integration/GemiusConnector.ts b/gemius/src/integration/GemiusConnector.ts
new file mode 100644
index 00000000..b0ec4cb6
--- /dev/null
+++ b/gemius/src/integration/GemiusConnector.ts
@@ -0,0 +1,34 @@
+import { ChromelessPlayer } from 'theoplayer';
+import { GemiusTHEOIntegration } from './GemiusTHEOIntegration';
+import { GemiusConfiguration } from './GemiusConfiguration';
+import { GemiusProgramParameters } from './GemiusParameters';
+
+export class GemiusConnector {
+ private gemiusIntegration: GemiusTHEOIntegration;
+
+ /**
+ * Constructor for the THEOplayer Gemius connector
+ * @param player a THEOplayer instance reference
+ * @param configuration a configuration object for the Gemius connector
+ * @param programParameters the parameters associated with the first source that will be set to the player
+ * @returns
+ */
+ constructor(
+ player: ChromelessPlayer,
+ configuration: GemiusConfiguration,
+ programParameters: GemiusProgramParameters
+ ) {
+ this.gemiusIntegration = new GemiusTHEOIntegration(player, configuration, programParameters);
+ }
+
+ update(programParameters: GemiusProgramParameters) {
+ this.gemiusIntegration.update(programParameters);
+ }
+
+ /**
+ * Destroy
+ */
+ destroy(): void {
+ this.gemiusIntegration.destroy();
+ }
+}
diff --git a/gemius/src/integration/GemiusEvents.ts b/gemius/src/integration/GemiusEvents.ts
new file mode 100644
index 00000000..dba482c4
--- /dev/null
+++ b/gemius/src/integration/GemiusEvents.ts
@@ -0,0 +1,21 @@
+export type PlayEvent = 'play';
+export type BreakEvent = 'break';
+export type ChangeResolutionEvent = 'chngRes';
+export type ChangeVolumeEvent = 'chngVol';
+export type ChangeQualityEvent = 'chngQual';
+
+export enum ListEvent {
+ NEXT = 'next',
+ PREVIOUS = 'prev'
+}
+
+export enum BasicEvent {
+ STOP = 'stop',
+ PAUSE = 'pause',
+ BUFFER = 'buffer',
+ SEEK = 'seek',
+ COMPLETE = 'complete',
+ CLOSE = 'close',
+ SKIP = 'skip',
+ NEXT = 'next'
+}
diff --git a/gemius/src/integration/GemiusParameters.ts b/gemius/src/integration/GemiusParameters.ts
new file mode 100644
index 00000000..ad51cdeb
--- /dev/null
+++ b/gemius/src/integration/GemiusParameters.ts
@@ -0,0 +1,117 @@
+export enum ProgramType {
+ AUDIO = 'audio',
+ VIDEO = 'video'
+}
+
+export enum TransmissionType {
+ ON_DEMAND = 1,
+ BROADCAST_LIVE_TIMESHIFT = 2
+}
+
+export enum ProgramGenre {
+ LIVE = 1,
+ FILM = 2,
+ SERIES_VLOG = 3,
+ PROGRAM = 4,
+ MUSIC = 5,
+ TRAILER_TEASER = 6
+}
+
+export enum AdType {
+ BREAK = 'break',
+ PROMO = 'promo',
+ SPOT = 'spot',
+ SPONSOR = 'sponsor'
+}
+
+export enum AdFormat {
+ VIDEO = 1,
+ AUDIO = 2
+}
+
+export interface PlayerAdditionalParameters {
+ currentDomain?: string;
+ volume?: number;
+ resolution?: string;
+}
+
+export interface NewProgramAdditionalParameters {
+ programName: string;
+ programDuration: number;
+ programType: ProgramType;
+ transmissionType?: TransmissionType;
+ transmissionChannel?: string;
+ transmissionStartTime?: EpochTimeStamp;
+ programGenre?: ProgramGenre;
+ programThematicCategory?: string; // TODO check separate document for details
+ series?: string;
+ programSeason?: string;
+ programPartialName?: string;
+ programProducer?: string;
+ typology?: string;
+ premiereDate?: string;
+ externalPremiereDate?: string;
+ quality?: string;
+ resolution?: string;
+ volume?: number;
+ customAttributes?: {
+ [key: string]: string;
+ };
+}
+
+export interface NewAdAdditionalParameters {
+ adName?: string;
+ adDuration?: number;
+ adType?: AdType;
+ campaignClassification?: string;
+ adFormat?: AdFormat;
+ quality?: string;
+ resolution?: string;
+ volume?: number;
+ customAttributes?: {
+ [key: string]: string;
+ };
+}
+
+export interface PlayAdEventAdditionalParameters {
+ autoPlay?: boolean;
+ adPosition?: number;
+ breakSize?: number;
+ resolution?: string;
+ volume?: number;
+ adDuration?: number;
+ customAttributes?: {
+ [key: string]: string;
+ };
+}
+
+export interface PlayProgramEventAdditionalParameters {
+ autoPlay?: boolean;
+ partID?: number;
+ resolution?: string;
+ volume?: number;
+ programDuration?: number;
+ customAttributes?: {
+ [key: string]: string;
+ };
+}
+
+export interface ListEventAdditionalParameters {
+ listID?: number;
+}
+
+export interface ChangeResolutionEventAddtionalParameters {
+ resolution?: string;
+}
+
+export interface ChangeVolumeEventAddtionalParameters {
+ volume?: number;
+}
+
+export interface ChangeQualityEventAddtionalParameters {
+ quality?: string;
+}
+
+export interface GemiusProgramParameters extends NewProgramAdditionalParameters {
+ programID: string;
+}
diff --git a/gemius/src/integration/GemiusTHEOIntegration.ts b/gemius/src/integration/GemiusTHEOIntegration.ts
new file mode 100644
index 00000000..bc7bbb6b
--- /dev/null
+++ b/gemius/src/integration/GemiusTHEOIntegration.ts
@@ -0,0 +1,325 @@
+import {
+ Ad,
+ AdBreak,
+ AdBreakEvent,
+ AdEvent,
+ AdSkipEvent,
+ AddTrackEvent,
+ ChromelessPlayer,
+ EndedEvent,
+ GoogleImaAd,
+ MediaTrack,
+ PauseEvent,
+ PlayEvent,
+ PlayingEvent,
+ QualityEvent,
+ RemoveTrackEvent,
+ SeekingEvent,
+ SourceChangeEvent,
+ VideoQuality,
+ VolumeChangeEvent,
+ WaitingEvent
+} from 'theoplayer';
+import type { GemiusPlayer } from '../gemius/Gemius';
+import { GemiusConfiguration } from './GemiusConfiguration';
+import { AdType, GemiusProgramParameters } from './GemiusParameters';
+import { Logger } from '../utils/Logger';
+import { BasicEvent } from './GemiusEvents';
+
+const THEOPLAYER_ID = 'THEOplayer';
+const DEFAULT_AD_ID = 'PLACEHOLDER_ID';
+
+export class GemiusTHEOIntegration {
+ private player: ChromelessPlayer;
+ private logger: Logger;
+ private gemiusPlayer: GemiusPlayer;
+ private programParameters: GemiusProgramParameters;
+
+ private partCount: number = 1;
+ private adCount: number = 1;
+ private currentAd: Ad | undefined;
+
+ constructor(
+ player: ChromelessPlayer,
+ configuration: GemiusConfiguration,
+ programParameters: GemiusProgramParameters
+ ) {
+ this.player = player;
+ this.logger = new Logger(Boolean(configuration.debug));
+ this.gemiusPlayer = new window.GemiusPlayer(THEOPLAYER_ID, configuration.gemiusID, {});
+ this.programParameters = programParameters;
+ this.addListeners();
+ }
+
+ public update(programParameters: GemiusProgramParameters) {
+ this.programParameters = programParameters;
+ }
+
+ public destroy() {
+ this.removeListeners();
+ this.gemiusPlayer.dispose();
+ }
+
+ private addListeners(): void {
+ this.player.addEventListener('sourcechange', this.onSourceChange);
+ this.player.addEventListener('playing', this.onFirstPlaying);
+ this.player.addEventListener('play', this.onPlay);
+ this.player.addEventListener('pause', this.onPause);
+ this.player.addEventListener('waiting', this.onWaiting);
+ this.player.addEventListener('seeking', this.onSeeking);
+ this.player.addEventListener('ended', this.onEnded);
+ this.player.addEventListener('volumechange', this.onVolumeChange);
+ this.player.videoTracks.addEventListener('addtrack', this.onAddTrack);
+ this.player.videoTracks.addEventListener('removetrack', this.onRemoveTrack);
+ if (this.player.ads) {
+ this.player.ads.addEventListener('adbreakbegin', this.onAdBreakBegin);
+ this.player.ads.addEventListener('adbreakend', this.onAdBreakEnd);
+ this.player.ads.addEventListener('adbegin', this.onAdBegin);
+ this.player.ads.addEventListener('adend', this.onAdEnd);
+ this.player.ads.addEventListener('adskip', this.onAdSkip);
+ }
+ }
+
+ private removeListeners(): void {
+ this.player.removeEventListener('sourcechange', this.onSourceChange);
+ this.player.removeEventListener('playing', this.onFirstPlaying);
+ this.player.removeEventListener('play', this.onPlay);
+ this.player.removeEventListener('pause', this.onPause);
+ this.player.removeEventListener('waiting', this.onWaiting);
+ this.player.removeEventListener('seeking', this.onSeeking);
+ this.player.removeEventListener('ended', this.onEnded);
+ this.player.removeEventListener('volumechange', this.onVolumeChange);
+ this.player.videoTracks.removeEventListener('addtrack', this.onAddTrack);
+ this.player.videoTracks.removeEventListener('removetrack', this.onRemoveTrack);
+ if (this.player.ads) {
+ this.player.ads.removeEventListener('adbreakbegin', this.onAdBreakBegin);
+ this.player.ads.removeEventListener('adbreakend', this.onAdBreakEnd);
+ this.player.ads.removeEventListener('adbegin', this.onAdBegin);
+ this.player.ads.removeEventListener('adend', this.onAdEnd);
+ this.player.ads.removeEventListener('adskip', this.onAdSkip);
+ }
+ }
+
+ private onSourceChange = (event: SourceChangeEvent) => {
+ this.logger.log(event);
+ if (!this.programParameters) {
+ console.log(`[GEMIUS] No program parameters were provdided`);
+ return;
+ }
+ this.reportBasicEvent(BasicEvent.CLOSE);
+ if (!event.source) {
+ // TODO handle some clear source flow
+ return;
+ }
+ this.partCount = 1;
+ this.currentAd = undefined;
+
+ const { programID, customAttributes, ...additionalParameters } = this.programParameters;
+ this.gemiusPlayer.newProgram(programID, { ...additionalParameters, ...customAttributes });
+ };
+
+ private onFirstPlaying = (event: PlayingEvent) => {
+ this.logger.log(event);
+ const { programID } = this.programParameters;
+ const computedVolume = this.player.muted ? -1 : this.player.volume * 100;
+ if (this.currentAd) {
+ const { id, adBreak, duration } = this.currentAd;
+ const { timeOffset, ads } = adBreak;
+ const normalizedTimeOffset = this.normalizeTime(timeOffset);
+ this.gemiusPlayer.adEvent(programID, id ?? DEFAULT_AD_ID, normalizedTimeOffset, 'play', {
+ autoPlay: true,
+ adPosition: this.adCount,
+ breakSize: ads?.length,
+ // resolution: `AxB`, TODO
+ volume: computedVolume,
+ adDuration: duration
+ });
+ } else {
+ if (this.player.ads?.scheduledAdBreaks.some((adBreak) => adBreak.timeOffset === 0)) return;
+ const offset = this.player.currentTime < 0.5 ? 0 : this.player.currentTime;
+ this.gemiusPlayer.programEvent(programID, offset, 'play', {
+ autoPlay: this.player.autoplay,
+ partID: this.partCount,
+ // resolution: `AxB`; TODO
+ volume: computedVolume,
+ programDuration: this.programParameters.programDuration
+ });
+ }
+ this.player.removeEventListener('playing', this.onFirstPlaying);
+ };
+
+ private onPause = (event: PauseEvent) => {
+ this.logger.log(event);
+ this.reportBasicEvent(BasicEvent.PAUSE);
+ };
+
+ private onPlay = (event: PlayEvent) => {
+ this.logger.log(event);
+ const { programID } = this.programParameters;
+ const computedVolume = this.player.muted ? -1 : this.player.volume * 100;
+ if (this.currentAd) {
+ const { id, adBreak, duration } = this.currentAd;
+ const { timeOffset, ads } = adBreak;
+ const normalizedTimeOffset = this.normalizeTime(timeOffset);
+ this.gemiusPlayer.adEvent(programID, id ?? DEFAULT_AD_ID, normalizedTimeOffset, 'play', {
+ autoPlay: true,
+ adPosition: this.adCount,
+ breakSize: ads?.length,
+ // resolution: `AxB`, TODO
+ volume: computedVolume,
+ adDuration: duration
+ });
+ } else {
+ if (this.player.ads?.scheduledAdBreaks.some((adBreak) => adBreak.timeOffset === 0)) return;
+ const offset = this.player.currentTime < 0.5 ? 0 : this.player.currentTime;
+ this.gemiusPlayer.programEvent(programID, offset, 'play', {
+ autoPlay: this.player.autoplay,
+ partID: this.partCount,
+ // resolution: `AxB`; TODO
+ volume: computedVolume,
+ programDuration: this.programParameters.programDuration
+ });
+ }
+ };
+
+ private onWaiting = (event: WaitingEvent) => {
+ this.logger.log(event);
+ this.reportBasicEvent(BasicEvent.BUFFER);
+ };
+
+ private onSeeking = (event: SeekingEvent) => {
+ this.logger.log(event);
+ this.reportBasicEvent(BasicEvent.SEEK);
+ };
+
+ private onEnded = (event: EndedEvent) => {
+ this.logger.log(event);
+ this.reportBasicEvent(BasicEvent.COMPLETE);
+ };
+
+ private onVolumeChange = (event: VolumeChangeEvent) => {
+ this.logger.log(event);
+ const { volume } = event;
+ const { programID } = this.programParameters;
+ const computedVolume = this.player.muted ? -1 : volume * 100;
+ if (this.currentAd) {
+ const { id, adBreak } = this.currentAd;
+ const { timeOffset } = adBreak;
+ const normalizedTimeOffset = this.normalizeTime(timeOffset);
+ this.gemiusPlayer.adEvent(programID, id ?? DEFAULT_AD_ID, normalizedTimeOffset, 'chngVol', {
+ volume: computedVolume
+ }); // TODO make SSAI ready
+ } else {
+ const { currentTime } = this.player;
+ this.gemiusPlayer.programEvent(programID, currentTime, 'chngVol', { volume: computedVolume });
+ }
+ };
+
+ private onAddTrack = (event: AddTrackEvent) => {
+ const videoTrack = event.track as MediaTrack;
+ videoTrack.addEventListener('activequalitychanged', this.onActiveQualityChanged);
+ };
+
+ private onRemoveTrack = (event: RemoveTrackEvent) => {
+ const videoTrack = event.track as MediaTrack;
+ videoTrack.removeEventListener('activequalitychanged', this.onActiveQualityChanged);
+ };
+
+ private onActiveQualityChanged = (event: QualityEvent<'activequalitychanged'>) => {
+ this.logger.log(event);
+ const { quality } = event;
+ const videoQuality = quality as VideoQuality;
+ const { width, height } = videoQuality;
+ const { programID } = this.programParameters;
+ const { currentTime } = this.player;
+ this.gemiusPlayer.programEvent(programID, currentTime, 'chngQual', { quality: `${width}x${height}` });
+ };
+
+ private onAdBreakBegin = (event: AdBreakEvent<'adbreakbegin'>) => {
+ this.logger.log(event);
+ const { programID } = this.programParameters;
+ const { adBreak } = event;
+ const { timeOffset } = adBreak;
+ const normalizedTimeOffset = this.normalizeTime(timeOffset);
+ this.gemiusPlayer.programEvent(programID, normalizedTimeOffset, 'break');
+ this.player.removeEventListener('playing', this.onFirstPlaying);
+ this.player.addEventListener('playing', this.onFirstPlaying);
+ };
+
+ private onAdBreakEnd = (event: AdBreakEvent<'adbreakend'>) => {
+ this.logger.log(event);
+ this.adCount = 1;
+ const { adBreak } = event;
+ const { timeOffset } = adBreak;
+ if (!this.isPreRoll(adBreak)) this.partCount++;
+ const { programID, customAttributes, ...additionalParameters } = this.programParameters;
+ this.gemiusPlayer.newProgram(programID, { ...additionalParameters, ...customAttributes });
+ this.player.removeEventListener('playing', this.onFirstPlaying);
+ if (timeOffset === 0) this.player.addEventListener('playing', this.onFirstPlaying);
+ };
+
+ private onAdBegin = (event: AdEvent<'adbegin'>) => {
+ this.logger.log(event);
+ const { ad } = event;
+ this.currentAd = ad;
+ const { id, duration, width, height } = ad;
+ const { clientWidth, clientHeight } = this.player.element;
+ this.gemiusPlayer.newAd(id ?? DEFAULT_AD_ID, {
+ adName: ad.integration?.includes('google') ? (ad as GoogleImaAd).title ?? '' : '',
+ adDuration: duration,
+ adType: AdType.BREAK,
+ // TODO campaignClassification
+ adFormat: 1,
+ quality: `${width}x${height}`,
+ resolution: `${clientWidth}x${clientHeight}`,
+ volume: this.player.muted ? -1 : this.player.volume * 100
+ });
+ };
+
+ private onAdEnd = (event: AdEvent<'adend'>) => {
+ this.logger.log(event);
+ const { programID } = this.programParameters;
+ const { ad } = event;
+ const { adBreak } = ad;
+ const { timeOffset } = adBreak;
+ const normalizedTimeOffset = this.normalizeTime(timeOffset);
+ this.gemiusPlayer.adEvent(programID, ad.id ?? DEFAULT_AD_ID, normalizedTimeOffset, BasicEvent.COMPLETE);
+ this.gemiusPlayer.adEvent(programID, ad.id ?? DEFAULT_AD_ID, normalizedTimeOffset, BasicEvent.CLOSE);
+ this.adCount++;
+ this.currentAd = undefined;
+ this.player.removeEventListener('playing', this.onFirstPlaying);
+ this.player.addEventListener('playing', this.onFirstPlaying);
+ };
+
+ private onAdSkip = (event: AdSkipEvent) => {
+ this.logger.log(event);
+ const { programID } = this.programParameters;
+ const { ad } = event;
+ const { adBreak } = ad;
+ const { timeOffset } = adBreak;
+ const normalizedTimeOffset = this.normalizeTime(timeOffset);
+ this.gemiusPlayer.programEvent(programID, normalizedTimeOffset, BasicEvent.SKIP);
+ };
+
+ private reportBasicEvent = (event: BasicEvent) => {
+ const { programID } = this.programParameters;
+ const { currentTime } = this.player;
+ if (this.currentAd) {
+ const { id, adBreak } = this.currentAd;
+ const { timeOffset } = adBreak;
+ const normalizedTimeOffset = this.normalizeTime(timeOffset);
+ this.gemiusPlayer.adEvent(programID, id ?? DEFAULT_AD_ID, normalizedTimeOffset, event); // TODO make SSAI ready
+ }
+ if (!this.currentAd && event !== BasicEvent.PAUSE) {
+ this.gemiusPlayer.programEvent(programID, currentTime, event);
+ }
+ };
+
+ private normalizeTime = (time: number) => {
+ return this.player.ads?.dai?.contentTimeForStreamTime(time) ?? time;
+ };
+
+ private isPreRoll = (adBreak: AdBreak) => {
+ return adBreak.timeOffset === 0;
+ };
+}
diff --git a/gemius/src/utils/Logger.ts b/gemius/src/utils/Logger.ts
new file mode 100644
index 00000000..b5c01ef3
--- /dev/null
+++ b/gemius/src/utils/Logger.ts
@@ -0,0 +1,13 @@
+import { Event } from 'theoplayer';
+
+export class Logger {
+ private readonly debug: boolean;
+
+ constructor(debug: boolean = false) {
+ this.debug = debug;
+ }
+
+ log = (event: Event) => {
+ if (this.debug) console.log(`[GEMIUS - THEOplayer EVENTS] ${event.type} event`);
+ };
+}
diff --git a/gemius/test/pages/gplayer.js b/gemius/test/pages/gplayer.js
new file mode 100644
index 00000000..21e55e10
--- /dev/null
+++ b/gemius/test/pages/gplayer.js
@@ -0,0 +1,680 @@
+// (c) by Gemius SA - gemius player tools
+// ver. 2.12
+
+// gemius_pending.js
+function gemius_pending(i) { window[i] = window[i] || function() {var x = window[i+'_pdata'] = window[i+'_pdata'] || []; x[x.length]=Array.prototype.slice.call(arguments, 0);};};
+(function(cmds) { var c; while(c = cmds.pop()) gemius_pending(c)})(['gemius_cmd', 'gemius_hit', 'gemius_event', 'gemius_init', 'pp_gemius_hit', 'pp_gemius_event', 'pp_gemius_init']);
+window.pp_gemius_cmd = window.pp_gemius_cmd || window.gemius_cmd;
+
+if (typeof GemiusPlayerVisibility == "undefined") {
+ var GemiusPlayerVisibility = {
+ isframe : null,
+ framevis : null,
+ childs : [],
+ timerID : null,
+ init : function() {
+ try {
+ GemiusPlayerVisibility.isframe = (top !== self);
+ if (window.addEventListener) {
+ window.addEventListener("message", GemiusPlayerVisibility._msgreceive, false);
+ } else if (window.attachEvent) {
+ window.attachEvent("onmessage", GemiusPlayerVisibility._msgreceive);
+ }
+
+ if (GemiusPlayerVisibility.isframe) {
+ parent.postMessage("__xx_gplayer_vischeck_xx__","*");
+ }
+ } catch (e) {
+ }
+ },
+ check : function(object) {
+ try {
+ if (GemiusPlayerVisibility.isframe && GemiusPlayerVisibility.framevis===null) {
+ parent.postMessage("__xx_gplayer_vischeck_xx__","*");
+ return null;
+ } else if (GemiusPlayerVisibility.isframe && GemiusPlayerVisibility.framevis===false) {
+ return false;
+ } else if (GemiusPlayerVisibility.isframe !== null) {
+ var vis = GemiusPlayerVisibility._inScreen(object);
+ return vis;
+ } else {
+ return null;
+ }
+ } catch (e) {
+ return null;
+ }
+ },
+ _msgreceive : function(e) {
+ if (typeof e.data=="string" && e.data=="__xx_gplayer_vischeck_xx__") {
+ try {
+ if (GemiusPlayerVisibility.isframe && GemiusPlayerVisibility.framevis===null) {
+ parent.postMessage("__xx_gplayer_vischeck_xx__","*");
+ }
+ var frames = document.getElementsByTagName('iframe');
+ var frame = null;
+ try {
+ for (var i = 0; i0 && visy>0 && (visx*visy > rect.height*rect.width*0.5 || visx*visy > window.innerHeight*window.innerWidth*0.5);
+ return vis;
+ }
+ }
+ },
+ _isChild : function(p, c) {
+ var node = c.parentNode;
+ while (node != null) {
+ if (node == p) {
+ return true;
+ }
+ node = node.parentNode;
+ }
+ return false;
+ }
+ }
+
+ GemiusPlayerVisibility.init();
+}
+
+function GemiusPlayer(playerID, gemiusID, playerData) {
+ this.interval = 5 * 60;
+ this.updateInterval = 1;
+ this.instanceID = (((new Date()).getTime()) + Math.floor(Math.random()*1000)).toString();
+ this.playerID = playerID;
+ this.gemiusID = gemiusID;
+ this.playerData = (playerData || {});
+ this.initialized = false;
+ this.disposed = false;
+ this.programs = {};
+ this.ads = {};
+ this.hitsCounter = 0;
+ this.currentProgramID = null;
+ this.videoObject = null;
+ this.resolution = null;
+ this.visible = null;
+ this.hidetime = null;
+ this.continueIntervalID = null;
+ this.updateIntervalID = null;
+ this.unloadFun = null;
+ this.pagehideFun = null;
+ this.pageshowFun = null;
+
+ this._playerDataKeys = {"currentDomain":"_SPD", "resolution":"_SPR", "volume":"_SPV"};
+ this._programDataKeys = {'programType':'_SCTE','programDuration':'_SCD','programTransmission':'_SCTR','programName':'_SCT',
+ 'series':'_SCS','typology':'_SCTY','premiereDate':'_SCPD','externalPremiereDate':'_SCEPD','quality':'_SCQ',
+ 'resolution':'_SCR', 'volume':'_SCV', 'programGenre':'_SCG','programPartialName':'_SCPN','programProducer':'_SCPP',
+ 'programThematicCategory':'_SCTC','programSeason':'_SCSS','transmissionChannel':'_SCTB','transmissionStartTime':'_SCTS',
+ 'transmissionType':'_SCTT'};
+ this._programExtraDataKeys = {'partID':'_SCP'};
+ this._adDataKeys = {'adName':'_SAN','adDuration':'_SAD','adTransmission':'_SATR','adType':'_SAT','campaignClassification':'_SAC',
+ 'quality':'_SAQ', 'resolution':'_SAR', 'volume':'_SAV', 'adFormat':'_SAF'};
+ this._adExtraDataKeys = {'adPosition':'_SAP', 'breakSize':'_SBS', 'breakType':'_SBT'};
+ this._eventDataKeys = {'listID':'_SL'};
+ this._reservedDataKeys = {'autoPlay':true};
+
+ //public methods
+ this.setVideoObject = function(video) {
+ this.videoObject = video;
+ }
+
+ this.newProgram = function(programID, programData) {
+ this._init();
+ this.programs[programID] = {"state": "new", "started": false, "program_started": false, "last_action_time": null, "data": this._clone(programData), "extradata": {}};
+ this._validateParams(this.programs[programID]["data"]);
+ this._sendHit(programID, null, "data", "streamcontent", []);
+ }
+
+ this.newAd = function(adID, adData) {
+ this._init();
+ this.ads[adID] = {"state": "new", "started": false, "last_action_time": null, "currentProgramID": null, "currentAdPosition": null, "data": this._clone(adData), "extradata": {}};
+ this._validateParams(this.ads[adID]["data"]);
+ this._sendHit(null, adID, "data", "streamspot", []);
+ }
+
+ this.programEvent = function(programID, offset, eventType, eventData) {
+ eventData = eventData || {};
+ if (this.programs[programID]) {
+ this._updateCustomProgramData(programID, eventData);
+ this.hitsCounter = (programID==this.currentProgramID)?(this.hitsCounter+1):1;
+ this.currentProgramID = programID;
+ if (this.hitsCounter<=2000) {
+ if (eventType == "play") {
+ this._playProgram(programID, offset, eventData);
+ } else if (eventType == "chngQual" || eventType == "chngRes" || eventType == "chngVol") {
+ this._changeParam(programID, null, offset, eventType, eventData);
+ } else {
+ this._event(programID, null, offset, eventType, eventData);
+ }
+ } else {
+ this._stopAll();
+ }
+ }
+ }
+
+ this.adEvent = function(programID, adID, offset, eventType, eventData) {
+ eventData = eventData || {};
+ if (this.programs[programID] && this.ads[adID]) {
+ this._updateCustomAdData(adID, eventData);
+ this.hitsCounter = (programID==this.currentProgramID)?(this.hitsCounter+1):1;
+ this.currentProgramID = programID;
+ if (this.hitsCounter<=2000) {
+ if (eventType == "play") {
+ this._playAd(programID, adID, offset, eventData);
+ } else if (eventType == "chngQual" || eventType == "chngRes" || eventType == "chngVol") {
+ this._changeParam(programID, adID, offset, eventType, eventData);
+ } else {
+ this._event(programID, adID, offset, eventType, eventData);
+ }
+ } else {
+ this._stopAll();
+ }
+ }
+ }
+
+ this.dispose = function() {
+ this._dispose();
+ }
+
+ //private methods
+ this._playProgram = function(programID, offset, eventData) {
+ this._stopAll();
+ var eventCategory = (this.programs[programID]['program_started']==false)?"programstart":(this.programs[programID]["started"]?"play":"start");
+ var params = ["_SCO="+offset, "_SED="+this._updateActionTime(this.programs[programID])];
+ if (typeof eventData['autoPlay'] != "undefined") params = params.concat(["_ECA=" + (eventData['autoPlay']?1:0)]);
+ if (typeof eventData['partID'] != "undefined") this.programs[programID]["extradata"]["partID"] = eventData['partID'];
+ else delete this.programs[programID]["extradata"]["partID"];
+ if (typeof eventData['volume'] != "undefined") params = params.concat(["_SPVN=" + eventData['volume']]);
+ if (typeof eventData['resolution'] != "undefined") params = params.concat(["_SPRN=" + eventData['resolution']]);
+ this.programs[programID]["state"] = "play";
+ this.programs[programID]["started"] = true;
+ this.programs[programID]["program_started"] = true;
+ this._sendHit(programID, null, "stream", eventCategory, params);
+
+ if (typeof eventData['volume'] != "undefined") this.playerData['volume'] = eventData['volume'];
+ if (typeof eventData['resolution'] != "undefined") this.playerData['resolution'] = eventData['resolution'];
+
+ for (var adID in this.ads) {
+ this.ads[adID]["started"] = false;
+ }
+
+ for (var pID in this.programs) {
+ if (pID != programID) {
+ this.programs[pID]["started"] = false;
+ this.programs[pID]["program_started"] = false;
+ }
+ }
+ }
+
+ this._playAd = function(programID, adID, offset, eventData) {
+ this._stopAll();
+ var start = (!this.ads[adID]["started"] || this.ads[adID]["currentProgramID"]!=programID || this.ads[adID]["currentAdPosition"]!=eventData['adPosition']);
+ var breakType = this._getBreakType(programID, offset);
+ var eventCategory = (start && breakType!="post" && this.programs[programID]['program_started']==false)?"programstart":(start?"start":"play");
+ var params = ["_SCO="+offset,"_SED="+this._updateActionTime(this.ads[adID])];
+ if (typeof eventData['autoPlay'] != "undefined") params = params.concat(["_ECA=" + (eventData['autoPlay']?1:0)]);
+ if (typeof eventData['adPosition'] != "undefined") this.ads[adID]["extradata"]["adPosition"] = eventData['adPosition'];
+ else delete this.ads[adID]["extradata"]["adPosition"];
+ if (typeof eventData['breakSize'] != "undefined") this.ads[adID]["extradata"]["breakSize"] = eventData['breakSize'];
+ else delete this.ads[adID]["extradata"]["breakSize"];
+ if (breakType) this.ads[adID]["extradata"]["breakType"] = breakType;
+ else delete this.ads[adID]["extradata"]["breakType"];
+ if (typeof eventData['volume'] != "undefined") params = params.concat(["_SPVN=" + eventData['volume']]);
+ if (typeof eventData['resolution'] != "undefined") params = params.concat(["_SPRN=" + eventData['resolution']]);
+ this.ads[adID]["state"] = "play";
+ this.ads[adID]["started"] = true;
+ this.ads[adID]["currentProgramID"] = programID;
+ this.ads[adID]["currentAdPosition"] = eventData['adPosition'];
+ if (breakType!="post") this.programs[programID]["program_started"] = true;
+ this._sendHit(programID, adID, "stream", eventCategory, params);
+
+ if (typeof eventData['volume'] != "undefined") this.playerData['volume'] = eventData['volume'];
+ if (typeof eventData['resolution'] != "undefined") this.playerData['resolution'] = eventData['resolution'];
+ }
+
+ this._event = function(programID, adID, offset, eventType, eventData) {
+ var evtypes = {"pause":"pause", "stop":"stop", "close":"close", "buffer":"buffering", "break":"break", "seek":"seek", "complete":"complete", "skip":"skip", "next":"next", "prev":"prev"};
+ var data = (adID?this.ads[adID]:this.programs[programID]);
+ if (evtypes[eventType]) {
+ var params = ["_SED="+this._updateActionTime(data)].concat(this._convertEventParams(eventData));
+ if (typeof offset != "undefined") params = params.concat(["_SCO="+offset]);
+ if (eventType == "stop" || eventType == "complete" || eventType == "close") {
+ data["started"] = false;
+ if (!adID) this.programs[programID]["program_started"] = false;
+ }
+ data["state"] = evtypes[eventType];
+ this._sendHit(programID, adID, "stream", evtypes[eventType], params);
+ }
+ }
+
+ this._changeParam = function(programID, adID, offset, eventType, eventData) {
+ var data = (adID?this.ads[adID]:this.programs[programID]);
+ var params = ["_SED="+this._updateActionTime(data)];
+ if (typeof offset != "undefined") params = params.concat(["_SCO="+offset]);
+ if (typeof eventData['volume'] != "undefined") params = params.concat(["_SPVN=" + eventData['volume']]);
+ if (typeof eventData['resolution'] != "undefined") params = params.concat(["_SPRN=" + eventData['resolution']]);
+ if (typeof eventData['quality'] != "undefined") params = params.concat([(adID?"_SAQN=":"_SCQN=") + eventData['quality']]);
+ this._sendHit(programID, adID, "stream", "continue", params);
+
+ if (eventData['quality']) data['data']['quality'] = eventData['quality'];
+ if (eventData['resolution']) this.playerData['resolution'] = eventData['resolution'];
+ if (eventData['volume']) this.playerData['volume'] = eventData['volume'];
+ }
+
+ this._getBreakType = function(programID, offset) {
+ try {
+ var duration = (this.programs[programID]['data']['programDuration'] || 0);
+ var transmissionType = (this.programs[programID]['data']['transmissionType'] || 0);
+ if (typeof duration != "number") duration = parseInt(duration,10);
+ if (typeof transmissionType != "number") transmissionType = parseInt(transmissionType,10);
+ if (typeof offset != "number") offset = parseInt(offset,10);
+ if (transmissionType==2) return "live";
+ else if (duration<0) return "unknown";
+ else if ((duration<=100 && offset<=0) || (duration>100 && offset<5)) return "pre";
+ else if (duration>0 && ((duration<=100 && offset>=duration) || (duration>100 && offset>duration-5))) return "post";
+ else return "mid";
+ } catch (e) {
+ return "";
+ }
+ }
+
+ this._stopAll = function() {
+ for (var programID in this.programs) {
+ if (this.programs[programID]["state"] == "play") {
+ this._event(programID, null, undefined, "pause", {});
+ }
+ }
+
+ for (var adID in this.ads) {
+ if (this.ads[adID]["state"] == "play" && this.programs[this.ads[adID]["currentProgramID"]]) {
+ this._event(this.ads[adID]["currentProgramID"], adID, undefined, "pause", {});
+ }
+ }
+ }
+
+ this._update = function() {
+ var vis = GemiusPlayerVisibility.check(this.videoObject);
+ var res = this._getPlayerSize();
+ if (vis !== this.visible || res !== this.resolution) {
+ this._continue();
+ this.visible = vis;
+ this.resolution = res;
+ }
+ }
+
+ this._getPlayerSize = function() {
+ if (!this.videoObject) {
+ return null;
+ } else {
+ try {
+ var rect = this.videoObject.getBoundingClientRect();
+ return parseInt(rect.width,10) + "x" + parseInt(rect.height,10);
+ } catch (e) {
+ return null;
+ }
+ }
+ }
+
+ this._continue = function() {
+ for (var programID in this.programs) {
+ if (this.programs[programID]["state"] == "play") {
+ var params = ["_SED="+this._updateActionTime(this.programs[programID])];
+ this._sendHit(programID, null, "stream", "continue", params);
+ }
+ }
+ for (var adID in this.ads) {
+ if (this.ads[adID]["state"] == "play") {
+ var params = ["_SED="+this._updateActionTime(this.ads[adID])];
+ this._sendHit(this.ads[adID]["currentProgramID"], adID, "stream", "continue", params);
+ }
+ }
+ }
+
+ this._sendClosingHits = function(eventCategory) {
+ try {
+ if (typeof gemius_close == 'function') {
+ gemius_close();
+ }
+ var delay = false;
+ for (var programID in this.programs) {
+ if (this.programs[programID]["state"] == "play") {
+ var params = ["_SED="+this._updateActionTime(this.programs[programID])];
+ this._sendHit(programID, null, "stream", eventCategory, params);
+ delay = true;
+ }
+ }
+ for (var adID in this.ads) {
+ if (this.ads[adID]["state"] == "play") {
+ var params = ["_SED="+this._updateActionTime(this.ads[adID])];
+ this._sendHit(this.ads[adID]["currentProgramID"], adID, "stream", eventCategory, params);
+ delay = true;
+ }
+ }
+ if (delay && typeof navigator.sendBeacon != "function") {
+ var start = (new Date()).getTime();
+ while (start+250>(new Date()).getTime());
+ }
+ } catch(e) {}
+ }
+
+ this._unload = function() {
+ this._sendClosingHits("unload");
+ }
+
+ this._pagehide = function() {
+ this._unload();
+ if (this.hidetime == null) {
+ this.hidetime = (new Date()).getTime();
+ }
+ }
+
+ this._pageshow = function() {
+ if (this.hidetime == null) {
+ return;
+ }
+ var showtime = ((new Date()).getTime());
+ var leap = (showtime > this.hidetime) ? showtime - this.hidetime : 0;
+ try {
+ for (var programID in this.programs) {
+ this._updateHideTime(this.programs[programID], leap);
+ }
+ for (var adID in this.ads) {
+ this._updateHideTime(this.ads[adID], leap);
+ }
+ } catch(e) {}
+ this.hidetime = null;
+ }
+
+ this._updateHideTime = function(streamData, leap) {
+ try {
+ if (streamData['state'] == 'play' && streamData['last_action_time']) {
+ streamData['last_action_time'] += leap;
+ }
+ } catch(e) {}
+ }
+
+ this._updateActionTime = function(streamData) {
+ var duration = 0;
+ try {
+ if (streamData['state'] == 'play' && streamData['last_action_time']) {
+ duration = Math.round(((new Date()).getTime() - streamData['last_action_time']) / 1000);
+ if (duration < 0 || duration > 2*this.interval) {
+ duration = 0;
+ }
+ }
+ streamData['last_action_time'] = (new Date()).getTime();
+ } catch (e) {}
+ return duration;
+ }
+
+ this._sendHit = function(programID, adID, eventType, eventCategory, params) {
+ if (this.disposed) {
+ return;
+ }
+ var extra = ["_EC="+eventCategory].concat(this._getPlayerParams()).concat(this._getProgramParams(programID)).concat(this._getAdParams(adID)).concat(params);
+ gemius_event.apply(window, ['_' + eventType + '_', this.gemiusID].concat(extra));
+ }
+
+ this._getProgramParams = function(programID) {
+ if (!this.programs[programID]) return [];
+ var params = ["_SC="+programID];
+ params = params.concat(this._convertParams(this.programs[programID]["data"], this._programDataKeys, true));
+ params = params.concat(this._convertParams(this.programs[programID]["extradata"], this._programExtraDataKeys));
+ return params;
+ }
+
+ this._getAdParams = function(adID) {
+ if (!this.ads[adID]) return [];
+ var params = ["_SA="+adID];
+ params = params.concat(this._convertParams(this.ads[adID]["data"], this._adDataKeys, true));
+ params = params.concat(this._convertParams(this.ads[adID]["extradata"], this._adExtraDataKeys));
+ return params;
+ }
+
+ this._getPlayerParams = function() {
+ var params = ["_SPI="+this.instanceID,"_SP="+this.playerID];
+ if (this.resolution !== null) params = params.concat(["_SPS="+this.resolution]);
+ if (this.visible !== null) params = params.concat(["_SPIS="+(this.visible?"1":"0")]);
+ return params.concat(this._convertParams(this.playerData,this._playerDataKeys));
+ }
+
+ this._validateParams = function(params) {
+ var intTypes = {'programDuration':1, 'adDuration':1};
+ var maxLengths = {'programTransmission':20,'adTransmission':20,'programPartialName':64,'programProducer':64,
+ 'programSeason':64,'transmissionChannel':64,'transmissionStartTime':10};
+ if (typeof params != 'undefined') {
+ for (var key in params) {
+ if (maxLengths[key]) {
+ if (typeof params[key] == 'string') {
+ params[key] = params[key].substr(0, maxLengths[key]);
+ } else {
+ delete params[key];
+ }
+ }
+ if (intTypes[key]) {
+ params[key] = parseInt(params[key]);
+ }
+ }
+ }
+ }
+
+ this._convertEventParams = function(eventData) {
+ return this._convertParams(eventData, this._eventDataKeys);
+ }
+
+ this._convertParams = function(params, keyMap, allowCustomParams) {
+ var res = [];
+ if (typeof params != 'undefined') {
+ for (var key in params) {
+ if (keyMap[key] || allowCustomParams) {
+ res[res.length] = (keyMap[key] || key) + "=" + params[key];
+ }
+ }
+ }
+ return res;
+ }
+
+ this._updateCustomProgramData = function(programID, eventData) {
+ for (var key in eventData) {
+ if (!(key in this._reservedDataKeys)) {
+ this.programs[programID]['data'][key] = eventData[key];
+ }
+ }
+ this._validateParams(this.programs[programID]['data']);
+ }
+
+ this._updateCustomAdData = function(adID, eventData) {
+ for (var key in eventData) {
+ if (!(key in this._reservedDataKeys)) {
+ this.ads[adID]['data'][key] = eventData[key];
+ }
+ }
+ this._validateParams(this.ads[adID]['data']);
+ }
+
+ this._assignDataKeys = function(target, source) {
+ for (var key in source) {
+ target[key] = true;
+ }
+ }
+
+ this._initReservedKeys = function() {
+ this._assignDataKeys(this._reservedDataKeys, this._playerDataKeys);
+ this._assignDataKeys(this._reservedDataKeys, this._programDataKeys);
+ this._assignDataKeys(this._reservedDataKeys, this._programExtraDataKeys);
+ this._assignDataKeys(this._reservedDataKeys, this._adDataKeys);
+ this._assignDataKeys(this._reservedDataKeys, this._adExtraDataKeys);
+ this._assignDataKeys(this._reservedDataKeys, this._eventDataKeys);
+ delete this._reservedDataKeys['programDuration'];
+ delete this._reservedDataKeys['adDuration'];
+ }
+
+ this._addEvent = function(obj,type,fn) {
+ if (obj.addEventListener) {
+ obj.addEventListener(type, fn, false);
+ } else if (obj.attachEvent) {
+ obj.attachEvent('on'+type, fn);
+ }
+ }
+
+ this._removeEvent = function(obj,type,fn) {
+ if (obj.removeEventListener) {
+ obj.removeEventListener(type, fn, false);
+ } else if (obj.detachEvent) {
+ obj.detachEvent('on'+type, fn);
+ }
+ }
+
+ this._init = function() {
+ if (!this.initialized && !this.disposed) {
+ this.initialized = true;
+ this._initReservedKeys();
+ this._update();
+ this.continueIntervalID = setInterval(this._wrapFun(this,"_continue"), this.interval * 1000);
+ this.updateIntervalID = setInterval(this._wrapFun(this,"_update"), this.updateInterval * 1000);
+ this.unloadFun = this._wrapFun(this,"_unload");
+ this.pagehideFun = this._wrapFun(this,"_pagehide");
+ this.pageshowFun = this._wrapFun(this,"_pageshow");
+ try {
+ if ('onpagehide' in window) {
+ this._addEvent(window.top, 'pagehide', this.pagehideFun);
+ this._addEvent(window.top, 'pageshow', this.pageshowFun);
+ } else if (typeof navigator.sendBeacon == "function") {
+ this._addEvent(window.top, 'unload', this.unloadFun);
+ } else {
+ this._addEvent(window.top, 'beforeunload', this.unloadFun);
+ }
+ } catch (e) {
+ if ('onpagehide' in window) {
+ this._addEvent(window, 'pagehide', this.pagehideFun);
+ this._addEvent(window, 'pageshow', this.pageshowFun);
+ } else if (typeof navigator.sendBeacon == "function") {
+ this._addEvent(window, 'unload', this.unloadFun);
+ } else {
+ this._addEvent(window, 'beforeunload', this.unloadFun);
+ }
+ }
+ }
+ }
+
+ this._dispose = function() {
+ if (this.initialized && !this.disposed) {
+ this._sendClosingHits("dispose");
+ clearInterval(this.continueIntervalID);
+ clearInterval(this.updateIntervalID);
+ try {
+ if ('onpagehide' in window) {
+ this._removeEvent(window.top, 'pagehide', this.pagehideFun);
+ this._removeEvent(window.top, 'pageshow', this.pageshowFun);
+ } else if (typeof navigator.sendBeacon == "function") {
+ this._removeEvent(window.top, 'unload', this.unloadFun);
+ } else {
+ this._removeEvent(window.top, 'beforeunload', this.unloadFun);
+ }
+ } catch (e) {
+ if ('onpagehide' in window) {
+ this._removeEvent(window, 'pagehide', this.pagehideFun);
+ this._removeEvent(window, 'pageshow', this.pageshowFun);
+ } else if (typeof navigator.sendBeacon == "function") {
+ this._removeEvent(window, 'unload', this.unloadFun);
+ } else {
+ this._removeEvent(window, 'beforeunload', this.unloadFun);
+ }
+ }
+ this.disposed = true;
+ }
+ }
+
+ this._wrapFun = function(self, method) {
+ return function() {
+ self[method]();
+ }
+ }
+
+ this._clone = function(obj) {
+ var res = {};
+ if (!obj) return res;
+ for (var key in obj) {
+ if (obj.hasOwnProperty(key)) res[key] = obj[key];
+ }
+ return res;
+ }
+}
+
+if (typeof window['gemius_player_data'] != 'undefined') {
+ for (var i=0; i
+
+
+
+ Connector test page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gemius/test/pages/main_umd.html b/gemius/test/pages/main_umd.html
new file mode 100644
index 00000000..202dad1e
--- /dev/null
+++ b/gemius/test/pages/main_umd.html
@@ -0,0 +1,100 @@
+
+
+
+
+ Connector test page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gemius/test/pages/preroll-empty.xml b/gemius/test/pages/preroll-empty.xml
new file mode 100644
index 00000000..6850c951
--- /dev/null
+++ b/gemius/test/pages/preroll-empty.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/gemius/test/pages/test-assets.json b/gemius/test/pages/test-assets.json
new file mode 100644
index 00000000..55772faf
--- /dev/null
+++ b/gemius/test/pages/test-assets.json
@@ -0,0 +1,273 @@
+[
+ {
+ "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": {
+ "programID": "000001",
+ "programName": "Big Buck Bunny (DASH)",
+ "programDuration": 635,
+ "programType": "video",
+ "transmissionType": 1,
+ "programGenre": 4,
+ "series": "Test Content",
+ "programSeason": "season 1",
+ "programProducer": "Blender",
+ "customAttributes": {
+ "intCategory": "Comedy",
+ "intType": "vod",
+ "intStatus": "public"
+ }
+ }
+ },
+ {
+ "label": "VOD (HLS)",
+ "source": {
+ "sources": [
+ {
+ "src": "https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny.m3u8",
+ "type": "application/x-mpegurl"
+ }
+ ]
+ },
+ "metadata": {
+ "programID": "000002",
+ "programName": "Big Buck Bunny (HLS)",
+ "programDuration": 596,
+ "programType": "video",
+ "transmissionType": 1,
+ "programGenre": 4,
+ "series": "Test Content",
+ "programSeason": "season 1",
+ "programProducer": "Blender",
+ "customAttributes": {
+ "intName": "BBB",
+ "intCategory": "Comedy",
+ "intType": "vod",
+ "intStatus": "public"
+ }
+ }
+ },
+ {
+ "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%3Dpremidpost&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": {
+ "programID": "000002",
+ "programName": "Big Buck Bunny (HLS)",
+ "programDuration": 596,
+ "programType": "video",
+ "transmissionType": 1,
+ "programGenre": 4,
+ "series": "Test Content",
+ "programSeason": "season 1",
+ "programProducer": "Blender",
+ "customAttributes": {
+ "intName": "BBB",
+ "intCategory": "Comedy",
+ "intType": "vod",
+ "intStatus": "public"
+ }
+ }
+ },
+ {
+ "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%3Dpremidpost&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": {
+ "programID": "000002",
+ "programName": "Big Buck Bunny (HLS)",
+ "programDuration": 596,
+ "programType": "video",
+ "transmissionType": 1,
+ "programGenre": 4,
+ "series": "Test Content",
+ "programSeason": "season 1",
+ "programProducer": "Blender",
+ "customAttributes": {
+ "intName": "BBB",
+ "intCategory": "Comedy",
+ "intType": "vod",
+ "intStatus": "public"
+ }
+ }
+ },
+ {
+ "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": {
+ "programID": "000003",
+ "programName": "Livesim",
+ "programDuration": -1,
+ "programType": "video",
+ "transmissionType": 2,
+ "transmissionChannel": "DASHIF1",
+ "programGenre": 1,
+ "series": "Test Content",
+ "programSeason": "season 1",
+ "programProducer": "DASHIF",
+ "customAttributes": {
+ "intName": "LIVESIM",
+ "intCategory": "Generic",
+ "intType": "vod",
+ "intStatus": "public"
+ }
+ }
+ },
+ {
+ "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/gemius/test/pages/preroll-empty.xml"
+ }
+ ]
+ },
+ "metadata": {
+ "programID": "000003",
+ "programName": "Livesim",
+ "programDuration": -1,
+ "programType": "video",
+ "transmissionType": 2,
+ "programGenre": 1,
+ "series": "Test Content",
+ "programSeason": "season 1",
+ "programProducer": "DASHIF",
+ "customAttributes": {
+ "intName": "LIVESIM",
+ "intCategory": "Generic",
+ "intType": "vod",
+ "intStatus": "public"
+ }
+ }
+ },
+ {
+ "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": {
+ "programID": "000003",
+ "programName": "Livesim",
+ "programDuration": -1,
+ "programType": "video",
+ "transmissionType": 2,
+ "programGenre": 1,
+ "series": "Test Content",
+ "programSeason": "season 1",
+ "programProducer": "DASHIF",
+ "customAttributes": {
+ "intName": "LIVESIM",
+ "intCategory": "Generic",
+ "intType": "vod",
+ "intStatus": "public"
+ }
+ }
+ },
+ {
+ "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": {
+ "programID": "000003",
+ "programName": "Livesim",
+ "programDuration": 794,
+ "programType": "video",
+ "transmissionType": 1,
+ "programGenre": 4,
+ "series": "Test Content",
+ "programSeason": "season 1",
+ "programProducer": "Blender Foundation",
+ "customAttributes": {
+ "intName": "TOS",
+ "intCategory": "Generic",
+ "intType": "vod",
+ "intStatus": "public"
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/gemius/tsconfig.json b/gemius/tsconfig.json
new file mode 100644
index 00000000..d416c1ae
--- /dev/null
+++ b/gemius/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist",
+ "declarationDir": "dist/types"
+ },
+ "include": [
+ "src/**/*"
+ ]
+}
diff --git a/gemius/typedoc.json b/gemius/typedoc.json
new file mode 100644
index 00000000..7cafc8ff
--- /dev/null
+++ b/gemius/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": "Gemius Connector"
+}
diff --git a/package-lock.json b/package-lock.json
index 4babe749..4df815c9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,7 +14,8 @@
"nielsen",
"cmcd",
"comscore",
- "adscript"
+ "adscript",
+ "gemius"
],
"devDependencies": {
"@changesets/cli": "^2.27.1",
@@ -45,7 +46,7 @@
},
"adscript": {
"name": "@theoplayer/adscript-connector-web",
- "version": "0.0.1",
+ "version": "0.1.0",
"license": "MIT",
"peerDependencies": {
"theoplayer": "^7.0.0"
@@ -69,7 +70,7 @@
},
"conviva": {
"name": "@theoplayer/conviva-connector-web",
- "version": "2.1.1",
+ "version": "2.1.3",
"license": "MIT",
"devDependencies": {
"@convivainc/conviva-js-coresdk": "^4.7.4"
@@ -85,6 +86,14 @@
}
}
},
+ "gemius": {
+ "name": "@theoplayer/gemius-connector-web",
+ "version": "0.0.1",
+ "license": "MIT",
+ "peerDependencies": {
+ "theoplayer": "^7.0.0"
+ }
+ },
"nielsen": {
"name": "@theoplayer/nielsen-connector-web",
"version": "1.1.2",
@@ -2421,6 +2430,10 @@
"resolved": "conviva",
"link": true
},
+ "node_modules/@theoplayer/gemius-connector-web": {
+ "resolved": "gemius",
+ "link": true
+ },
"node_modules/@theoplayer/nielsen-connector-web": {
"resolved": "nielsen",
"link": true
@@ -8750,7 +8763,7 @@
},
"yospace": {
"name": "@theoplayer/yospace-connector-web",
- "version": "2.2.0",
+ "version": "2.3.0",
"license": "MIT",
"peerDependencies": {
"theoplayer": "^5.0.0 || ^6.0.0 || ^7.0.0"
diff --git a/package.json b/package.json
index b56f2b8f..2676258a 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,8 @@
"nielsen",
"cmcd",
"comscore",
- "adscript"
+ "adscript",
+ "gemius"
],
"scripts": {
"changeset:version": "changeset version && node .changeset/post-process.js",