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 @@