diff --git a/.changeset/chilly-cars-cry.md b/.changeset/chilly-cars-cry.md
new file mode 100644
index 00000000..05b3f961
--- /dev/null
+++ b/.changeset/chilly-cars-cry.md
@@ -0,0 +1,6 @@
+---
+"@theoplayer/conviva-connector-web": major
+---
+
+Updated dependencies:
+ - @theoplayer/yospace-connector-web@2.0.0
diff --git a/.changeset/config.json b/.changeset/config.json
index bde68675..a4e45737 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -6,6 +6,6 @@
"linked": [],
"access": "public",
"baseBranch": "main",
- "updateInternalDependencies": "patch",
+ "updateInternalDependencies": "minor",
"ignore": []
}
diff --git a/.changeset/format.js b/.changeset/format.js
index 814b384a..1e62dba8 100644
--- a/.changeset/format.js
+++ b/.changeset/format.js
@@ -19,7 +19,12 @@ const getReleaseLine = async (changeset, type, changelogOpts) => {
* @type {import('@changesets/types').GetDependencyReleaseLine}
*/
const getDependencyReleaseLine = async (changesets, dependenciesUpdated, changelogOpts) => {
- return '';
+ if (dependenciesUpdated.length === 0) return "";
+
+ const updatedDependenciesList = dependenciesUpdated.map(
+ (dependency) => ` - ${dependency.name}@${dependency.newVersion}`
+ );
+ return [['- Updated dependencies:'], ...updatedDependenciesList].join("\n");
}
/**
diff --git a/.changeset/poor-ads-explain.md b/.changeset/poor-ads-explain.md
new file mode 100644
index 00000000..a1ef4fdb
--- /dev/null
+++ b/.changeset/poor-ads-explain.md
@@ -0,0 +1,5 @@
+---
+"@theoplayer/yospace-connector-web": minor
+---
+
+Exposed SessionErrorCode.
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000..66ca3899
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,2 @@
+*/dist/
+*/test/pages/
diff --git a/yospace/.eslintrc.json b/.eslintrc.json
similarity index 57%
rename from yospace/.eslintrc.json
rename to .eslintrc.json
index 85899441..6d4bf046 100644
--- a/yospace/.eslintrc.json
+++ b/.eslintrc.json
@@ -4,13 +4,20 @@
"es2021": true,
"jest": true
},
- "extends": ["airbnb-base", "eslint:recommended", "prettier"],
+ "extends": [
+ "airbnb-base",
+ "eslint:recommended",
+ "prettier"
+ ],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
- "plugins": ["@typescript-eslint", "import"],
+ "plugins": [
+ "@typescript-eslint",
+ "import"
+ ],
"rules": {
"class-methods-use-this": 0,
"import/extensions": [
@@ -27,10 +34,23 @@
"import/no-unresolved": 1,
"no-shadow": "off",
"no-use-before-define": 0,
- "max-classes-per-file": ["error", 5],
+ "max-classes-per-file": [
+ "error",
+ 5
+ ],
"no-unused-vars": "off",
- "@typescript-eslint/no-unused-vars": ["warn", {"argsIgnorePattern": "^_"}],
- "@typescript-eslint/no-shadow": 1
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ "argsIgnorePattern": "^_"
+ }
+ ],
+ "@typescript-eslint/no-shadow": 1,
+ "no-useless-return": 0,
+ "prefer-destructuring": 0,
+ "no-console": 0,
+ "no-plusplus": 0,
+ "lines-between-class-members": 0
},
"settings": {
"import/resolver": {
@@ -40,5 +60,8 @@
}
}
},
- "ignorePatterns": ["lib/**/*", "dist/**/*"]
+ "ignorePatterns": [
+ "lib/**/*",
+ "dist/**/*"
+ ]
}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1858237a..93964f31 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,9 @@ jobs:
node-version: 20
cache: 'npm'
- name: Install dependencies
- run: npm ci --workspaces
+ run: npm ci --workspaces --include-workspace-root
+ - name: Build
+ run: npm run build
- name: Create release PR or publish to npm
id: changesets
uses: changesets/action@v1
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 00000000..03d9549e
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/prettier.xml b/.idea/prettier.xml
new file mode 100644
index 00000000..b0c1c68f
--- /dev/null
+++ b/.idea/prettier.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/web-connectors.iml b/.idea/web-connectors.iml
index 24643cc3..23928731 100644
--- a/.idea/web-connectors.iml
+++ b/.idea/web-connectors.iml
@@ -3,8 +3,10 @@
+
+
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 00000000..fb190ee6
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,2 @@
+*/dist/
+*/test/pages/
\ No newline at end of file
diff --git a/yospace/.prettierrc.json b/.prettierrc.json
similarity index 62%
rename from yospace/.prettierrc.json
rename to .prettierrc.json
index b8256925..48e9ab00 100644
--- a/yospace/.prettierrc.json
+++ b/.prettierrc.json
@@ -2,9 +2,13 @@
"printWidth": 120,
"tabWidth": 4,
"trailingComma": "none",
+ "singleQuote": true,
"overrides": [
{
- "files": ["**/*.json", "**/*.yml"],
+ "files": [
+ "**/*.json",
+ "**/*.yml"
+ ],
"options": {
"tabWidth": 2
}
diff --git a/conviva/.gitignore b/conviva/.gitignore
new file mode 100644
index 00000000..bf62141d
--- /dev/null
+++ b/conviva/.gitignore
@@ -0,0 +1,24 @@
+# These are some examples of commonly ignored file patterns.
+# You should customize this list as applicable to your project.
+# Learn more about .gitignore:
+# https://www.atlassian.com/git/tutorials/saving-changes/gitignore
+
+# Node artifact files
+node_modules/
+lib/
+dist/
+
+# JetBrains IDE
+.idea/
+
+# Unit test reports
+TEST*.xml
+
+# Generated by MacOS
+.DS_Store
+
+# Generated by Windows
+Thumbs.db
+
+# THEOplayer build and TypeScript definitions
+local/
diff --git a/conviva/BUILD.md b/conviva/BUILD.md
new file mode 100644
index 00000000..b75f4716
--- /dev/null
+++ b/conviva/BUILD.md
@@ -0,0 +1,59 @@
+# conviva-connector-web
+
+A connector implementing Conviva for web.
+
+## Getting started
+
+```
+npm install
+```
+
+## Testing and code quality
+
+A test stack is set up and can be used by adding tests to the `test/unit/` folder. Run these tests with
+
+```
+npm run test
+```
+
+This project is set up with [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/). You can run these checks with
+
+```
+npm run lint
+npm run prettier
+```
+
+but it's a good idea to set up the necessary IDE integration for both.
+
+CI will automatically verify whether the code passes all necessary quality gates.
+
+## Manual testing
+
+- Run `npm run serve` to start `http-server` in the root folder by running.
+- Run `npm run build` to create the integrations library `conviva-connector.umd.js` under `dist/`.
+- Navigate to `localhost:8080/test/pages/main_umd.html` or add an alternative test page.
+
+## Release process
+
+This release process is based on the assumption that the `master` branch is the default branch, and working branches are branched off from it and PR's back to it.
+It is mostly automated by creating tags with a specific format on the `master` branch.
+
+### Prerequisites
+
+- All the necesary code for the release is present on the `master` branch.
+- The `README.md` file contains the necessary information for an external developer to use the connector. This will be published along with the code.
+- Bitbucket Pipelines has been correctly enabled for the repository.
+ - Github Actions integration is on the roadmap.
+- The necessary variables have been configured for Bitbucket Pipelines:
+ - `NPMRC_CONTENT`: the full content of an `.npmrc` file to be used during publishing. Linebreaks in the file can be substituted by `\n` in the variable.
+ - `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`: The AWS credentials necessary to publish a bundle to CDN.
+ - This might be replaced with a Github release artifact later.
+
+### Creating a release
+
+These steps assume that the new release version is `X.Y.Z`.
+
+- Create a new version bump commit on the `master` branch to bump the project version to the release version.
+- Create a tag on the version bump commit of the format `vX.Y.Z`.
+- Verify that the release pipeline correctly runs for your new tag.
+- Verify that the correct artifacts have been published once the pipeline has completed.
diff --git a/conviva/CHANGELOG.md b/conviva/CHANGELOG.md
new file mode 100644
index 00000000..178ed887
--- /dev/null
+++ b/conviva/CHANGELOG.md
@@ -0,0 +1,82 @@
+# @theoplayer/conviva-connector-web
+
+## 1.3.0
+
+### β¨ Features
+
+- Updated to be compatible with THEOplayer `6.X`.
+
+## 1.2.0
+
+### β¨ Features
+
+- Added error event with addition error information on playback failed.
+
+## 1.1.7
+
+### π Issues
+
+- Removed reporting a buffering state on getting an `emptied` event.
+
+## 1.1.6
+
+### β¨ Features
+
+- Added ad metadata for CSAI.
+
+### π Issues
+
+- Fixed an issue where the ad break position would be incorrectly reported.
+
+## 1.1.5
+
+### π Issues
+
+- Updated yospace connector peer dependency.
+
+## 1.1.4
+
+### π Issues
+
+- Fixed an issue where a session could be created without a source.
+
+## 1.1.3
+
+### Changed
+
+- Made THEOplayer an external dependency.
+
+## 1.1.2
+
+### π Issues
+
+- Fixed passing content length for a live stream or on early error.
+
+## 1.1.1
+
+### Changed
+
+- Updated THEOplayer version to 5.X.
+
+## 1.1.0
+
+### β¨ Features
+
+- Added `setContentInfo` to pass video metadata during playback.
+- Added `setAdInfo` to pass ad metadata during playback.
+- Added `reportPlaybackFailed` to notify Conviva of non-video errors.
+- Added `stopAndStartNewSession` to enable explicitly stopping the current session and starting a new one.
+- Added visibility change reporting.
+- Updated THEOplayer version to 4.X.
+- Improved error handling.
+- Improved default metadata.
+
+### π Issues
+
+- Fixed handling a replay of the same source.
+
+## 1.0.0
+
+### β¨ Features
+
+- Initial release
diff --git a/conviva/LICENSE b/conviva/LICENSE
new file mode 100644
index 00000000..146db44c
--- /dev/null
+++ b/conviva/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 THEO Technologies NV
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/conviva/README.md b/conviva/README.md
new file mode 100644
index 00000000..9a3d23c6
--- /dev/null
+++ b/conviva/README.md
@@ -0,0 +1,103 @@
+# conviva-connector-web
+
+The Conviva connector provides a Conviva integration for THEOplayer.
+
+## Prerequisites
+In order to use this connector, a [THEOplayer](https://www.npmjs.com/package/theoplayer) build with a valid license is required. You can use your existing THEOplayer HTML5 SDK license or request yours via [THEOportal](https://portal.theoplayer.com/).
+
+For setting up a valid Conviva session, you must have access to a [Conviva developer account](https://pulse.conviva.com/) with access to a debug or production key.
+
+## Installation
+
+Install using your favorite package manager for Node (such as `npm` or `yarn`):
+
+### Install via npm
+
+```bash
+npm install @theoplayer/conviva-connector-web
+```
+
+### Install via yarn
+
+```bash
+yarn add @theoplayer/conviva-connector-web
+```
+
+## Usage
+
+First you need to define the Conviva metadata and configuration:
+
+```javascript
+ const convivaMetadata = {
+ ['Conviva.assetName']: 'ASSET_NAME_GOES_HERE',
+ ['Conviva.streamUrl']: 'CUSTOMER_STREAM_URL_GOES_HERE',
+ ['Conviva.streamType']: 'STREAM_TYPE_GOES_HERE', // VOD or LIVE
+ ['Conviva.applicationName']: 'APPLICATION_NAME_GOES_HERE',
+ ['Conviva.viewerId']: 'VIEWER_ID_GOES_HERE'
+ };
+
+ const convivaConfig = {
+ debug: false,
+ gatewayUrl: 'CUSTOMER_GATEWAY_GOES_HERE',
+ customerKey: 'CUSTOMER_KEY_GOES_HERE' // Can be a test or production key.
+ };
+```
+
+Using these configs you can create the Conviva connector with THEOplayer.
+
+* Add as a regular script:
+
+```html
+
+
+```
+
+* Add as an ES2015 module:
+
+```html
+
+```
+
+The Conviva connector is now ready to start a session once THEOplayer starts playing a source.
+
+## Usage with Yospace connector
+
+If you have a Yospace SSAI stream and want to also report ad related events to Conviva, you can use this connector in combination with the Yospace connector: [@theoplayer/yospace-connector-web](https://www.npmjs.com/package/@theoplayer/yospace-connector-web)
+
+After configuring the Yospace connector, can link it to the Conviva connector:
+
+```javascript
+async function setupYospaceConnector(player) {
+ const source = {
+ sources: [
+ {
+ src: "https://csm-e-sdk-validation.bln1.yospace.com/csm/extlive/yospace02,hlssample42.m3u8?yo.br=true&yo.av=4",
+ ssai: {
+ integration: "yospace"
+ }
+ }
+ ]
+ };
+
+ // Create the connectors.
+ const yospace = new THEOplayerYospaceConnector.YospaceConnector(player);
+ const conviva = new THEOplayerConvivaConnector.ConvivaConnector(player, convivaMetadata, convivaConfig);
+
+ // Link ConvivaConnector with the YospaceConnector.
+ conviva.connect(yospace);
+
+ // Set the source.
+ await yospace.setupYospaceSession(source);
+ }
+```
diff --git a/conviva/jest.config.js b/conviva/jest.config.js
new file mode 100644
index 00000000..78e93fe8
--- /dev/null
+++ b/conviva/jest.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ preset: "ts-jest",
+ testEnvironment: "node"
+};
diff --git a/conviva/package.json b/conviva/package.json
new file mode 100644
index 00000000..bdf9ff01
--- /dev/null
+++ b/conviva/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "@theoplayer/conviva-connector-web",
+ "version": "1.3.0",
+ "description": "A connector implementing Conviva for web.",
+ "main": "dist/conviva-connector.umd.js",
+ "repository": "https://github.com/THEOplayer/conviva-connector-web",
+ "homepage": "https://theoplayer.com/",
+ "module": "dist/conviva-connector.esm.js",
+ "types": "dist/conviva-connector.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/conviva-connector.d.ts",
+ "import": "./dist/conviva-connector.esm.js",
+ "require": "./dist/conviva-connector.umd.js"
+ },
+ "./dist/*": "./dist/*",
+ "./package": "./package.json",
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "clean": "rimraf lib dist",
+ "bundle": "rollup -c rollup.config.mjs",
+ "build": "npm run clean && npm run bundle",
+ "serve": "http-server",
+ "test": "jest"
+ },
+ "author": "THEO Technologies NV",
+ "license": "MIT",
+ "files": [
+ "dist/",
+ "CHANGELOG.md",
+ "README.md",
+ "package.json"
+ ],
+ "dependencies": {
+ "@convivainc/conviva-js-coresdk": "^4.6.1"
+ },
+ "peerDependencies": {
+ "theoplayer": "^5.0.0 || ^6.0.0",
+ "@theoplayer/yospace-connector-web": "^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@theoplayer/yospace-connector-web": {
+ "optional": true
+ }
+ }
+}
diff --git a/conviva/rollup.config.mjs b/conviva/rollup.config.mjs
new file mode 100644
index 00000000..a9f8ae73
--- /dev/null
+++ b/conviva/rollup.config.mjs
@@ -0,0 +1,14 @@
+import fs from "node:fs";
+import {getSharedBuildConfiguration} from "../tools/build.mjs";
+
+const {version} = JSON.parse(fs.readFileSync("./package.json", "utf8"));
+
+const fileName = 'conviva-connector';
+const globalName = 'THEOplayerConvivaConnector';
+const banner = `
+/**
+ * THEOplayer Conviva Connector v${version}
+ */`.trim();
+
+
+export default getSharedBuildConfiguration(fileName, globalName, banner);
diff --git a/conviva/src/index.ts b/conviva/src/index.ts
new file mode 100644
index 00000000..f85287c6
--- /dev/null
+++ b/conviva/src/index.ts
@@ -0,0 +1,2 @@
+export { ConvivaConnector } from './integration/ConvivaConnector';
+export { ConvivaConfiguration } from './integration/ConvivaHandler';
diff --git a/conviva/src/integration/ConvivaCallbackFunctions.ts b/conviva/src/integration/ConvivaCallbackFunctions.ts
new file mode 100644
index 00000000..44104790
--- /dev/null
+++ b/conviva/src/integration/ConvivaCallbackFunctions.ts
@@ -0,0 +1,80 @@
+import { Constants, ConvivaUtils } from '@convivainc/conviva-js-coresdk';
+
+export const CONVIVA_CALLBACK_FUNCTIONS: ConvivaUtils = {
+ [Constants.CallbackFunctions.CONSOLE_LOG](message: string, logLevel: number) {
+ if (typeof console === 'undefined') {
+ return;
+ }
+ if (logLevel === Constants.LogLevel.DEBUG || logLevel === Constants.LogLevel.INFO) {
+ console.log(message);
+ } else if (console.warn && logLevel === Constants.LogLevel.WARNING) {
+ console.warn(message);
+ } else if (console.error && logLevel === Constants.LogLevel.ERROR) {
+ console.error(message);
+ }
+ },
+ [Constants.CallbackFunctions.MAKE_REQUEST](httpMethod, url, data, contentType, timeoutMs, callback) {
+ const xmlHttpReq = new XMLHttpRequest();
+ xmlHttpReq.open(httpMethod, url, true);
+ if (contentType && xmlHttpReq.overrideMimeType) {
+ xmlHttpReq.overrideMimeType(contentType);
+ }
+ if (contentType && xmlHttpReq.setRequestHeader) {
+ xmlHttpReq.setRequestHeader('Content-Type', contentType);
+ }
+ if (timeoutMs > 0) {
+ xmlHttpReq.timeout = timeoutMs;
+ xmlHttpReq.ontimeout = () => {
+ // Often this callback will be called after onreadystatechange.
+ // The first callback called will cleanup the other to prevent duplicate responses.
+ xmlHttpReq.ontimeout = null;
+ xmlHttpReq.onreadystatechange = null;
+ if (callback) {
+ callback(false, `timeout after ${timeoutMs} ms`);
+ }
+ };
+ }
+
+ xmlHttpReq.onreadystatechange = () => {
+ if (xmlHttpReq.readyState === 4) {
+ xmlHttpReq.ontimeout = null;
+ xmlHttpReq.onreadystatechange = null;
+ if (xmlHttpReq.status === 200) {
+ if (callback) {
+ callback(true, xmlHttpReq.responseText);
+ }
+ } else if (callback) {
+ callback(false, `http status ${xmlHttpReq.status}`);
+ }
+ }
+ };
+ xmlHttpReq.send(data);
+ },
+ [Constants.CallbackFunctions.SAVE_DATA](storageSpace, storageKey, data, callback) {
+ const localStorageKey = `${storageSpace}.${storageKey}`;
+ try {
+ localStorage.setItem(localStorageKey, data);
+ callback(true, '');
+ } catch (e: any) {
+ callback(false, e.toString());
+ }
+ },
+ [Constants.CallbackFunctions.LOAD_DATA](storageSpace, storageKey, callback) {
+ const localStorageKey = `${storageSpace}.${storageKey}`;
+ try {
+ const data = localStorage.getItem(localStorageKey) ?? '';
+ callback(true, data);
+ } catch (e: any) {
+ callback(false, e.toString());
+ }
+ },
+ [Constants.CallbackFunctions.GET_EPOCH_TIME_IN_MS]() {
+ return Date.now();
+ },
+ [Constants.CallbackFunctions.CREATE_TIMER](timerAction, intervalMs) {
+ const timerId = setInterval(timerAction, intervalMs);
+ return () => {
+ clearInterval(timerId);
+ };
+ }
+};
diff --git a/conviva/src/integration/ConvivaConnector.ts b/conviva/src/integration/ConvivaConnector.ts
new file mode 100644
index 00000000..72ccbbc3
--- /dev/null
+++ b/conviva/src/integration/ConvivaConnector.ts
@@ -0,0 +1,64 @@
+import { ChromelessPlayer } from 'theoplayer';
+import { ConvivaMetadata } from '@convivainc/conviva-js-coresdk';
+import { YospaceConnector } from '@theoplayer/yospace-connector-web';
+import { ConvivaConfiguration, ConvivaHandler } from './ConvivaHandler';
+
+export class ConvivaConnector {
+ private convivaHandler: ConvivaHandler;
+
+ constructor(player: ChromelessPlayer, convivaMetadata: ConvivaMetadata, convivaConfig: ConvivaConfiguration) {
+ this.convivaHandler = new ConvivaHandler(player, convivaMetadata, convivaConfig);
+ }
+
+ /**
+ * Optionally connects the ConvivaConnector to the YospaceConnector to report SSAI.
+ * @param connector the YospaceConnector
+ */
+ connect(connector: YospaceConnector): void {
+ this.convivaHandler.connect(connector);
+ }
+
+ /**
+ * Sets Conviva metadata on the Conviva video analytics.
+ * @param metadata object of key value pairs
+ */
+ setContentInfo(metadata: ConvivaMetadata): void {
+ this.convivaHandler.setContentInfo(metadata);
+ }
+
+ /**
+ * Sets Conviva metadata on the Conviva ad analytics.
+ * @param metadata object of key value pairs
+ */
+ setAdInfo(metadata: ConvivaMetadata): void {
+ this.convivaHandler.setAdInfo(metadata);
+ }
+
+ /**
+ * Reports an error to the Conviva session and closes the session.
+ * @param errorMessage string explaining what the error is.
+ */
+ reportPlaybackFailed(errorMessage: string): void {
+ this.convivaHandler.reportPlaybackFailed(errorMessage);
+ }
+
+ /**
+ * Explicitly stop the current session and start a new one.
+ *
+ * This can be used to manually mark the start of a new session during a live stream,
+ * for example when a new program starts.
+ * By default, new sessions are only started on play-out of a new source, or for an ad break.
+ *
+ * @param metadata object of key value pairs.
+ */
+ stopAndStartNewSession(metadata: ConvivaMetadata): void {
+ this.convivaHandler.stopAndStartNewSession(metadata);
+ }
+
+ /**
+ * Stops video and ad analytics and closes all sessions.
+ */
+ destroy(): void {
+ this.convivaHandler.destroy();
+ }
+}
diff --git a/conviva/src/integration/ConvivaHandler.ts b/conviva/src/integration/ConvivaHandler.ts
new file mode 100644
index 00000000..86c2a8dc
--- /dev/null
+++ b/conviva/src/integration/ConvivaHandler.ts
@@ -0,0 +1,369 @@
+import { ChromelessPlayer, SourceDescription, VideoQuality } from 'theoplayer';
+import { AdAnalytics, Analytics, Constants, ConvivaMetadata, VideoAnalytics } from '@convivainc/conviva-js-coresdk';
+import { YospaceConnector } from '@theoplayer/yospace-connector-web';
+import { CONVIVA_CALLBACK_FUNCTIONS } from './ConvivaCallbackFunctions';
+import {
+ calculateBufferLength,
+ calculateConvivaOptions,
+ collectContentMetadata,
+ collectDeviceMetadata,
+ collectPlayerInfo,
+ flattenAndStringifyObject
+} from '../utils/Utils';
+import { CsaiAdReporter } from './ads/CsaiAdReporter';
+import { YospaceAdReporter } from './ads/YospaceAdReporter';
+import { VerizonAdReporter } from './ads/VerizonAdReporter';
+
+export interface ConvivaConfiguration {
+ customerKey: string;
+ debug?: boolean;
+ gatewayUrl?: string;
+}
+
+export class ConvivaHandler {
+ private readonly player: ChromelessPlayer;
+ private readonly convivaMetadata: ConvivaMetadata;
+ private readonly convivaConfig: ConvivaConfiguration;
+ private customMetadata: ConvivaMetadata = {};
+
+ private convivaVideoAnalytics: VideoAnalytics | undefined;
+ private convivaAdAnalytics: AdAnalytics | undefined;
+
+ private adReporter: CsaiAdReporter | undefined;
+ private yospaceAdReporter: YospaceAdReporter | undefined;
+ private verizonAdReporter: VerizonAdReporter | undefined;
+
+ private currentSource: SourceDescription | undefined;
+ private playbackRequested: boolean = false;
+
+ private yospaceConnector: YospaceConnector | undefined;
+
+ constructor(player: ChromelessPlayer, convivaMetaData: ConvivaMetadata, config: ConvivaConfiguration) {
+ this.player = player;
+ this.convivaMetadata = convivaMetaData;
+ this.customMetadata = convivaMetaData;
+ this.convivaConfig = config;
+ this.currentSource = player.source;
+
+ Analytics.setDeviceMetadata(collectDeviceMetadata());
+ Analytics.init(
+ this.convivaConfig.customerKey,
+ CONVIVA_CALLBACK_FUNCTIONS,
+ calculateConvivaOptions(this.convivaConfig)
+ );
+
+ this.addEventListeners();
+ }
+
+ private initializeSession(): void {
+ this.convivaVideoAnalytics = Analytics.buildVideoAnalytics();
+ this.convivaVideoAnalytics.setPlayerInfo(collectPlayerInfo());
+ this.convivaVideoAnalytics.setCallback(this.convivaCallback);
+
+ this.convivaAdAnalytics = Analytics.buildAdAnalytics(this.convivaVideoAnalytics);
+
+ if (this.player.ads !== undefined) {
+ this.adReporter = new CsaiAdReporter(
+ this.player,
+ this.convivaVideoAnalytics,
+ this.convivaAdAnalytics,
+ () => this.customMetadata
+ );
+ }
+
+ if (this.player.verizonMedia !== undefined) {
+ this.verizonAdReporter = new VerizonAdReporter(
+ this.player,
+ this.convivaVideoAnalytics,
+ this.convivaAdAnalytics
+ );
+ }
+
+ if (this.yospaceConnector !== undefined) {
+ this.yospaceAdReporter = new YospaceAdReporter(
+ this.player,
+ this.convivaVideoAnalytics!,
+ this.convivaAdAnalytics!,
+ this.yospaceConnector
+ );
+ }
+ }
+
+ connect(connector: YospaceConnector): void {
+ if (!this.convivaVideoAnalytics) {
+ this.initializeSession();
+ }
+ this.yospaceAdReporter?.destroy();
+ this.yospaceAdReporter = new YospaceAdReporter(
+ this.player,
+ this.convivaVideoAnalytics!,
+ this.convivaAdAnalytics!,
+ connector
+ );
+ this.yospaceConnector = connector;
+ }
+
+ setContentInfo(metadata: ConvivaMetadata): void {
+ if (!this.convivaVideoAnalytics) {
+ this.initializeSession();
+ }
+ this.customMetadata = { ...this.customMetadata, ...metadata };
+ this.convivaVideoAnalytics!.setContentInfo(metadata);
+ }
+
+ setAdInfo(metadata: ConvivaMetadata): void {
+ if (!this.convivaVideoAnalytics) {
+ this.initializeSession();
+ }
+ this.convivaAdAnalytics!.setAdInfo(metadata);
+ }
+
+ reportPlaybackFailed(errorMessage: string): void {
+ this.convivaVideoAnalytics?.reportPlaybackFailed(errorMessage);
+ this.releaseSession();
+ }
+
+ stopAndStartNewSession(metadata: ConvivaMetadata): void {
+ this.maybeReportPlaybackEnded();
+ this.maybeReportPlaybackRequested();
+ this.setContentInfo(metadata);
+ if (this.player.paused) {
+ this.onPause();
+ } else {
+ this.onPlaying();
+ }
+ }
+
+ private addEventListeners(): void {
+ this.player.addEventListener('play', this.onPlay);
+ this.player.addEventListener('playing', this.onPlaying);
+ this.player.addEventListener('pause', this.onPause);
+ this.player.addEventListener('waiting', this.onWaiting);
+ this.player.addEventListener('seeking', this.onSeeking);
+ this.player.addEventListener('seeked', this.onSeeked);
+ this.player.addEventListener('error', this.onError);
+ this.player.addEventListener('segmentnotfound', this.onSegmentNotFound);
+ this.player.addEventListener('sourcechange', this.onSourceChange);
+ this.player.addEventListener('ended', this.onEnded);
+ this.player.addEventListener('durationchange', this.onDurationChange);
+ this.player.addEventListener('destroy', this.onDestroy);
+
+ this.player.network.addEventListener('offline', this.onNetworkOffline);
+
+ document.addEventListener('visibilitychange', this.onVisibilityChange);
+ window.addEventListener('beforeunload', this.onBeforeUnload);
+ }
+
+ private removeEventListeners(): void {
+ this.player.removeEventListener('play', this.onPlay);
+ this.player.removeEventListener('playing', this.onPlaying);
+ this.player.removeEventListener('pause', this.onPause);
+ this.player.removeEventListener('waiting', this.onWaiting);
+ this.player.removeEventListener('seeking', this.onSeeking);
+ this.player.removeEventListener('seeked', this.onSeeked);
+ this.player.removeEventListener('error', this.onError);
+ this.player.removeEventListener('segmentnotfound', this.onSegmentNotFound);
+ this.player.removeEventListener('sourcechange', this.onSourceChange);
+ this.player.removeEventListener('ended', this.onEnded);
+ this.player.removeEventListener('durationchange', this.onDurationChange);
+ this.player.removeEventListener('destroy', this.onDestroy);
+
+ this.player.network.removeEventListener('offline', this.onNetworkOffline);
+
+ document.removeEventListener('visibilitychange', this.onVisibilityChange);
+ window.removeEventListener('beforeunload', this.onBeforeUnload);
+ }
+
+ private convivaCallback = () => {
+ const currentTime = this.player.currentTime * 1000;
+ this.convivaVideoAnalytics!.reportPlaybackMetric(Constants.Playback.PLAY_HEAD_TIME, currentTime);
+ this.convivaVideoAnalytics!.reportPlaybackMetric(
+ Constants.Playback.BUFFER_LENGTH,
+ calculateBufferLength(this.player)
+ );
+ this.convivaVideoAnalytics!.reportPlaybackMetric(
+ Constants.Playback.RESOLUTION,
+ this.player.videoWidth,
+ this.player.videoHeight
+ );
+ const activeVideoTrack = this.player.videoTracks[0];
+ const activeQuality = activeVideoTrack?.activeQuality;
+ if (activeQuality) {
+ const frameRate = (activeQuality as VideoQuality).frameRate;
+ this.convivaVideoAnalytics!.reportPlaybackMetric(
+ Constants.Playback.BITRATE,
+ activeQuality.bandwidth / 1000
+ );
+ if (frameRate) {
+ this.convivaVideoAnalytics!.reportPlaybackMetric(Constants.Playback.RENDERED_FRAMERATE, frameRate);
+ }
+ }
+ };
+
+ private readonly onPlay = () => {
+ this.maybeReportPlaybackRequested();
+ };
+
+ private maybeReportPlaybackRequested() {
+ if (!this.playbackRequested && this.player.source !== undefined) {
+ this.playbackRequested = true;
+ if (!this.convivaVideoAnalytics) {
+ this.initializeSession();
+ }
+ this.convivaVideoAnalytics!.reportPlaybackRequested(
+ collectContentMetadata(this.player, this.convivaMetadata)
+ );
+ this.reportMetadata();
+ }
+ }
+
+ private maybeReportPlaybackEnded() {
+ if (this.playbackRequested) {
+ this.convivaVideoAnalytics?.reportPlaybackEnded();
+ this.releaseSession();
+ this.playbackRequested = false;
+ }
+ }
+
+ private reportMetadata() {
+ const src = this.player.src ?? '';
+ const streamType = this.player.duration === Infinity ? Constants.StreamType.LIVE : Constants.StreamType.VOD;
+ const assetName = this.customMetadata[Constants.ASSET_NAME] ?? this.currentSource?.metadata?.title ?? 'NA';
+ const playerName = this.customMetadata[Constants.PLAYER_NAME] ?? 'THEOplayer';
+ const metadata = {
+ [Constants.STREAM_URL]: src,
+ [Constants.IS_LIVE]: streamType,
+ [Constants.ASSET_NAME]: assetName,
+ [Constants.PLAYER_NAME]: playerName
+ };
+ this.setContentInfo(metadata);
+ }
+
+ private readonly onPlaying = () => {
+ this.convivaVideoAnalytics?.reportPlaybackMetric(
+ Constants.Playback.PLAYER_STATE,
+ Constants.PlayerState.PLAYING
+ );
+ };
+
+ private readonly onPause = () => {
+ this.convivaVideoAnalytics?.reportPlaybackMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PAUSED);
+ };
+
+ private readonly onWaiting = () => {
+ this.convivaVideoAnalytics?.reportPlaybackMetric(
+ Constants.Playback.PLAYER_STATE,
+ Constants.PlayerState.BUFFERING
+ );
+ };
+
+ private readonly onSeeking = () => {
+ this.convivaVideoAnalytics?.reportPlaybackMetric(Constants.Playback.SEEK_STARTED);
+ };
+
+ private readonly onSeeked = () => {
+ this.convivaVideoAnalytics?.reportPlaybackMetric(Constants.Playback.SEEK_ENDED);
+ };
+
+ private readonly onError = () => {
+ const metadata: ConvivaMetadata = {};
+ if (Number.isNaN(this.player.duration)) {
+ metadata[Constants.DURATION] = -1;
+ }
+ const error = this.player.errorObject;
+
+ // Optionally report error details, which should be a flat {[key: string]: string} object.
+ if (error?.cause) {
+ try {
+ const errorDetails = flattenAndStringifyObject(error?.cause);
+ if (Object.keys(errorDetails).length > 0) {
+ this.convivaVideoAnalytics?.reportPlaybackEvent('ErrorDetailsEvent', errorDetails);
+ }
+ } catch (ignore) {
+ // Failed to stringify body
+ }
+ }
+
+ this.convivaVideoAnalytics?.reportPlaybackFailed(error?.message ?? 'Fatal error occurred', metadata);
+
+ this.releaseSession();
+ };
+
+ private readonly onSegmentNotFound = () => {
+ this.convivaVideoAnalytics?.reportPlaybackError(
+ 'A Video Playback Failure has occurred: Segment not found',
+ Constants.ErrorSeverity.FATAL
+ );
+ };
+
+ private readonly onNetworkOffline = () => {
+ this.convivaVideoAnalytics?.reportPlaybackError(
+ 'A Video Playback Failure has occurred: Waiting for the manifest to come back online',
+ Constants.ErrorSeverity.FATAL
+ );
+ };
+
+ // eslint-disable-next-line class-methods-use-this
+ private readonly onVisibilityChange = () => {
+ if (document.visibilityState === 'visible') {
+ Analytics.reportAppForegrounded();
+ } else {
+ Analytics.reportAppBackgrounded();
+ }
+ };
+
+ private readonly onBeforeUnload = () => {
+ this.maybeReportPlaybackEnded();
+ };
+
+ private readonly onSourceChange = () => {
+ this.maybeReportPlaybackEnded();
+ this.currentSource = this.player.source;
+ };
+
+ private readonly onEnded = () => {
+ this.convivaVideoAnalytics?.reportPlaybackMetric(
+ Constants.Playback.PLAYER_STATE,
+ Constants.PlayerState.STOPPED
+ );
+ this.maybeReportPlaybackEnded();
+ };
+
+ private readonly onDurationChange = () => {
+ const contentInfo: ConvivaMetadata = {};
+ const duration = this.player.duration;
+ if (duration === Infinity) {
+ contentInfo[Constants.IS_LIVE] = Constants.StreamType.LIVE;
+ } else {
+ contentInfo[Constants.IS_LIVE] = Constants.StreamType.VOD;
+ contentInfo[Constants.DURATION] = duration;
+ }
+ this.convivaVideoAnalytics?.setContentInfo(contentInfo);
+ };
+
+ private readonly onDestroy = () => {
+ this.destroy();
+ };
+
+ private releaseSession(): void {
+ this.adReporter?.destroy();
+ this.verizonAdReporter?.destroy();
+ this.yospaceAdReporter?.destroy();
+ this.adReporter = undefined;
+ this.verizonAdReporter = undefined;
+ this.yospaceAdReporter = undefined;
+
+ this.convivaAdAnalytics?.release();
+ this.convivaVideoAnalytics?.release();
+ this.convivaAdAnalytics = undefined;
+ this.convivaVideoAnalytics = undefined;
+
+ this.customMetadata = {};
+ }
+
+ destroy(): void {
+ this.maybeReportPlaybackEnded();
+ this.removeEventListeners();
+ Analytics.release();
+ }
+}
diff --git a/conviva/src/integration/ads/CsaiAdReporter.ts b/conviva/src/integration/ads/CsaiAdReporter.ts
new file mode 100644
index 00000000..76b0da9a
--- /dev/null
+++ b/conviva/src/integration/ads/CsaiAdReporter.ts
@@ -0,0 +1,158 @@
+import { Ad, AdBreak, ChromelessPlayer, GoogleImaAd } from 'theoplayer';
+import { AdAnalytics, Constants, ConvivaMetadata, VideoAnalytics } from '@convivainc/conviva-js-coresdk';
+import { calculateCurrentAdBreakInfo, collectAdMetadata, collectPlayerInfo } from '../../utils/Utils';
+
+export class CsaiAdReporter {
+ private readonly player: ChromelessPlayer;
+ private readonly convivaVideoAnalytics: VideoAnalytics;
+ private readonly convivaAdAnalytics: AdAnalytics;
+ private readonly contentInfo: () => ConvivaMetadata;
+
+ private currentAdBreak: AdBreak | undefined;
+ private adBreakCounter: number = 1;
+
+ constructor(
+ player: ChromelessPlayer,
+ videoAnalytics: VideoAnalytics,
+ adAnalytics: AdAnalytics,
+ contentInfo: () => ConvivaMetadata
+ ) {
+ this.player = player;
+ this.convivaVideoAnalytics = videoAnalytics;
+ this.convivaAdAnalytics = adAnalytics;
+ this.convivaAdAnalytics.setCallback(this.convivaAdCallback);
+ this.convivaAdAnalytics.setAdPlayerInfo(collectPlayerInfo());
+ this.contentInfo = contentInfo;
+ this.addEventListeners();
+ }
+
+ private readonly onAdBreakBegin = (event: any) => {
+ this.currentAdBreak = event.ad as AdBreak;
+ this.convivaVideoAnalytics.reportAdBreakStarted(
+ Constants.AdType.CLIENT_SIDE,
+ Constants.AdPlayer.CONTENT,
+ calculateCurrentAdBreakInfo(this.currentAdBreak, this.adBreakCounter)
+ );
+ this.adBreakCounter++;
+ };
+
+ private readonly onAdBreakEnd = () => {
+ this.convivaVideoAnalytics.reportAdBreakEnded();
+ this.currentAdBreak = undefined;
+ };
+
+ private readonly onAdBegin = (event: any) => {
+ const currentAd = event.ad as Ad;
+ if (currentAd.type !== 'linear') {
+ return;
+ }
+ const adMetadata = collectAdMetadata(currentAd);
+
+ // Every session ad or content has its session ID. In order to βattachβ an ad to its respective content session,
+ // there are two tags that are critical:
+ // - `c3.csid`: the contentβs sessionID;
+ // - `contentAssetName`: the content's assetName.
+ // @ts-ignore: getSessionId() is not present in type declarations.
+ adMetadata['c3.csid'] = `${this.convivaVideoAnalytics.getSessionId()}`;
+ adMetadata.contentAssetName =
+ this.contentInfo()[Constants.ASSET_NAME] ?? this.player.source?.metadata?.title ?? 'NA';
+
+ this.convivaAdAnalytics.setAdInfo(adMetadata);
+ this.convivaAdAnalytics.reportAdLoaded(adMetadata);
+ this.convivaAdAnalytics.reportAdStarted(adMetadata);
+ this.convivaAdAnalytics.reportAdMetric(
+ Constants.Playback.RESOLUTION,
+ this.player.videoWidth,
+ this.player.videoHeight
+ );
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.BITRATE, (currentAd as GoogleImaAd).bitrate || 0);
+ };
+
+ private readonly onAdEnd = (event: any) => {
+ const currentAd = event.ad as Ad;
+ if (currentAd.type !== 'linear') {
+ return;
+ }
+ this.convivaAdAnalytics.reportAdEnded();
+ };
+
+ private readonly onAdSkip = () => {
+ if (!this.currentAdBreak) {
+ return;
+ }
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.STOPPED);
+ };
+
+ private readonly onAdBuffering = () => {
+ if (!this.currentAdBreak) {
+ return;
+ }
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.BUFFERING);
+ };
+
+ private readonly onAdError = (event: any) => {
+ this.convivaAdAnalytics.reportAdFailed(event.message || 'Ad Request Failed');
+ };
+
+ private readonly onPlaying = () => {
+ if (!this.currentAdBreak) {
+ return;
+ }
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PLAYING);
+ };
+
+ private readonly onPause = () => {
+ if (!this.currentAdBreak) {
+ return;
+ }
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PAUSED);
+ };
+
+ private convivaAdCallback = () => {
+ const currentTime = this.player.currentTime * 1000;
+ this.convivaAdAnalytics!.reportAdMetric(Constants.Playback.PLAY_HEAD_TIME, currentTime);
+ };
+
+ private addEventListeners(): void {
+ this.player.addEventListener('playing', this.onPlaying);
+ this.player.addEventListener('pause', this.onPause);
+ if (this.player.ads === undefined) {
+ // should not happen
+ return;
+ }
+ 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);
+ this.player.ads.addEventListener('adbuffering', this.onAdBuffering);
+ this.player.ads.addEventListener('aderror', this.onAdError);
+ }
+
+ private removeEventListeners(): void {
+ this.player.removeEventListener('playing', this.onPlaying);
+ this.player.removeEventListener('pause', this.onPause);
+ if (this.player.ads === undefined) {
+ // should not happen
+ return;
+ }
+ 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);
+ this.player.ads.removeEventListener('adbuffering', this.onAdBuffering);
+ this.player.ads.removeEventListener('aderror', this.onAdError);
+ }
+
+ reset(): void {
+ this.adBreakCounter = 0;
+ }
+
+ destroy(): void {
+ if (this.currentAdBreak) {
+ this.onAdBreakEnd();
+ }
+ this.removeEventListeners();
+ }
+}
diff --git a/conviva/src/integration/ads/VerizonAdReporter.ts b/conviva/src/integration/ads/VerizonAdReporter.ts
new file mode 100644
index 00000000..ba35cc83
--- /dev/null
+++ b/conviva/src/integration/ads/VerizonAdReporter.ts
@@ -0,0 +1,126 @@
+import {
+ ChromelessPlayer,
+ VerizonMediaAdBeginEvent,
+ VerizonMediaAdBreak,
+ VerizonMediaAdBreakBeginEvent,
+ VerizonMediaAddAdBreakEvent,
+ VerizonMediaRemoveAdBreakEvent,
+ VideoQuality
+} from 'theoplayer';
+import { AdAnalytics, Constants, VideoAnalytics } from '@convivainc/conviva-js-coresdk';
+import { calculateVerizonAdBreakInfo, collectPlayerInfo, collectVerizonAdMetadata } from '../../utils/Utils';
+
+export class VerizonAdReporter {
+ private readonly player: ChromelessPlayer;
+ private readonly convivaVideoAnalytics: VideoAnalytics;
+ private readonly convivaAdAnalytics: AdAnalytics;
+
+ private currentAdBreak: VerizonMediaAdBreak | undefined;
+ private adBreakCounter: number = 1;
+
+ constructor(player: ChromelessPlayer, videoAnalytics: VideoAnalytics, adAnalytics: AdAnalytics) {
+ this.player = player;
+ this.convivaVideoAnalytics = videoAnalytics;
+ this.convivaAdAnalytics = adAnalytics;
+ this.convivaAdAnalytics.setAdPlayerInfo(collectPlayerInfo());
+ this.addEventListeners();
+ }
+
+ private onAdBreakBegin = (event: VerizonMediaAdBreakBeginEvent) => {
+ this.currentAdBreak = event.adBreak;
+ this.convivaVideoAnalytics.reportAdBreakStarted(
+ Constants.AdType.SERVER_SIDE,
+ Constants.AdPlayer.CONTENT,
+ calculateVerizonAdBreakInfo(this.currentAdBreak, this.adBreakCounter)
+ );
+ this.adBreakCounter++;
+ };
+
+ private onAdBreakEnd = () => {
+ this.convivaVideoAnalytics.reportAdBreakEnded();
+ this.currentAdBreak = undefined;
+ };
+
+ private onAdBreakSkip = () => {
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.STOPPED);
+ this.convivaVideoAnalytics.reportAdBreakEnded();
+ this.currentAdBreak = undefined;
+ };
+
+ private onAdBegin = (event: VerizonMediaAdBeginEvent) => {
+ const adMetadata = collectVerizonAdMetadata(event.ad);
+ this.convivaAdAnalytics.setAdInfo(adMetadata);
+ this.convivaAdAnalytics.reportAdStarted(adMetadata);
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PLAYING);
+ this.convivaAdAnalytics.reportAdMetric(
+ Constants.Playback.RESOLUTION,
+ this.player.videoWidth,
+ this.player.videoHeight
+ );
+ const activeVideoTrack = this.player.videoTracks[0];
+ const activeQuality = activeVideoTrack?.activeQuality;
+ if (activeQuality) {
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.BITRATE, activeQuality.bandwidth / 1000);
+ const frameRate = (activeQuality as VideoQuality).frameRate;
+ if (frameRate) {
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.RENDERED_FRAMERATE, frameRate);
+ }
+ }
+ };
+
+ private onAdEnd = () => {
+ this.convivaAdAnalytics.reportAdEnded();
+ };
+
+ private onAddAdBreak = (event: VerizonMediaAddAdBreakEvent) => {
+ const adBreak = event.adBreak;
+ adBreak.addEventListener('adbreakbegin', this.onAdBreakBegin);
+ adBreak.addEventListener('adbreakend', this.onAdBreakEnd);
+ adBreak.addEventListener('adbreakskip', this.onAdBreakSkip);
+ for (let i = 0; i < adBreak.ads.length; i++) {
+ adBreak.ads[i].addEventListener('adbegin', this.onAdBegin);
+ adBreak.ads[i].addEventListener('adend', this.onAdEnd);
+ }
+ };
+
+ private onRemoveAdBreak = (event: VerizonMediaRemoveAdBreakEvent) => {
+ const adBreak = event.adBreak;
+ adBreak.removeEventListener('adbreakbegin', this.onAdBreakBegin);
+ adBreak.removeEventListener('adbreakend', this.onAdBreakEnd);
+ adBreak.removeEventListener('adbreakskip', this.onAdBreakSkip);
+ for (let i = 0; i < adBreak.ads.length; i++) {
+ adBreak.ads[i].removeEventListener('adbegin', this.onAdBegin);
+ adBreak.ads[i].removeEventListener('adend', this.onAdEnd);
+ }
+ };
+
+ private readonly onPlaying = () => {
+ if (this.currentAdBreak) {
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PLAYING);
+ }
+ };
+
+ private readonly onPause = () => {
+ if (this.currentAdBreak) {
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PAUSED);
+ }
+ };
+
+ private addEventListeners() {
+ this.player.verizonMedia!.ads.adBreaks.addEventListener('addadbreak', this.onAddAdBreak);
+ this.player.verizonMedia!.ads.adBreaks.addEventListener('removeadbreak', this.onRemoveAdBreak);
+ this.player.addEventListener('playing', this.onPlaying);
+ this.player.addEventListener('pause', this.onPause);
+ }
+
+ private removeEventListeners() {
+ this.player.verizonMedia!.ads.adBreaks.removeEventListener('addadbreak', this.onAddAdBreak);
+ this.player.verizonMedia!.ads.adBreaks.removeEventListener('removeadbreak', this.onRemoveAdBreak);
+ this.player.removeEventListener('playing', this.onPlaying);
+ this.player.removeEventListener('pause', this.onPause);
+ }
+
+ destroy() {
+ this.removeEventListeners();
+ }
+}
diff --git a/conviva/src/integration/ads/YospaceAdReporter.ts b/conviva/src/integration/ads/YospaceAdReporter.ts
new file mode 100644
index 00000000..a7e72e92
--- /dev/null
+++ b/conviva/src/integration/ads/YospaceAdReporter.ts
@@ -0,0 +1,123 @@
+import { ChromelessPlayer, VideoQuality } from 'theoplayer';
+import { AdAnalytics, Constants, VideoAnalytics } from '@convivainc/conviva-js-coresdk';
+import {
+ AdBreak,
+ AdVert,
+ AnalyticEventObserver,
+ SessionErrorCode,
+ YospaceConnector
+} from '@theoplayer/yospace-connector-web';
+import { collectPlayerInfo, collectYospaceAdMetadata } from '../../utils/Utils';
+
+export class YospaceAdReporter {
+ private readonly player: ChromelessPlayer;
+ private readonly convivaAdAnalytics: AdAnalytics;
+ private readonly convivaVideoAnalytics: VideoAnalytics;
+ private readonly yospaceConnector: YospaceConnector;
+
+ private readonly observer: AnalyticEventObserver;
+
+ private currentAdBreak: AdBreak | undefined;
+
+ constructor(
+ player: ChromelessPlayer,
+ videoAnalytics: VideoAnalytics,
+ adAnalytics: AdAnalytics,
+ yospace: YospaceConnector
+ ) {
+ this.player = player;
+ this.convivaVideoAnalytics = videoAnalytics;
+ this.convivaAdAnalytics = adAnalytics;
+ this.yospaceConnector = yospace;
+ this.observer = {
+ onAnalyticUpdate: () => {},
+ onAdvertBreakEarlyReturn: (_: AdBreak) => {},
+ onAdvertBreakStart: this.onYospaceAdBreakStart,
+ onAdvertBreakEnd: this.onYospaceAdBreakEnd,
+ onAdvertStart: this.onYospaceAdvertStart,
+ onAdvertEnd: this.onYospaceAdvertEnd,
+ onSessionError: this.onYospaceSessionError,
+ onTrackingError: () => {},
+ onTrackingEvent: (_: string) => {}
+ };
+ this.yospaceConnector.addEventListener('sessionavailable', () => {
+ console.log('session initialized');
+ this.yospaceConnector.registerAnalyticEventObserver(this.observer);
+ });
+ this.convivaAdAnalytics.setAdPlayerInfo(collectPlayerInfo());
+ this.addEventListeners();
+ }
+
+ private readonly onYospaceAdBreakStart = (adBreak: AdBreak) => {
+ this.currentAdBreak = adBreak;
+ this.convivaVideoAnalytics.reportAdBreakStarted(Constants.AdType.SERVER_SIDE, Constants.AdPlayer.CONTENT);
+ };
+
+ private readonly onYospaceAdvertStart = (advert: AdVert) => {
+ if (this.currentAdBreak === undefined) {
+ return;
+ }
+ const adMetadata = collectYospaceAdMetadata(this.player, advert);
+ this.convivaAdAnalytics.setAdInfo(adMetadata);
+ this.convivaAdAnalytics.reportAdStarted(adMetadata);
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PLAYING);
+ this.convivaAdAnalytics.reportAdMetric(
+ Constants.Playback.RESOLUTION,
+ this.player.videoWidth,
+ this.player.videoHeight
+ );
+ const activeVideoTrack = this.player.videoTracks[0];
+ const activeQuality = activeVideoTrack?.activeQuality;
+ if (activeQuality) {
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.BITRATE, activeQuality.bandwidth / 1000);
+ const frameRate = (activeQuality as VideoQuality).frameRate;
+ if (frameRate) {
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.RENDERED_FRAMERATE, frameRate);
+ }
+ }
+ };
+
+ private readonly onPlaying = () => {
+ if (this.currentAdBreak) {
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PLAYING);
+ }
+ };
+
+ private readonly onPause = () => {
+ if (this.currentAdBreak) {
+ this.convivaAdAnalytics.reportAdMetric(Constants.Playback.PLAYER_STATE, Constants.PlayerState.PAUSED);
+ }
+ };
+
+ private readonly onYospaceAdBreakEnd = () => {
+ this.convivaVideoAnalytics.reportAdBreakEnded();
+ this.currentAdBreak = undefined;
+ };
+
+ private readonly onYospaceAdvertEnd = () => {
+ this.convivaAdAnalytics.reportAdEnded();
+ };
+
+ private readonly onYospaceSessionError = (code: SessionErrorCode) => {
+ if (code === SessionErrorCode.TIMEOUT) {
+ this.convivaVideoAnalytics.reportPlaybackError('The Yospace session has timed out.');
+ } else {
+ this.convivaVideoAnalytics.reportPlaybackError('The Yospace session has errored.');
+ }
+ };
+
+ private addEventListeners(): void {
+ this.player.addEventListener('playing', this.onPlaying);
+ this.player.addEventListener('pause', this.onPause);
+ }
+
+ private removeEventListeners(): void {
+ this.player.removeEventListener('playing', this.onPlaying);
+ this.player.removeEventListener('pause', this.onPause);
+ }
+
+ destroy() {
+ this.removeEventListeners();
+ this.yospaceConnector.unregisterAnalyticEventObserver(this.observer);
+ }
+}
diff --git a/conviva/src/utils/Utils.ts b/conviva/src/utils/Utils.ts
new file mode 100644
index 00000000..15b8e0fe
--- /dev/null
+++ b/conviva/src/utils/Utils.ts
@@ -0,0 +1,204 @@
+import {
+ Constants,
+ ConvivaAdBreakInfo,
+ ConvivaDeviceMetadata,
+ ConvivaMetadata,
+ ConvivaOptions,
+ ConvivaPlayerInfo
+} from '@convivainc/conviva-js-coresdk';
+import { AdVert } from '@theoplayer/yospace-connector-web';
+import { Ad, AdBreak, ChromelessPlayer, GoogleImaAd, VerizonMediaAd, VerizonMediaAdBreak, version } from 'theoplayer';
+import { ConvivaConfiguration } from '../integration/ConvivaHandler';
+
+export function collectDeviceMetadata(): ConvivaDeviceMetadata {
+ // Most device metadata is auto-collected by Conviva.
+ return {
+ [Constants.DeviceMetadata.CATEGORY]: Constants.DeviceCategory.WEB
+ };
+}
+
+export function calculateVerizonAdBreakInfo(adBreak: VerizonMediaAdBreak, adBreakIndex: number): ConvivaAdBreakInfo {
+ return {
+ [Constants.POD_DURATION]: adBreak.duration!,
+ [Constants.POD_INDEX]: adBreakIndex
+ };
+}
+
+export function calculateCurrentAdBreakPosition(adBreak: AdBreak): string {
+ const currentAdBreakTimeOffset = adBreak.timeOffset;
+ if (currentAdBreakTimeOffset === 0) {
+ return Constants.AdPosition.PREROLL;
+ }
+ if (currentAdBreakTimeOffset < 0) {
+ return Constants.AdPosition.POSTROLL;
+ }
+ return Constants.AdPosition.MIDROLL;
+}
+
+export function calculateCurrentAdBreakInfo(adBreak: AdBreak, adBreakIndex: number): ConvivaAdBreakInfo {
+ return {
+ [Constants.POD_POSITION]: calculateCurrentAdBreakPosition(adBreak),
+ [Constants.POD_DURATION]: adBreak.maxDuration!,
+ [Constants.POD_INDEX]: adBreakIndex
+ };
+}
+
+export function calculateConvivaOptions(config: ConvivaConfiguration): ConvivaOptions {
+ const options: ConvivaOptions = {};
+ if (config.debug) {
+ options[Constants.GATEWAY_URL] = config.gatewayUrl;
+ options[Constants.LOG_LEVEL] = Constants.LogLevel.DEBUG;
+ } else {
+ // No need to set GATEWAY_URL and LOG_LEVEL settings for your production release.
+ // The Conviva SDK provides the default values for production
+ }
+ return options;
+}
+
+export function collectPlayerInfo(): ConvivaPlayerInfo {
+ return {
+ [Constants.FRAMEWORK_NAME]: 'THEOplayer',
+ [Constants.FRAMEWORK_VERSION]: version
+ };
+}
+
+export function collectContentMetadata(
+ player: ChromelessPlayer,
+ configuredContentMetadata: ConvivaMetadata
+): ConvivaMetadata {
+ const contentInfo: ConvivaMetadata = {};
+ const duration = player.duration;
+ if (!Number.isNaN(duration) && duration !== Infinity) {
+ contentInfo[Constants.DURATION] = duration;
+ }
+ // @ts-ignore
+ return {
+ ...configuredContentMetadata,
+ ...contentInfo
+ };
+}
+
+export function collectYospaceAdMetadata(player: ChromelessPlayer, ad: AdVert): ConvivaMetadata {
+ return {
+ [Constants.ASSET_NAME]: ad.getProperty('AdTitle')?.getValue(),
+ [Constants.STREAM_URL]: player.src!,
+ [Constants.DURATION]: (ad.getDuration() / 1000) as any,
+ 'c3.ad.technology': Constants.AdType.SERVER_SIDE,
+ 'c3.ad.id': ad.getIdentifier(),
+ 'c3.ad.system': ad.getProperty('AdSystem')?.getValue(),
+ 'c3.ad.isSlate': ad.isFiller() ? 'true' : 'false',
+ 'c3.ad.mediaFileApiFramework': 'NA',
+ 'c3.ad.adStitcher': 'YoSpace',
+ 'c3.ad.firstAdSystem': 'NA',
+ 'c3.ad.firstAdId': 'NA',
+ 'c3.ad.firstCreativeId': 'NA',
+ 'c3.ad.creativeId': ad.getLinearCreative().getCreativeIdentifier()
+ };
+}
+
+export function collectVerizonAdMetadata(ad: VerizonMediaAd): ConvivaMetadata {
+ const adMetadata: ConvivaMetadata = {
+ [Constants.DURATION]: ad.duration as any
+ };
+ const assetName = ad.creative;
+ if (assetName) {
+ adMetadata[Constants.ASSET_NAME] = assetName;
+ }
+
+ return adMetadata;
+}
+
+export function collectAdMetadata(ad: Ad): ConvivaMetadata {
+ const adMetadata: ConvivaMetadata = {
+ [Constants.DURATION]: ad.duration as any
+ };
+ const streamUrl = (ad as GoogleImaAd).mediaUrl || ad.resourceURI;
+ if (streamUrl) {
+ adMetadata[Constants.STREAM_URL] = streamUrl;
+ }
+ const assetName = (ad as GoogleImaAd).title || ad.id;
+ if (assetName) {
+ adMetadata[Constants.ASSET_NAME] = assetName;
+ }
+ // [Required] This Ad ID is from the Ad Server that actually has the ad creative.
+ // For wrapper ads, this is the last Ad ID at the end of the wrapper chain.
+ adMetadata['c3.ad.id'] = ad.id || 'NA';
+
+ // [Required] The creative name (may be the same as the ad name) as a string.
+ // Creative name is available from the ad server. Set to "NA" if not available.
+ adMetadata['c3.ad.creativeName'] = assetName || 'NA';
+
+ // [Required] The creative id of the ad. This creative id is from the Ad Server that actually has the ad creative.
+ // For wrapper ads, this is the last creative id at the end of the wrapper chain. Set to "NA" if not available.
+ adMetadata['c3.ad.creativeId'] = ad.creativeId || 'NA';
+
+ // [Required] The ad technology as CLIENT_SIDE/SERVER_SIDE
+ adMetadata['c3.ad.technology'] = Constants.AdType.CLIENT_SIDE;
+
+ // [Required] The ad position as a string "Pre-roll", "Mid-roll" or "Post-roll"
+ adMetadata['c3.ad.position'] = calculateCurrentAdBreakPosition(ad.adBreak);
+
+ // [Preferred] A string that identifies the Ad System (i.e. the Ad Server). This Ad System represents
+ // the Ad Server that actually has the ad creative. For wrapper ads, this is the last Ad System at the end of
+ // the wrapper chain. Set to "NA" if not available
+ adMetadata['c3.ad.system'] = ad.adSystem || 'NA';
+
+ // [Preferred] A boolean value that indicates whether this ad is a Slate or not.
+ // Set to "true" for Slate and "false" for a regular ad. By default, set to "false"
+ adMetadata['c3.ad.isSlate'] = 'false';
+
+ // [Preferred] Only valid for wrapper VAST responses.
+ // This tag must capture the "first" Ad Id in the wrapper chain when a Linear creative is available or there is
+ // an error at the end of the wrapper chain. Set to "NA" if not available. If there is no wrapper VAST response
+ // then the Ad Id and First Ad Id should be the same.
+ adMetadata['c3.ad.firstAdId'] = (ad as GoogleImaAd).wrapperAdIds[0] || ad.id || 'NA';
+
+ // [Preferred] Only valid for wrapper VAST responses.
+ // This tag must capture the "first" Creative Id in the wrapper chain when a Linear creative is available or
+ // there is an error at the end of the wrapper chain. Set to "NA" if not available. If there is no wrapper
+ // VAST response then the Ad Creative Id and First Ad Creative Id should be the same.
+ adMetadata['c3.ad.firstCreativeId'] = (ad as GoogleImaAd).wrapperCreativeIds[0] || ad.creativeId || 'NA';
+
+ // [Preferred] Only valid for wrapper VAST responses. This tag must capture the "first" Ad System in the wrapper
+ // chain when a Linear creative is available or there is an error at the end of the wrapper chain. Set to "NA" if
+ // not available. If there is no wrapper VAST response then the Ad System and First Ad System should be the same.
+ // Examples: "GDFP", "NA".
+ adMetadata['c3.ad.firstAdSystem'] = (ad as GoogleImaAd).wrapperAdSystems[0] || ad.adSystem || 'NA';
+
+ // The name of the Ad Stitcher. If not using an Ad Stitcher, set to "NA"
+ adMetadata['c3.ad.adStitcher'] = 'NA';
+
+ return adMetadata;
+}
+
+export function calculateBufferLength(player: ChromelessPlayer): number {
+ const buffered = player.buffered;
+ if (buffered === undefined) {
+ return 0;
+ }
+ let bufferLength = 0;
+ for (let i = 0; i < buffered.length; i += 1) {
+ const start = buffered.start(i);
+ const end = buffered.end(i);
+ if (start <= player.currentTime && player.currentTime < end) {
+ bufferLength += end - player.currentTime;
+ }
+ }
+ return bufferLength * 1000;
+}
+
+export function flattenAndStringifyObject(obj: any): { [key: string]: string } {
+ const result: Record = {};
+ Object.keys(obj).forEach((key) => {
+ try {
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
+ result[key] = JSON.stringify(obj[key]);
+ } else {
+ result[key] = obj[key].toString();
+ }
+ } catch (ignore) {
+ // Failed to stringify value.
+ }
+ });
+ return result;
+}
diff --git a/conviva/test/pages/main_esm.html b/conviva/test/pages/main_esm.html
new file mode 100644
index 00000000..8fd06d34
--- /dev/null
+++ b/conviva/test/pages/main_esm.html
@@ -0,0 +1,53 @@
+
+
+
+
+ Connector test page
+
+
+
+
+
+
+
+
diff --git a/conviva/test/pages/main_umd.html b/conviva/test/pages/main_umd.html
new file mode 100644
index 00000000..d3146abf
--- /dev/null
+++ b/conviva/test/pages/main_umd.html
@@ -0,0 +1,58 @@
+
+
+
+
+ Connector test page
+
+
+
+
+
+
+
+
+
+
diff --git a/conviva/test/pages/yospace/yospace.html b/conviva/test/pages/yospace/yospace.html
new file mode 100644
index 00000000..55118f4d
--- /dev/null
+++ b/conviva/test/pages/yospace/yospace.html
@@ -0,0 +1,67 @@
+
+
+
+
+ Connector test page
+
+
+
+
+
+
+
+
+
+
+
diff --git a/conviva/test/unit/example.spec.ts b/conviva/test/unit/example.spec.ts
new file mode 100644
index 00000000..6ddb9eac
--- /dev/null
+++ b/conviva/test/unit/example.spec.ts
@@ -0,0 +1,6 @@
+describe('My connector code', () => {
+ it('should be decently tested', () => {
+ const a: number = 3;
+ expect(a).toBe(3);
+ });
+});
diff --git a/conviva/tsconfig.json b/conviva/tsconfig.json
new file mode 100644
index 00000000..5fd53772
--- /dev/null
+++ b/conviva/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "module": "commonjs",
+ "strict": true,
+ "baseUrl": "./",
+ "rootDir": ".",
+ "paths": { "THEOplayer": ["./src/THEOplayer"] },
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index bd307b3f..6f1f0d5b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,11 +9,52 @@
"version": "0.0.0",
"license": "MIT",
"workspaces": [
- "yospace"
+ "yospace",
+ "conviva"
],
"devDependencies": {
"@changesets/cli": "^2.27.1",
- "@changesets/types": "^6.0.0"
+ "@changesets/types": "^6.0.0",
+ "@rollup/plugin-commonjs": "^25.0.7",
+ "@rollup/plugin-node-resolve": "^15.2.3",
+ "@rollup/plugin-typescript": "^11.1.6",
+ "@types/jest": "^29.5.11",
+ "@types/node": "^20.11.6",
+ "@typescript-eslint/eslint-plugin": "^6.19.1",
+ "@typescript-eslint/parser": "^6.19.1",
+ "eslint": "^8.56.0",
+ "eslint-config-airbnb-base": "^15.0.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-import-resolver-typescript": "^3.6.1",
+ "eslint-plugin-import": "^2.29.1",
+ "http-server": "^14.1.1",
+ "jest": "^29.7.0",
+ "prettier": "^3.2.4",
+ "rimraf": "^5.0.5",
+ "rollup": "^4.9.6",
+ "rollup-plugin-dts": "^6.1.0",
+ "theoplayer": "^6.8.0",
+ "ts-jest": "^29.1.2",
+ "ts-node": "^10.9.2",
+ "tslib": "^2.6.2",
+ "typescript": "^5.3.3"
+ }
+ },
+ "conviva": {
+ "name": "@theoplayer/conviva-connector-web",
+ "version": "1.3.0",
+ "license": "MIT",
+ "dependencies": {
+ "@convivainc/conviva-js-coresdk": "^4.6.1"
+ },
+ "peerDependencies": {
+ "@theoplayer/yospace-connector-web": "^2.0.0",
+ "theoplayer": "^5.0.0 || ^6.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@theoplayer/yospace-connector-web": {
+ "optional": true
+ }
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -1295,6 +1336,11 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
+ "node_modules/@convivainc/conviva-js-coresdk": {
+ "version": "4.7.4",
+ "resolved": "https://registry.npmjs.org/@convivainc/conviva-js-coresdk/-/conviva-js-coresdk-4.7.4.tgz",
+ "integrity": "sha512-/OgItGtIPQcbgGtQLj+MaMRsb75Z5vzm7vbiGpO7d1hAtvuBSX3tJluInzLFs6cM6xO/4tex5CVL2opgbeEaAA=="
+ },
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -2006,16 +2052,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/source-map": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
- "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
- "dev": true,
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.0",
- "@jridgewell/trace-mapping": "^0.3.9"
- }
- },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
@@ -2245,28 +2281,6 @@
}
}
},
- "node_modules/@rollup/plugin-terser": {
- "version": "0.4.4",
- "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
- "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
- "dev": true,
- "dependencies": {
- "serialize-javascript": "^6.0.1",
- "smob": "^1.0.0",
- "terser": "^5.17.4"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "peerDependencies": {
- "rollup": "^2.0.0||^3.0.0||^4.0.0"
- },
- "peerDependenciesMeta": {
- "rollup": {
- "optional": true
- }
- }
- },
"node_modules/@rollup/plugin-typescript": {
"version": "11.1.6",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz",
@@ -2508,6 +2522,10 @@
"@sinonjs/commons": "^3.0.0"
}
},
+ "node_modules/@theoplayer/conviva-connector-web": {
+ "resolved": "conviva",
+ "link": true
+ },
"node_modules/@theoplayer/yospace-connector-web": {
"resolved": "yospace",
"link": true
@@ -3619,12 +3637,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
- "node_modules/commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
- "dev": true
- },
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -7580,15 +7592,6 @@
"node": ">=8"
}
},
- "node_modules/randombytes": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
- "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
- "dev": true,
- "dependencies": {
- "safe-buffer": "^5.1.0"
- }
- },
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@@ -8079,15 +8082,6 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
- "node_modules/serialize-javascript": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
- "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
- "dev": true,
- "dependencies": {
- "randombytes": "^2.1.0"
- }
- },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -8323,12 +8317,6 @@
"node": ">=6"
}
},
- "node_modules/smob": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz",
- "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==",
- "dev": true
- },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -8685,34 +8673,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/terser": {
- "version": "5.27.1",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.1.tgz",
- "integrity": "sha512-29wAr6UU/oQpnTw5HoadwjUZnFQXGdOfj0LjZ4sVxzqwHh/QVkvr7m8y9WoR4iN3FRitVduTc6KdjcW38Npsug==",
- "dev": true,
- "dependencies": {
- "@jridgewell/source-map": "^0.3.3",
- "acorn": "^8.8.2",
- "commander": "^2.20.0",
- "source-map-support": "~0.5.20"
- },
- "bin": {
- "terser": "bin/terser"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/terser/node_modules/source-map-support": {
- "version": "0.5.21",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
- "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "dev": true,
- "dependencies": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- }
- },
"node_modules/test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@@ -8778,8 +8738,7 @@
"node_modules/theoplayer": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/theoplayer/-/theoplayer-6.10.0.tgz",
- "integrity": "sha512-LLukJIR9a7pNWYGVt1SIf7QOwlJH5N/l+TSo4RNdHpTCWO0+pNU58os6RqG4id2D8dCgP3Q3Mn7plTX6ow07Gg==",
- "dev": true
+ "integrity": "sha512-LLukJIR9a7pNWYGVt1SIf7QOwlJH5N/l+TSo4RNdHpTCWO0+pNU58os6RqG4id2D8dCgP3Q3Mn7plTX6ow07Gg=="
},
"node_modules/tmp": {
"version": "0.0.33",
@@ -9444,32 +9403,6 @@
"name": "@theoplayer/yospace-connector-web",
"version": "2.0.0",
"license": "MIT",
- "devDependencies": {
- "@rollup/plugin-commonjs": "^25.0.7",
- "@rollup/plugin-node-resolve": "^15.2.3",
- "@rollup/plugin-terser": "^0.4.4",
- "@rollup/plugin-typescript": "^11.1.6",
- "@types/jest": "^29.5.11",
- "@types/node": "^20.11.6",
- "@typescript-eslint/eslint-plugin": "^6.19.1",
- "@typescript-eslint/parser": "^6.19.1",
- "eslint": "^8.56.0",
- "eslint-config-airbnb-base": "^15.0.0",
- "eslint-config-prettier": "^9.1.0",
- "eslint-import-resolver-typescript": "^3.6.1",
- "eslint-plugin-import": "^2.29.1",
- "http-server": "^14.1.1",
- "jest": "^29.7.0",
- "prettier": "^3.2.4",
- "rimraf": "^5.0.5",
- "rollup": "^4.9.6",
- "rollup-plugin-dts": "^6.1.0",
- "theoplayer": "^6.8.0",
- "ts-jest": "^29.1.2",
- "ts-node": "^10.9.2",
- "tslib": "^2.6.2",
- "typescript": "^5.3.3"
- },
"peerDependencies": {
"theoplayer": "^5.0.0 || ^6.0.0"
}
diff --git a/package.json b/package.json
index 1a0c1b6f..5fa7b0ba 100644
--- a/package.json
+++ b/package.json
@@ -7,12 +7,43 @@
"author": "THEO Technologies NV",
"license": "MIT",
"workspaces": [
- "yospace"
+ "yospace",
+ "conviva"
],
"scripts": {
- "changeset:version": "changeset version && node .changeset/post-process.js"
+ "changeset:version": "changeset version && node .changeset/post-process.js",
+ "build": "npm run build --workspaces",
+ "clean": "npm run clean --workspaces",
+ "test": "npm run test --workspaces",
+ "prettier": "prettier --check \"*/(src|test)/**/*\"",
+ "prettier:fix": "prettier --write \"*/(src|test)/**/*\"",
+ "lint": "eslint \"*/src/**/*.ts\" \"*/test*/**/*.ts\"",
+ "lint:fix": "npm run lint -- --fix"
},
"devDependencies": {
+ "@rollup/plugin-commonjs": "^25.0.7",
+ "@rollup/plugin-node-resolve": "^15.2.3",
+ "@rollup/plugin-typescript": "^11.1.6",
+ "@types/jest": "^29.5.11",
+ "@types/node": "^20.11.6",
+ "@typescript-eslint/eslint-plugin": "^6.19.1",
+ "@typescript-eslint/parser": "^6.19.1",
+ "eslint": "^8.56.0",
+ "eslint-config-airbnb-base": "^15.0.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-import-resolver-typescript": "^3.6.1",
+ "eslint-plugin-import": "^2.29.1",
+ "http-server": "^14.1.1",
+ "jest": "^29.7.0",
+ "prettier": "^3.2.4",
+ "rimraf": "^5.0.5",
+ "rollup": "^4.9.6",
+ "rollup-plugin-dts": "^6.1.0",
+ "theoplayer": "^6.8.0",
+ "ts-jest": "^29.1.2",
+ "ts-node": "^10.9.2",
+ "tslib": "^2.6.2",
+ "typescript": "^5.3.3",
"@changesets/cli": "^2.27.1",
"@changesets/types": "^6.0.0"
}
diff --git a/tools/build.mjs b/tools/build.mjs
new file mode 100644
index 00000000..d1bf5c63
--- /dev/null
+++ b/tools/build.mjs
@@ -0,0 +1,61 @@
+import {defineConfig} from "rollup";
+import nodeResolve from "@rollup/plugin-node-resolve";
+import commonjs from "@rollup/plugin-commonjs";
+import typescript from "@rollup/plugin-typescript";
+import dts from "rollup-plugin-dts";
+
+export function getSharedBuildConfiguration(fileName, globalName, banner) {
+ return defineConfig([{
+ input: {
+ [fileName]: "src/index.ts"
+ },
+ output: [
+ {
+ dir: "dist",
+ entryFileNames: "[name].umd.js",
+ name: globalName,
+ format: "umd",
+ indent: false,
+ banner,
+ globals: {theoplayer: "THEOplayer"}
+ },
+ {
+ dir: "dist",
+ entryFileNames: "[name].esm.js",
+ format: "esm",
+ indent: false,
+ banner
+ }
+ ],
+ plugins: [
+ nodeResolve({
+ extensions: [".ts", ".js"]
+ }),
+ commonjs({
+ include: ['node_modules/**', '../node_modules/**']
+ }),
+ typescript({
+ tsconfig: "tsconfig.json",
+ module: "es2015",
+ include: ["src/**/*"]
+ })
+ ]
+ }, {
+ input: {
+ [fileName]: "src/index.ts"
+ },
+ output: [
+ {
+ dir: "dist",
+ format: "esm",
+ banner,
+ footer: `export as namespace ${globalName};`
+ }
+ ],
+ plugins: [
+ dts({
+ tsconfig: "tsconfig.json",
+ })
+ ]
+ }]);
+}
\ No newline at end of file
diff --git a/yospace/.eslintignore b/yospace/.eslintignore
deleted file mode 100644
index b6f205df..00000000
--- a/yospace/.eslintignore
+++ /dev/null
@@ -1,2 +0,0 @@
-dist/
-*.min.js
diff --git a/yospace/.prettierignore b/yospace/.prettierignore
deleted file mode 100644
index b6f205df..00000000
--- a/yospace/.prettierignore
+++ /dev/null
@@ -1,2 +0,0 @@
-dist/
-*.min.js
diff --git a/yospace/LICENSE b/yospace/LICENSE
index a0bbaa41..146db44c 100644
--- a/yospace/LICENSE
+++ b/yospace/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2022 THEO Technologies NV
+Copyright (c) 2024 THEO Technologies NV
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/yospace/bitbucket-pipelines.yml b/yospace/bitbucket-pipelines.yml
deleted file mode 100644
index 4eaa212b..00000000
--- a/yospace/bitbucket-pipelines.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-image: node:20-slim
-
-definitions:
- caches:
- npm: $HOME/.npm
-
-stepdefinitions:
- - tests: &tests
- name: Run tests and quality gate
- script:
- - npm ci --prefer-offline --no-audit
- - npm run build
- - npm run test
- - npm run prettier
- - npm run lint
- - publish-npm: &publish-npm
- name: Build and publish npm package
- caches:
- - npm
- script:
- - npm ci --prefer-offline --no-audit
- - npm run build
- - pipe: atlassian/npm-publish:0.2.0
- variables:
- EXTRA_ARGS: "--access public"
- NPM_TOKEN: $NPM_TOKEN
-
-pipelines:
- default:
- - step: *tests
- tags:
- v*:
- - step: *tests
- - step: *publish-npm
diff --git a/yospace/package.json b/yospace/package.json
index ac5e2072..1a0b82bb 100644
--- a/yospace/package.json
+++ b/yospace/package.json
@@ -26,10 +26,6 @@
"bundle": "rollup -c rollup.config.mjs",
"build": "npm run clean && npm run bundle",
"serve": "http-server",
- "lint": "eslint \"src/**/*.ts\" \"test*/**/*.ts\"",
- "lint:fix": "npm run lint -- --fix",
- "prettier": "prettier --check \"(src|test)/**/*\"",
- "prettier:fix": "prettier --write \"(src|test)/**/*\"",
"test": "jest"
},
"author": "THEO Technologies NV",
@@ -39,32 +35,6 @@
"README.md",
"package.json"
],
- "devDependencies": {
- "@rollup/plugin-commonjs": "^25.0.7",
- "@rollup/plugin-node-resolve": "^15.2.3",
- "@rollup/plugin-terser": "^0.4.4",
- "@rollup/plugin-typescript": "^11.1.6",
- "@types/jest": "^29.5.11",
- "@types/node": "^20.11.6",
- "@typescript-eslint/eslint-plugin": "^6.19.1",
- "@typescript-eslint/parser": "^6.19.1",
- "eslint": "^8.56.0",
- "eslint-config-airbnb-base": "^15.0.0",
- "eslint-config-prettier": "^9.1.0",
- "eslint-import-resolver-typescript": "^3.6.1",
- "eslint-plugin-import": "^2.29.1",
- "http-server": "^14.1.1",
- "jest": "^29.7.0",
- "prettier": "^3.2.4",
- "rimraf": "^5.0.5",
- "rollup": "^4.9.6",
- "rollup-plugin-dts": "^6.1.0",
- "theoplayer": "^6.8.0",
- "ts-jest": "^29.1.2",
- "ts-node": "^10.9.2",
- "tslib": "^2.6.2",
- "typescript": "^5.3.3"
- },
"peerDependencies": {
"theoplayer": "^5.0.0 || ^6.0.0"
},
diff --git a/yospace/rollup.config.mjs b/yospace/rollup.config.mjs
index d112c921..a815f84f 100644
--- a/yospace/rollup.config.mjs
+++ b/yospace/rollup.config.mjs
@@ -1,8 +1,5 @@
-import commonjs from "@rollup/plugin-commonjs";
-import nodeResolve from "@rollup/plugin-node-resolve";
-import typescript from "@rollup/plugin-typescript";
-import dts from "rollup-plugin-dts";
-import fs from "fs";
+import fs from "node:fs";
+import {getSharedBuildConfiguration} from "../tools/build.mjs";
const {version} = JSON.parse(fs.readFileSync("./package.json", "utf8"));
@@ -13,60 +10,4 @@ const banner = `
* THEOplayer Yospace Connector v${version}
*/`.trim();
-/**
- * @type {import("rollup").RollupOptions[]}
- */
-const options = [{
- input: {
- [fileName]: "src/index.ts"
- },
- output: [
- {
- dir: "dist",
- entryFileNames: "[name].umd.js",
- name: globalName,
- format: "umd",
- indent: false,
- banner,
- globals: {THEOplayer: "THEOplayer"}
- },
- {
- dir: "dist",
- entryFileNames: "[name].esm.js",
- format: "esm",
- indent: false,
- banner
- }
- ],
- plugins: [
- nodeResolve({
- extensions: [".ts", ".js"]
- }),
- commonjs({
- include: "node_modules/**"
- }),
- typescript({
- tsconfig: "tsconfig.json",
- module: "es2015",
- include: ["src/**/*"]
- })
- ]
-}, {
- input: {
- [fileName]: "src/index.ts"
- },
- output: [
- {
- dir: "dist",
- format: "esm",
- banner,
- footer: `export as namespace ${globalName};`
- }
- ],
- plugins: [
- dts({
- tsconfig: "tsconfig.json",
- })
- ]
-}];
-export default options;
+export default getSharedBuildConfiguration(fileName, globalName, banner);
\ No newline at end of file
diff --git a/yospace/src/index.ts b/yospace/src/index.ts
index 55fe18b1..899d3ddd 100644
--- a/yospace/src/index.ts
+++ b/yospace/src/index.ts
@@ -1,4 +1,4 @@
-export * from "./integration/YospaceConnector";
-export { AnalyticEventObserver } from "./yospace/AnalyticEventObserver";
-export * from "./yospace/AdBreak";
-export { SessionProperties } from "./yospace/SessionProperties";
+export * from './integration/YospaceConnector';
+export { AnalyticEventObserver, SessionErrorCode } from './yospace/AnalyticEventObserver';
+export * from './yospace/AdBreak';
+export { SessionProperties } from './yospace/SessionProperties';
diff --git a/yospace/src/integration/YospaceAd.ts b/yospace/src/integration/YospaceAd.ts
index 3f66b80d..56bc28ef 100644
--- a/yospace/src/integration/YospaceAd.ts
+++ b/yospace/src/integration/YospaceAd.ts
@@ -1,6 +1,6 @@
export enum YospaceAdType {
- LINEAR = "linear",
- NON_LINEAR = "nonlinear"
+ LINEAR = 'linear',
+ NON_LINEAR = 'nonlinear'
}
export abstract class YoSpaceAd {
diff --git a/yospace/src/integration/YospaceAdHandler.ts b/yospace/src/integration/YospaceAdHandler.ts
index 571166b1..6a239aed 100644
--- a/yospace/src/integration/YospaceAdHandler.ts
+++ b/yospace/src/integration/YospaceAdHandler.ts
@@ -1,12 +1,12 @@
-import { ChromelessPlayer } from "theoplayer";
-import { AnalyticEventObserver, SessionErrorCode } from "../yospace/AnalyticEventObserver";
-import { AdBreak, AdVert, ResourceType } from "../yospace/AdBreak";
-import { YospaceUiHandler } from "./YospaceUIHandler";
-import { YoSpaceLinearAd, YoSpaceNonLinearAd } from "./YospaceAd";
-import { YospaceManager } from "./YospaceManager";
-import { arrayRemove } from "../utils/DefaultEventDispatcher";
-import { TrackingError } from "../yospace/TrackingError";
-import { YospaceSessionManager } from "../yospace/YospaceSessionManager";
+import { ChromelessPlayer } from 'theoplayer';
+import { AnalyticEventObserver, SessionErrorCode } from '../yospace/AnalyticEventObserver';
+import { AdBreak, AdVert, ResourceType } from '../yospace/AdBreak';
+import { YospaceUiHandler } from './YospaceUIHandler';
+import { YoSpaceLinearAd, YoSpaceNonLinearAd } from './YospaceAd';
+import { YospaceManager } from './YospaceManager';
+import { arrayRemove } from '../utils/DefaultEventDispatcher';
+import { TrackingError } from '../yospace/TrackingError';
+import { YospaceSessionManager } from '../yospace/YospaceSessionManager';
export class YospaceAdHandler {
private yospaceManager: YospaceManager;
@@ -36,7 +36,7 @@ export class YospaceAdHandler {
private onAdvertStart(advert: AdVert) {
if (this.advertStartListener) {
- this.player.removeEventListener("play", this.advertStartListener);
+ this.player.removeEventListener('play', this.advertStartListener);
this.advertStartListener = undefined;
}
@@ -81,7 +81,7 @@ export class YospaceAdHandler {
this.advertStartListener = () => {
this.onAdvertStart(advert);
};
- this.player.addEventListener("play", this.advertStartListener);
+ this.player.addEventListener('play', this.advertStartListener);
}
this.analyticEventObservers.forEach((observer) => observer.onAdvertStart(advert, session));
},
diff --git a/yospace/src/integration/YospaceConnector.ts b/yospace/src/integration/YospaceConnector.ts
index 25776bb4..f14312db 100644
--- a/yospace/src/integration/YospaceConnector.ts
+++ b/yospace/src/integration/YospaceConnector.ts
@@ -1,15 +1,15 @@
-import { ChromelessPlayer, SourceDescription } from "theoplayer";
-import { YospaceManager } from "./YospaceManager";
-import { SessionProperties } from "../yospace/SessionProperties";
-import { AnalyticEventObserver } from "../yospace/AnalyticEventObserver";
-import { EventDispatcher, EventListener, StringKeyOf } from "../utils/event/EventDispatcher";
-import { BaseEvent } from "../utils/event/Event";
+import { ChromelessPlayer, SourceDescription } from 'theoplayer';
+import { YospaceManager } from './YospaceManager';
+import { SessionProperties } from '../yospace/SessionProperties';
+import { AnalyticEventObserver } from '../yospace/AnalyticEventObserver';
+import { EventDispatcher, EventListener, StringKeyOf } from '../utils/event/EventDispatcher';
+import { BaseEvent } from '../utils/event/Event';
export interface YospaceEventMap {
/**
* Fired when a new Yospace session starts.
*/
- sessionavailable: BaseEvent<"sessionavailable">;
+ sessionavailable: BaseEvent<'sessionavailable'>;
}
export class YospaceConnector implements EventDispatcher {
diff --git a/yospace/src/integration/YospaceEMSGMetadataHandler.ts b/yospace/src/integration/YospaceEMSGMetadataHandler.ts
index a9b0a145..d45f8ef2 100644
--- a/yospace/src/integration/YospaceEMSGMetadataHandler.ts
+++ b/yospace/src/integration/YospaceEMSGMetadataHandler.ts
@@ -1,7 +1,7 @@
-import { EmsgCue, TextTrackCue, TextTrackCueChangeEvent, YospaceId } from "theoplayer";
-import { YospaceMetadataHandler, YospaceReport } from "./YospaceMetadataHandler";
+import { EmsgCue, TextTrackCue, TextTrackCueChangeEvent, YospaceId } from 'theoplayer';
+import { YospaceMetadataHandler, YospaceReport } from './YospaceMetadataHandler';
-export const YOSPACE_EMSG_SCHEME_ID_URI = "urn:yospace:a:id3:2016";
+export const YOSPACE_EMSG_SCHEME_ID_URI = 'urn:yospace:a:id3:2016';
function isValidYospaceSchemeIDURI(schemeIDURI: string): boolean {
return schemeIDURI === YOSPACE_EMSG_SCHEME_ID_URI;
@@ -9,10 +9,10 @@ function isValidYospaceSchemeIDURI(schemeIDURI: string): boolean {
function parseEmsgYospaceMetadata(data: number[]): YospaceReport {
const emsgString = String.fromCharCode(...data);
- const parsedEmsg = emsgString.split(",");
+ const parsedEmsg = emsgString.split(',');
const result: YospaceReport = {};
parsedEmsg.forEach((metadataElement) => {
- const [key, value] = metadataElement.split("=");
+ const [key, value] = metadataElement.split('=');
result[key as YospaceId] = value;
});
return result;
diff --git a/yospace/src/integration/YospaceID3MetadataHandler.ts b/yospace/src/integration/YospaceID3MetadataHandler.ts
index afca82a5..2ce0fd67 100644
--- a/yospace/src/integration/YospaceID3MetadataHandler.ts
+++ b/yospace/src/integration/YospaceID3MetadataHandler.ts
@@ -1,13 +1,13 @@
-import { ID3Frame, ID3Yospace, TextTrackCue, TextTrackCueChangeEvent } from "theoplayer";
-import { YospaceMetadataHandler, YospaceReport } from "./YospaceMetadataHandler";
+import { ID3Frame, ID3Yospace, TextTrackCue, TextTrackCueChangeEvent } from 'theoplayer';
+import { YospaceMetadataHandler, YospaceReport } from './YospaceMetadataHandler';
function isID3YospaceFrame(frame: ID3Frame): frame is ID3Yospace {
switch (frame.id) {
- case "YMID":
- case "YTYP":
- case "YSEQ":
- case "YDUR":
- case "YCSP":
+ case 'YMID':
+ case 'YTYP':
+ case 'YSEQ':
+ case 'YDUR':
+ case 'YCSP':
return true;
default:
return false;
diff --git a/yospace/src/integration/YospaceManager.ts b/yospace/src/integration/YospaceManager.ts
index f5cac231..ed4209e8 100644
--- a/yospace/src/integration/YospaceManager.ts
+++ b/yospace/src/integration/YospaceManager.ts
@@ -1,19 +1,19 @@
-import { ChromelessPlayer, SourceDescription, YospaceTypedSource } from "theoplayer";
-import { isYospaceTypedSource, yoSpaceWebSdkIsAvailable } from "../utils/YospaceUtils";
-import { PromiseController } from "../utils/PromiseController";
-import { PlayerEvent } from "../yospace/PlayerEvent";
-import { toSources } from "../utils/SourceUtils";
-import { ResultCode, SessionState, YospaceSessionManager } from "../yospace/YospaceSessionManager";
-import { YospaceWindow } from "../yospace/YospaceWindow";
-import { YospaceAdHandler } from "./YospaceAdHandler";
-import { YospaceUiHandler } from "./YospaceUIHandler";
-import { YospaceID3MetadataHandler } from "./YospaceID3MetadataHandler";
-import { YospaceEMSGMetadataHandler } from "./YospaceEMSGMetadataHandler";
-import { SessionProperties } from "../yospace/SessionProperties";
-import { AnalyticEventObserver } from "../yospace/AnalyticEventObserver";
-import { DefaultEventDispatcher } from "../utils/DefaultEventDispatcher";
-import { YospaceEventMap } from "./YospaceConnector";
-import { BaseEvent } from "../utils/event/Event";
+import { ChromelessPlayer, SourceDescription, YospaceTypedSource } from 'theoplayer';
+import { isYospaceTypedSource, yoSpaceWebSdkIsAvailable } from '../utils/YospaceUtils';
+import { PromiseController } from '../utils/PromiseController';
+import { PlayerEvent } from '../yospace/PlayerEvent';
+import { toSources } from '../utils/SourceUtils';
+import { ResultCode, SessionState, YospaceSessionManager } from '../yospace/YospaceSessionManager';
+import { YospaceWindow } from '../yospace/YospaceWindow';
+import { YospaceAdHandler } from './YospaceAdHandler';
+import { YospaceUiHandler } from './YospaceUIHandler';
+import { YospaceID3MetadataHandler } from './YospaceID3MetadataHandler';
+import { YospaceEMSGMetadataHandler } from './YospaceEMSGMetadataHandler';
+import { SessionProperties } from '../yospace/SessionProperties';
+import { AnalyticEventObserver } from '../yospace/AnalyticEventObserver';
+import { DefaultEventDispatcher } from '../utils/DefaultEventDispatcher';
+import { YospaceEventMap } from './YospaceConnector';
+import { BaseEvent } from '../utils/event/Event';
export class YospaceManager extends DefaultEventDispatcher {
private readonly player: ChromelessPlayer;
@@ -89,11 +89,11 @@ export class YospaceManager extends DefaultEventDispatcher {
const properties = sessionProperties ?? new yospaceWindow.SessionProperties();
properties.setUserAgent(navigator.userAgent);
switch (this.yospaceTypedSource?.ssai.streamType) {
- case "vod":
+ case 'vod':
yospaceWindow.SessionVOD.create(this.yospaceTypedSource.src, properties, this.onInitComplete);
break;
- case "nonlinear":
- case "livepause":
+ case 'nonlinear':
+ case 'livepause':
yospaceWindow.SessionDVRLive.create(this.yospaceTypedSource.src, properties, this.onInitComplete);
break;
default:
@@ -102,9 +102,9 @@ export class YospaceManager extends DefaultEventDispatcher {
}
this.isMuted = this.player.muted;
} else if (this.yospaceTypedSource && !isYospaceSDKAvailable) {
- throw new Error("The Yospace Ad Management SDK has not been loaded.");
+ throw new Error('The Yospace Ad Management SDK has not been loaded.');
} else {
- throw new Error("The given source is not a Yospace source.");
+ throw new Error('The given source is not a Yospace source.');
}
}
@@ -152,20 +152,20 @@ export class YospaceManager extends DefaultEventDispatcher {
}
]
};
- this.dispatchEvent(new BaseEvent("sessionavailable"));
+ this.dispatchEvent(new BaseEvent('sessionavailable'));
this.yospaceSourceDescriptionDefined.resolve();
}
private handleSessionInitialisationErrors(result: ResultCode) {
let errorMessage: string;
if (result === ResultCode.MALFORMED_URL) {
- errorMessage = "Yospace: The stream URL is not correctly formatted";
+ errorMessage = 'Yospace: The stream URL is not correctly formatted';
} else if (result === ResultCode.CONNECTION_ERROR) {
- errorMessage = "Yospace: Connection error";
+ errorMessage = 'Yospace: Connection error';
} else if (result === ResultCode.CONNECTION_TIMEOUT) {
- errorMessage = "Yospace: Connection timeout";
+ errorMessage = 'Yospace: Connection timeout';
} else {
- errorMessage = "Yospace: Session could not be initialised";
+ errorMessage = 'Yospace: Session could not be initialised';
}
this.reset();
@@ -173,23 +173,23 @@ export class YospaceManager extends DefaultEventDispatcher {
}
private addEventListenersToNotifyYospace = () => {
- this.player.addEventListener("volumechange", this.handleVolumeChange);
- this.player.addEventListener("play", this.handlePlay);
- this.player.addEventListener("ended", this.handleEnded);
- this.player.addEventListener("pause", this.handlePause);
- this.player.addEventListener("seeked", this.handleSeeked);
- this.player.addEventListener("waiting", this.handleWaiting);
- this.player.addEventListener("playing", this.handlePlaying);
+ this.player.addEventListener('volumechange', this.handleVolumeChange);
+ this.player.addEventListener('play', this.handlePlay);
+ this.player.addEventListener('ended', this.handleEnded);
+ this.player.addEventListener('pause', this.handlePause);
+ this.player.addEventListener('seeked', this.handleSeeked);
+ this.player.addEventListener('waiting', this.handleWaiting);
+ this.player.addEventListener('playing', this.handlePlaying);
};
private removeEventListenersToNotifyYospace = () => {
- this.player.removeEventListener("volumechange", this.handleVolumeChange);
- this.player.removeEventListener("play", this.handlePlay);
- this.player.removeEventListener("ended", this.handleEnded);
- this.player.removeEventListener("pause", this.handlePause);
- this.player.removeEventListener("seeked", this.handleSeeked);
- this.player.removeEventListener("waiting", this.handleWaiting);
- this.player.removeEventListener("playing", this.handlePlaying);
+ this.player.removeEventListener('volumechange', this.handleVolumeChange);
+ this.player.removeEventListener('play', this.handlePlay);
+ this.player.removeEventListener('ended', this.handleEnded);
+ this.player.removeEventListener('pause', this.handlePause);
+ this.player.removeEventListener('seeked', this.handleSeeked);
+ this.player.removeEventListener('waiting', this.handleWaiting);
+ this.player.removeEventListener('playing', this.handlePlaying);
};
private handleVolumeChange = () => {
diff --git a/yospace/src/integration/YospaceMetadataHandler.ts b/yospace/src/integration/YospaceMetadataHandler.ts
index af73e463..1cfad7e0 100644
--- a/yospace/src/integration/YospaceMetadataHandler.ts
+++ b/yospace/src/integration/YospaceMetadataHandler.ts
@@ -1,6 +1,6 @@
-import { AddTrackEvent, TextTrack, TextTrackCue, TextTrackCueChangeEvent, TextTracksList } from "theoplayer";
-import { YospaceWindow } from "../yospace/YospaceWindow";
-import { YospaceSessionManager } from "../yospace/YospaceSessionManager";
+import { AddTrackEvent, TextTrack, TextTrackCue, TextTrackCueChangeEvent, TextTracksList } from 'theoplayer';
+import { YospaceWindow } from '../yospace/YospaceWindow';
+import { YospaceSessionManager } from '../yospace/YospaceSessionManager';
export interface YospaceMetadata {
YMID: string;
@@ -21,16 +21,16 @@ export abstract class YospaceMetadataHandler {
constructor(textTrackList: TextTracksList, session: YospaceSessionManager) {
this.textTrackList = textTrackList;
this.sessionManager = session;
- this.textTrackList.addEventListener("addtrack", this.handleAddTrack);
+ this.textTrackList.addEventListener('addtrack', this.handleAddTrack);
}
protected handleAddTrack = (event: AddTrackEvent) => {
const track = event.track as TextTrack;
- if (track.kind !== "metadata" || !track.cues) {
+ if (track.kind !== 'metadata' || !track.cues) {
return;
}
- track.addEventListener("cuechange", this.handleCueChange);
+ track.addEventListener('cuechange', this.handleCueChange);
};
protected abstract isCorrectCueType(cue: TextTrackCue): boolean;
@@ -57,7 +57,7 @@ export abstract class YospaceMetadataHandler {
}
reset(): void {
- this.textTrackList.forEach((track) => track.removeEventListener("cuechange", this.handleCueChange));
- this.textTrackList.removeEventListener("addtrack", this.handleAddTrack);
+ this.textTrackList.forEach((track) => track.removeEventListener('cuechange', this.handleCueChange));
+ this.textTrackList.removeEventListener('addtrack', this.handleAddTrack);
}
}
diff --git a/yospace/src/integration/YospaceUIHandler.ts b/yospace/src/integration/YospaceUIHandler.ts
index d861a049..ed069269 100644
--- a/yospace/src/integration/YospaceUIHandler.ts
+++ b/yospace/src/integration/YospaceUIHandler.ts
@@ -1,21 +1,21 @@
-import { YoSpaceLinearAd, YoSpaceNonLinearAd } from "./YospaceAd";
-import { YospaceSessionManager } from "../yospace/YospaceSessionManager";
+import { YoSpaceLinearAd, YoSpaceNonLinearAd } from './YospaceAd';
+import { YospaceSessionManager } from '../yospace/YospaceSessionManager';
export function stretchToParent(element: HTMLElement): void {
const { style } = element;
- style.position = "absolute";
- style.left = "0";
- style.right = "0";
- style.top = "0";
- style.bottom = "0";
- style.width = "100%";
- style.height = "100%";
+ style.position = 'absolute';
+ style.left = '0';
+ style.right = '0';
+ style.top = '0';
+ style.bottom = '0';
+ style.width = '100%';
+ style.height = '100%';
}
function createClickThrough(clickThroughURL: string, classToAdd?: string): HTMLElement {
- const clickThroughElement = document.createElement("a");
- clickThroughElement.setAttribute("href", clickThroughURL);
- clickThroughElement.setAttribute("target", "_blank");
+ const clickThroughElement = document.createElement('a');
+ clickThroughElement.setAttribute('href', clickThroughURL);
+ clickThroughElement.setAttribute('target', '_blank');
if (classToAdd) {
clickThroughElement.className = classToAdd;
@@ -23,7 +23,7 @@ function createClickThrough(clickThroughURL: string, classToAdd?: string): HTMLE
// security enhancement
// read more @ https://mathiasbynens.github.io/rel-noopener/
- clickThroughElement.setAttribute("rel", "noopener");
+ clickThroughElement.setAttribute('rel', 'noopener');
return clickThroughElement;
}
@@ -43,15 +43,15 @@ export class YospaceUiHandler {
}
createNonLinear(adToPlay: YoSpaceNonLinearAd) {
- const adImage = document.createElement("img");
+ const adImage = document.createElement('img');
adImage.src = adToPlay.imageUrl;
- adImage.className = "theoplayer-yospace-non-linear-image";
- adImage.style.maxWidth = "100%";
+ adImage.className = 'theoplayer-yospace-non-linear-image';
+ adImage.style.maxWidth = '100%';
- const nonLinearClickThrough = createClickThrough(adToPlay.clickThroughUrl, "theoplayer-yospace-advert");
+ const nonLinearClickThrough = createClickThrough(adToPlay.clickThroughUrl, 'theoplayer-yospace-advert');
nonLinearClickThrough.appendChild(adImage);
- nonLinearClickThrough.style.zIndex = "10";
- nonLinearClickThrough.style.position = "absolute";
+ nonLinearClickThrough.style.zIndex = '10';
+ nonLinearClickThrough.style.position = 'absolute';
this.element.appendChild(nonLinearClickThrough);
this.nonLinears.push(nonLinearClickThrough);
@@ -79,8 +79,8 @@ export class YospaceUiHandler {
}
createLinearClickThrough(adToPlay: YoSpaceLinearAd): void {
- const clickThrough = createClickThrough(adToPlay.clickThroughUrl, "theoplayer-yospace-ad-clickthrough");
- clickThrough.style.zIndex = "10";
+ const clickThrough = createClickThrough(adToPlay.clickThroughUrl, 'theoplayer-yospace-ad-clickthrough');
+ clickThrough.style.zIndex = '10';
stretchToParent(clickThrough);
this.element.appendChild(clickThrough);
diff --git a/yospace/src/utils/DefaultEventDispatcher.ts b/yospace/src/utils/DefaultEventDispatcher.ts
index 84933a2f..e8981ffb 100644
--- a/yospace/src/utils/DefaultEventDispatcher.ts
+++ b/yospace/src/utils/DefaultEventDispatcher.ts
@@ -1,5 +1,5 @@
-import { Event } from "./event/Event";
-import { EventDispatcher, EventMap, StringKeyOf } from "./event/EventDispatcher";
+import { Event } from './event/Event';
+import { EventDispatcher, EventMap, StringKeyOf } from './event/EventDispatcher';
export type EventListener = (event: TEvent) => void;
export type EventListenerList = Array> | EventListener | undefined;
@@ -19,10 +19,10 @@ export class DefaultEventDispatcher>> im
}
addEventListener>(types: K | readonly K[], listener: EventListener): void {
- if (typeof listener !== "function") {
+ if (typeof listener !== 'function') {
return;
}
- if (typeof types === "string") {
+ if (typeof types === 'string') {
this.addSingleEventListener(types, listener);
} else {
types.forEach((type) => {
@@ -36,7 +36,7 @@ export class DefaultEventDispatcher>> im
if (!eventListeners) {
// Optimize case of single listener, don't allocate extra array
this.eventListeners[type] = listener;
- } else if (typeof eventListeners === "function") {
+ } else if (typeof eventListeners === 'function') {
// Migrate from single listener to array of listeners
this.eventListeners[type] = [eventListeners, listener];
} else {
@@ -62,10 +62,10 @@ export class DefaultEventDispatcher>> im
};
removeEventListener>(types: K | readonly K[], listener: EventListener): void {
- if (typeof listener !== "function") {
+ if (typeof listener !== 'function') {
return;
}
- if (typeof types === "string") {
+ if (typeof types === 'string') {
this.removeSingleEventListener(types, listener);
} else {
types.forEach((type) => {
@@ -77,7 +77,7 @@ export class DefaultEventDispatcher>> im
private removeSingleEventListener>(type: K, listener: EventListener): void {
const eventListeners: EventListenerList = this.eventListeners[type];
if (eventListeners) {
- if (typeof eventListeners === "function") {
+ if (typeof eventListeners === 'function') {
// Remove single listener
if (eventListeners === listener) {
this.eventListeners[type] = undefined;
@@ -105,7 +105,7 @@ export function createDictionaryObject(): Record {
export function copyEventListenerList(
list: EventListenerList
): EventListenerList {
- if (!list || typeof list === "function") {
+ if (!list || typeof list === 'function') {
// No listeners, or single listener
// No need to copy
return list;
@@ -122,7 +122,7 @@ export function callEventListenerList(
): void {
if (!list) {
// No listeners
- } else if (typeof list === "function") {
+ } else if (typeof list === 'function') {
// Single listener
list.call(scope, event);
} else {
diff --git a/yospace/src/utils/SourceUtils.ts b/yospace/src/utils/SourceUtils.ts
index 0a93ef57..3d7db512 100644
--- a/yospace/src/utils/SourceUtils.ts
+++ b/yospace/src/utils/SourceUtils.ts
@@ -1,11 +1,11 @@
-import { Source, Sources, TypedSource } from "theoplayer";
+import { Source, Sources, TypedSource } from 'theoplayer';
export function isObject(x: unknown): x is object {
- return typeof x === "object" && x !== null;
+ return typeof x === 'object' && x !== null;
}
export function isString(parameter: unknown): parameter is string {
- return typeof parameter === "string";
+ return typeof parameter === 'string';
}
export function implementsInterface(object: object, interfaceProperties: string[]): boolean {
@@ -34,5 +34,5 @@ export function toSources(sources: Sources): Source[] {
return sources;
}
- throw new Error("not a good source");
+ throw new Error('not a good source');
}
diff --git a/yospace/src/utils/YospaceUtils.ts b/yospace/src/utils/YospaceUtils.ts
index ff42ef38..0c7d4cab 100644
--- a/yospace/src/utils/YospaceUtils.ts
+++ b/yospace/src/utils/YospaceUtils.ts
@@ -1,19 +1,19 @@
-import { YospaceServerSideAdInsertionConfiguration, YospaceSSAIIntegrationID, YospaceTypedSource } from "theoplayer";
-import { implementsInterface, isTypedSource } from "./SourceUtils";
-import { YospaceWindow } from "../yospace/YospaceWindow";
+import { YospaceServerSideAdInsertionConfiguration, YospaceSSAIIntegrationID, YospaceTypedSource } from 'theoplayer';
+import { implementsInterface, isTypedSource } from './SourceUtils';
+import { YospaceWindow } from '../yospace/YospaceWindow';
-export const YOSPACE_SSAI_INTEGRATION_ID: YospaceSSAIIntegrationID = "yospace";
+export const YOSPACE_SSAI_INTEGRATION_ID: YospaceSSAIIntegrationID = 'yospace';
export function isYoSpaceServerSideAdInsertionConfiguration(
ssai: any
): ssai is YospaceServerSideAdInsertionConfiguration {
- return implementsInterface(ssai, ["integration"]) && ssai.integration === YOSPACE_SSAI_INTEGRATION_ID;
+ return implementsInterface(ssai, ['integration']) && ssai.integration === YOSPACE_SSAI_INTEGRATION_ID;
}
export function isYospaceTypedSource(typedSource: any): typedSource is YospaceTypedSource {
return (
isTypedSource(typedSource) &&
- implementsInterface(typedSource, ["ssai"]) &&
+ implementsInterface(typedSource, ['ssai']) &&
isYoSpaceServerSideAdInsertionConfiguration(typedSource.ssai)
);
}
diff --git a/yospace/src/utils/event/EventDispatcher.ts b/yospace/src/utils/event/EventDispatcher.ts
index 89939a81..b5a329a7 100644
--- a/yospace/src/utils/event/EventDispatcher.ts
+++ b/yospace/src/utils/event/EventDispatcher.ts
@@ -1,4 +1,4 @@
-import { Event } from "./Event";
+import { Event } from './Event';
/**
* The keys of T which are strings.
diff --git a/yospace/src/yospace/AnalyticEventObserver.ts b/yospace/src/yospace/AnalyticEventObserver.ts
index 28d9fef3..6d5ab86a 100644
--- a/yospace/src/yospace/AnalyticEventObserver.ts
+++ b/yospace/src/yospace/AnalyticEventObserver.ts
@@ -1,6 +1,6 @@
-import { AdBreak, AdVert } from "./AdBreak";
-import { TrackingError } from "./TrackingError";
-import { YospaceSessionManager } from "./YospaceSessionManager";
+import { AdBreak, AdVert } from './AdBreak';
+import { TrackingError } from './TrackingError';
+import { YospaceSessionManager } from './YospaceSessionManager';
export enum SessionErrorCode {
TIMEOUT
diff --git a/yospace/src/yospace/YospaceAdManagement.ts b/yospace/src/yospace/YospaceAdManagement.ts
index b6a629b4..769bdd8f 100644
--- a/yospace/src/yospace/YospaceAdManagement.ts
+++ b/yospace/src/yospace/YospaceAdManagement.ts
@@ -1,6 +1,6 @@
-import { SessionProperties } from "./SessionProperties";
-import { TimedMetadata } from "./TimedMetadata";
-import { YospaceSessionManagerCreator } from "./YospaceSessionManager";
+import { SessionProperties } from './SessionProperties';
+import { TimedMetadata } from './TimedMetadata';
+import { YospaceSessionManagerCreator } from './YospaceSessionManager';
export interface YospaceAdManagement {
SessionLive: YospaceSessionManagerCreator;
diff --git a/yospace/src/yospace/YospaceSessionManager.ts b/yospace/src/yospace/YospaceSessionManager.ts
index e12b55c5..87f6bbc0 100644
--- a/yospace/src/yospace/YospaceSessionManager.ts
+++ b/yospace/src/yospace/YospaceSessionManager.ts
@@ -1,6 +1,6 @@
-import { PlayerEvent } from "./PlayerEvent";
-import { TimedMetadata } from "./TimedMetadata";
-import { AnalyticEventObserver } from "./AnalyticEventObserver";
+import { PlayerEvent } from './PlayerEvent';
+import { TimedMetadata } from './TimedMetadata';
+import { AnalyticEventObserver } from './AnalyticEventObserver';
export enum ResultCode {
CONNECTION_ERROR = -1,
diff --git a/yospace/src/yospace/YospaceWindow.ts b/yospace/src/yospace/YospaceWindow.ts
index 7fd0bf74..39a5fa5b 100644
--- a/yospace/src/yospace/YospaceWindow.ts
+++ b/yospace/src/yospace/YospaceWindow.ts
@@ -1,4 +1,4 @@
-import { YospaceAdManagement } from "./YospaceAdManagement";
+import { YospaceAdManagement } from './YospaceAdManagement';
export interface YospaceWindow extends Window {
YospaceAdManagement: YospaceAdManagement;
diff --git a/yospace/test/pages/main_esm.html b/yospace/test/pages/main_esm.html
index 0bcb8709..aaa8d92a 100644
--- a/yospace/test/pages/main_esm.html
+++ b/yospace/test/pages/main_esm.html
@@ -10,14 +10,14 @@