diff --git a/README.md b/README.md
index 2e6c094c..f622252f 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ the official THEOplayer React Native video player.
| Mux analytics | [](https://www.npmjs.com/package/%40theoplayer%2Freact-native-analytics-mux) | [`Mux`](https://github.com/THEOplayer/react-native-connectors/tree/main/mux) |
| Nielsen analytics | [](https://www.npmjs.com/package/%40theoplayer%2Freact-native-analytics-nielsen) | [`Nielsen`](https://github.com/THEOplayer/react-native-connectors/tree/main/nielsen) |
| Nielsen AdScript analytics | [](https://www.npmjs.com/package/%40theoplayer%2Freact-native-analytics-adscript) | [`AdScript`](https://github.com/THEOplayer/react-native-connectors/tree/main/adscript) |
+| NPAW (YOUBORA) analytics (NPAW v7+) | [](https://www.npmjs.com/package/%40theoplayer%2Freact-native-analytics-npaw) | [`NPAW`](https://github.com/THEOplayer/react-native-connectors/tree/main/npaw) |
| Yospace SSAI | [](https://www.npmjs.com/package/%40theoplayer%2Freact-native-yospace) | [`Yospace`](https://github.com/THEOplayer/react-native-connectors/tree/main/yospace) |
| Youbora analytics | [](https://www.npmjs.com/package/%40theoplayer%2Freact-native-analytics-youbora) | [`Youbora`](https://github.com/THEOplayer/react-native-connectors/tree/main/youbora) |
diff --git a/apps/e2e/.env.example b/apps/e2e/.env.example
index 73a74bd0..9a505bc2 100644
--- a/apps/e2e/.env.example
+++ b/apps/e2e/.env.example
@@ -6,6 +6,9 @@ CONVIVA_DEBUG=true
CONVIVA_TOUCHSTONE_SERVICE_URL=
CONVIVA_VIEWER_ID=e2e_viewer_id
+# NPAW
+NPAW_ACCOUNT_CODE=
+
# BITMOVIN
BITMOVIN_LICENSE_KEY=
BITMOVIN_LOG_LEVEL=DEBUG
diff --git a/apps/e2e/metro.config.js b/apps/e2e/metro.config.js
index d2cf485f..fd995762 100644
--- a/apps/e2e/metro.config.js
+++ b/apps/e2e/metro.config.js
@@ -11,9 +11,7 @@ function escapeRegExp(str) {
// Block these packages from being resolved out of root/node_modules.
// They are already explicitly mapped to the app-local copies via extraNodeModules.
-const rootNodeModulesBlockList = packages.map(
- (pkg) => new RegExp(`^${escapeRegExp(path.join(root, 'node_modules', pkg))}(/.*)?$`)
-);
+const rootNodeModulesBlockList = packages.map((pkg) => new RegExp(`^${escapeRegExp(path.join(root, 'node_modules', pkg))}(/.*)?$`));
const connectors = [
'adobe',
@@ -27,6 +25,7 @@ const connectors = [
'gemius',
'mux',
'nielsen',
+ 'npaw',
'yospace',
'youbora',
];
diff --git a/apps/e2e/src/App.tsx b/apps/e2e/src/App.tsx
index ae5c9dd7..cbbbe97c 100644
--- a/apps/e2e/src/App.tsx
+++ b/apps/e2e/src/App.tsx
@@ -39,6 +39,7 @@ import { ConnectorSubMenu } from './components/menu/ConnectorSubMenu';
import { ConnectorsMenuButton } from './components/menu/ConnectorMenu';
import { useAdobeEdgeConnector } from './connectors/adobe-edge';
import { muxOptions, useMuxConnector } from './connectors/mux';
+import { useNpawConnector } from './connectors/npaw';
const playerConfig: PlayerConfiguration = {
// Get your THEOplayer license from https://portal.theoplayer.com/
@@ -63,6 +64,7 @@ export default function App() {
const { bitmovin, initBitmovin, onProgramChange: onBitmovinProgramChange, onUpdateCustomData: onBitmovinUpdateCustomData } = useBitmovinConnector();
const { adobeEdge, initAdobeEdge } = useAdobeEdgeConnector();
const { initMux } = useMuxConnector();
+ const { initNpaw } = useNpawConnector();
const onSourceChange = () => {
console.log('onSourceChange');
@@ -70,7 +72,7 @@ export default function App() {
key1: 'value1',
key2: 'value2',
});
- }
+ };
const onPlayerReady = (player: THEOplayer) => {
setPlayer(player);
@@ -79,6 +81,7 @@ export default function App() {
initBitmovin(player, bitmovinDefaultMetadata);
initConviva(player);
initMux(player, muxOptions);
+ initNpaw(player);
// optional debug logs
player.addEventListener(PlayerEventType.SOURCE_CHANGE, onSourceChange);
diff --git a/apps/e2e/src/connectors/npaw.ts b/apps/e2e/src/connectors/npaw.ts
new file mode 100644
index 00000000..2cb3eb05
--- /dev/null
+++ b/apps/e2e/src/connectors/npaw.ts
@@ -0,0 +1,20 @@
+import { LogLevel, NpawConnectorConfig, useNpaw } from '@theoplayer/react-native-analytics-npaw';
+import Config from 'react-native-config';
+
+export const npawConfig: NpawConnectorConfig = {
+ accountCode: Config.NPAW_ACCOUNT_CODE,
+ analytics: {
+ 'content.title': 'THEOplayer E2E',
+ 'content.contentId': 'e2e-content',
+ 'content.metadata': {
+ customCategory: 'demo',
+ customEnvironment: 'e2e',
+ },
+ },
+ logLevel: LogLevel.DEBUG,
+};
+
+export function useNpawConnector() {
+ const [, initNpaw] = useNpaw(npawConfig);
+ return { initNpaw };
+}
diff --git a/apps/e2e/src/tests/Npaw.spec.ts b/apps/e2e/src/tests/Npaw.spec.ts
new file mode 100644
index 00000000..23c95a0d
--- /dev/null
+++ b/apps/e2e/src/tests/Npaw.spec.ts
@@ -0,0 +1,22 @@
+import { TestScope } from 'cavy';
+import { LogLevel, NpawConnector } from '@theoplayer/react-native-analytics-npaw';
+import { THEOplayer } from 'react-native-theoplayer';
+import { testConnector } from './ConnectorUtils';
+
+export default function (spec: TestScope) {
+ spec.describe(`Setup NPAW connector`, function () {
+ let connector: NpawConnector;
+ testConnector(
+ spec,
+ (player: THEOplayer) => {
+ connector = new NpawConnector(player, { accountCode: 'testAccountCode' });
+ },
+ () => {
+ connector.setLogLevel(LogLevel.DEBUG);
+ },
+ () => {
+ connector.destroy();
+ },
+ );
+ });
+}
diff --git a/apps/e2e/src/tests/index.ts b/apps/e2e/src/tests/index.ts
index 919e4cbf..ec5f9f38 100644
--- a/apps/e2e/src/tests/index.ts
+++ b/apps/e2e/src/tests/index.ts
@@ -5,7 +5,8 @@ import Bitmovin from './Bitmovin.spec';
import Comscore from './Comscore.spec';
import Conviva from './Conviva.spec';
import Nielsen from './Nielsen.spec';
+import Npaw from './Npaw.spec';
-const tests = [Adobe, AdobeNative, AdobeEdge, Bitmovin, Comscore, Conviva, Nielsen];
+const tests = [Adobe, AdobeNative, AdobeEdge, Bitmovin, Comscore, Conviva, Nielsen, Npaw];
export default tests;
diff --git a/apps/e2e/tsconfig.json b/apps/e2e/tsconfig.json
index 8d6cb506..1e97de64 100644
--- a/apps/e2e/tsconfig.json
+++ b/apps/e2e/tsconfig.json
@@ -10,6 +10,7 @@
"@theoplayer/react-native-analytics-conviva": ["../../conviva/src/index"],
"@theoplayer/react-native-analytics-mux": ["../../mux/src/index"],
"@theoplayer/react-native-analytics-nielsen": ["../../nielsen/src/index"],
+ "@theoplayer/react-native-analytics-npaw": ["../../npaw/src/index"],
"@theoplayer/react-native-yospace": ["../../yospace/src/index"],
"@theoplayer/react-native-adscript": ["../../adscript/src/index"],
"@theoplayer/react-native-gemius": ["../../gemius/src/index"]
diff --git a/npaw/.gitignore b/npaw/.gitignore
new file mode 100644
index 00000000..75356714
--- /dev/null
+++ b/npaw/.gitignore
@@ -0,0 +1,70 @@
+# OSX
+#
+.DS_Store
+
+# XDE
+.expo/
+
+# VSCode
+.vscode/
+jsconfig.json
+
+# Xcode
+#
+build/
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata
+*.xccheckout
+*.moved-aside
+DerivedData
+*.hmap
+*.ipa
+*.xcuserstate
+project.xcworkspace
+
+# Android/IJ
+#
+.classpath
+.cxx
+.gradle
+.idea
+.project
+.settings
+local.properties
+android.iml
+
+# Cocoapods
+#
+example/ios/Pods
+
+# Ruby
+example/vendor/
+
+# node.js
+#
+node_modules/
+npm-debug.log
+yarn-debug.log
+yarn-error.log
+
+# BUCK
+buck-out/
+\.buckd/
+android/app/libs
+android/keystores/debug.keystore
+
+# Expo
+.expo/
+
+# Turborepo
+.turbo/
+
+# generated by bob
+lib/
diff --git a/npaw/CHANGELOG.md b/npaw/CHANGELOG.md
new file mode 100644
index 00000000..6602719e
--- /dev/null
+++ b/npaw/CHANGELOG.md
@@ -0,0 +1,5 @@
+# @theoplayer/react-native-analytics-npaw
+
+## 1.0.0
+
+- Initial release.
diff --git a/npaw/LICENSE b/npaw/LICENSE
new file mode 100644
index 00000000..ed130fc5
--- /dev/null
+++ b/npaw/LICENSE
@@ -0,0 +1,33 @@
+The Clear BSD License
+
+Copyright (c) 2025 Dolby International AB
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted (subject to the limitations in the disclaimer
+below) provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED
+BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
+BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/npaw/README.md b/npaw/README.md
new file mode 100644
index 00000000..296a47e4
--- /dev/null
+++ b/npaw/README.md
@@ -0,0 +1,95 @@
+# THEOplayer React Native NPAW Connector
+
+An [NPAW](https://npaw.com/) (formerly YOUBORA) analytics connector for `@theoplayer/react-native`.
+
+This connector is JavaScript-only and works across iOS, Android and web through the unified
+`react-native-theoplayer` player, using only the `npaw-plugin-react-native` plugin — no native
+iOS/Android modules are required.
+
+> **Note:** This connector targets **NPAW v7+** (it depends on `npaw-plugin-react-native` `^7.3.x`).
+
+## Installation
+
+```sh
+npm install @theoplayer/react-native-analytics-npaw
+```
+
+## Usage
+
+### Configuring the connector
+
+Create the connector with the `useNpaw` hook, passing a config object and initializing it once the
+`THEOplayer` instance is ready:
+
+```tsx
+import { LogLevel, useNpaw } from '@theoplayer/react-native-analytics-npaw';
+
+const App = () => {
+ const [npaw, initNpaw] = useNpaw({
+ accountCode: 'your-account-code',
+ analytics: {
+ 'content.title': 'Big Buck Bunny',
+ 'content.isLive': false,
+ // Free-form custom metadata shown in the NPAW "Metadata" panel:
+ 'content.metadata': { genre: 'animation' },
+ },
+ // Optional NpawPlugin options (host, sessionRecovery, ...):
+ plugin: { sessionRecovery: 'auto' },
+ logLevel: LogLevel.DEBUG,
+ });
+
+ const onPlayerReady = (player: THEOplayer) => {
+ initNpaw(player);
+ };
+
+ return ;
+};
+```
+
+Alternatively, construct the `NpawConnector` class directly:
+
+```ts
+import { NpawConnector } from '@theoplayer/react-native-analytics-npaw';
+
+const connector = new NpawConnector(player, { accountCode: 'your-account-code' });
+```
+
+### Configuration
+
+| Field | Description |
+|---------------|------------------------------------------------------------------------------------|
+| `accountCode` | Your NPAW account code. **Required.** |
+| `analytics` | Per-video analytics options, e.g. `content.title`, `content.isLive`, `content.metadata`, `user.name`, ... |
+| `plugin` | `NpawPlugin` options such as `host`, `sessionRecovery` and `balancerAnalyticsEnabled`. |
+| `logLevel` | Initial `LogLevel`. |
+
+### What is tracked automatically
+
+Once initialized, the connector registers a video adapter (and an ads adapter) against the player
+and reports playback automatically:
+
+- Playback events (start, join, pause/resume, buffering, seeking, end and errors) mapped from the
+ unified `PlayerEventType` events.
+- Playhead, duration and live/VOD state, resource, rendition and bitrate.
+- The active subtitle/caption track language (`getSubtitles`) and audio language (`getVideoLanguage`).
+- Ads: ad breaks, ad start/join/stop, quartiles, skip, click and errors, with ad position
+ (`pre`/`mid`/`post`), duration and skippability.
+
+### Updating options at runtime
+
+```ts
+// Update options for the current video (e.g. late-arriving metadata):
+npaw.current?.setVideoOptions({ 'content.title': 'New Title' });
+
+// Update global analytics options:
+npaw.current?.setAnalyticsOptions({ 'user.name': 'viewer-1' });
+
+// Change the log level:
+npaw.current?.setLogLevel(LogLevel.SILENT);
+
+// Stop analytics and close all sessions:
+npaw.current?.destroy();
+```
+
+See the [NPAW integration documentation](https://documentation.npaw.com/integration-docs/docs/)
+for the full set of plugin and analytics options.
diff --git a/npaw/babel.config.js b/npaw/babel.config.js
new file mode 100644
index 00000000..f842b77f
--- /dev/null
+++ b/npaw/babel.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ presets: ['module:metro-react-native-babel-preset'],
+};
diff --git a/npaw/manifest.js b/npaw/manifest.js
new file mode 100755
index 00000000..e7629f28
--- /dev/null
+++ b/npaw/manifest.js
@@ -0,0 +1,6 @@
+const manifest = {
+ version: process.env.npm_package_version,
+ buildDate: new Date().toISOString(),
+};
+
+console.log(JSON.stringify(manifest));
diff --git a/npaw/package.json b/npaw/package.json
new file mode 100644
index 00000000..0142f317
--- /dev/null
+++ b/npaw/package.json
@@ -0,0 +1,75 @@
+{
+ "name": "@theoplayer/react-native-analytics-npaw",
+ "version": "1.0.0",
+ "description": "NPAW analytics connector for @theoplayer/react-native",
+ "main": "lib/commonjs/index",
+ "module": "lib/module/index",
+ "types": "lib/typescript/src/index.d.ts",
+ "react-native": "src/index",
+ "source": "src/index",
+ "files": [
+ "src",
+ "lib",
+ "*.tgz",
+ "CHANGELOG.md",
+ "!lib/typescript/example",
+ "!**/__tests__",
+ "!**/__fixtures__",
+ "!**/__mocks__",
+ "!**/.*"
+ ],
+ "scripts": {
+ "typescript": "tsc --noEmit",
+ "build": "bob build",
+ "manifest": "node manifest.js > src/manifest.json",
+ "prepare": "npm run manifest && npm run build",
+ "clean": "rimraf lib"
+ },
+ "keywords": [
+ "react-native",
+ "THEOplayer",
+ "npaw"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git@github.com:THEOplayer/react-native-connectors.git"
+ },
+ "author": "Dolby Laboratories, Inc.",
+ "license": "BSD-3-Clause-Clear",
+ "homepage": "https://github.com/THEOplayer/react-native-connectors/tree/main/npaw#readme",
+ "publishConfig": {
+ "registry": "https://registry.npmjs.org/"
+ },
+ "dependencies": {
+ "npaw-plugin-react-native": "^7.3.33"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*",
+ "react-native-theoplayer": "^7 || ^8 || ^9 || ^10 || ^11",
+ "theoplayer": "^7 || ^8 || ^9 || ^10 || ^11"
+ },
+ "peerDependenciesMeta": {
+ "theoplayer": {
+ "optional": true
+ }
+ },
+ "eslintIgnore": [
+ "node_modules/",
+ "lib/"
+ ],
+ "react-native-builder-bob": {
+ "source": "src",
+ "output": "lib",
+ "targets": [
+ "commonjs",
+ "module",
+ [
+ "typescript",
+ {
+ "project": "tsconfig.build.json"
+ }
+ ]
+ ]
+ }
+}
diff --git a/npaw/src/__tests__/index.test.tsx b/npaw/src/__tests__/index.test.tsx
new file mode 100644
index 00000000..cb0ff5c3
--- /dev/null
+++ b/npaw/src/__tests__/index.test.tsx
@@ -0,0 +1 @@
+export {};
diff --git a/npaw/src/api/LogLevel.ts b/npaw/src/api/LogLevel.ts
new file mode 100644
index 00000000..5d8a9209
--- /dev/null
+++ b/npaw/src/api/LogLevel.ts
@@ -0,0 +1,8 @@
+export enum LogLevel {
+ SILENT = 6,
+ ERROR = 5,
+ WARNING = 4,
+ NOTICE = 3,
+ DEBUG = 2,
+ VERBOSE = 1,
+}
diff --git a/npaw/src/api/NpawConnector.ts b/npaw/src/api/NpawConnector.ts
new file mode 100644
index 00000000..123b2e2a
--- /dev/null
+++ b/npaw/src/api/NpawConnector.ts
@@ -0,0 +1,66 @@
+import type { THEOplayer } from 'react-native-theoplayer';
+import type NpawPluginOptions from 'npaw-plugin-react-native/dist/src/core/NpawPluginOptions';
+import { LogLevel } from './LogLevel';
+import { NpawConnectorAdapter } from '../internal/NpawConnectorAdapter';
+
+export type NpawAnalyticsOptions = { [key: string]: unknown };
+
+export interface NpawConnectorConfig {
+ /** NPAW account code. */
+ accountCode: string;
+ /** Per-video analytics options (e.g. 'content.title', 'content.isLive', ...). */
+ analytics?: NpawAnalyticsOptions;
+ /** NpawPlugin options (host, sessionRecovery, ...). */
+ plugin?: NpawPluginOptions;
+ /** Initial log level. */
+ logLevel?: LogLevel;
+}
+
+export class NpawConnector {
+ private connectorAdapter: NpawConnectorAdapter;
+
+ /**
+ * Create an NPAW connector.
+ *
+ * @public
+ */
+ constructor(player: THEOplayer, config: NpawConnectorConfig) {
+ this.connectorAdapter = new NpawConnectorAdapter(player, config);
+ }
+
+ /**
+ * Change log level.
+ * @param level
+ */
+ setLogLevel(level: LogLevel) {
+ this.connectorAdapter.setLogLevel(level);
+ }
+
+ /**
+ * Sets or updates NPAW analytics options for the current video.
+ *
+ * @param options Analytics options, such as `content.title` or `content.metadata`.
+ * @param videoKey Optional NPAW video key.
+ */
+ setVideoOptions(options: NpawAnalyticsOptions, videoKey?: string): void {
+ this.connectorAdapter.setVideoOptions(options, videoKey);
+ }
+
+ /**
+ * Sets or updates global NPAW analytics options.
+ *
+ * @param options Global analytics options.
+ */
+ setAnalyticsOptions(options: NpawAnalyticsOptions): void {
+ this.connectorAdapter.setAnalyticsOptions(options);
+ }
+
+ /**
+ * Stops video and ad analytics and closes all sessions.
+ *
+ * @public
+ */
+ destroy(): void {
+ this.connectorAdapter.destroy();
+ }
+}
diff --git a/npaw/src/api/hooks/useNpaw.ts b/npaw/src/api/hooks/useNpaw.ts
new file mode 100644
index 00000000..fe819ac4
--- /dev/null
+++ b/npaw/src/api/hooks/useNpaw.ts
@@ -0,0 +1,39 @@
+import { PlayerEventType, THEOplayer } from 'react-native-theoplayer';
+import { RefObject, useEffect, useRef } from 'react';
+import { NpawConnector } from '../NpawConnector';
+import type { NpawConnectorConfig } from '../NpawConnector';
+
+export function useNpaw(config: NpawConnectorConfig): [RefObject, (player: THEOplayer | undefined) => void] {
+ const connector = useRef(undefined);
+ const theoPlayer = useRef(undefined);
+
+ const initialize = (player: THEOplayer | undefined) => {
+ // Optionally destroy existent connector
+ onDestroy();
+
+ theoPlayer.current = player;
+ if (player) {
+ connector.current = new NpawConnector(player, config);
+ player.addEventListener(PlayerEventType.DESTROY, onDestroy);
+ } else {
+ throw new Error('Invalid THEOplayer instance');
+ }
+ };
+
+ const onDestroy = () => {
+ if (connector.current) {
+ if (!theoPlayer.current) {
+ throw new Error('Invalid THEOplayer instance');
+ }
+ theoPlayer.current.removeEventListener(PlayerEventType.DESTROY, onDestroy);
+ connector.current.destroy();
+ connector.current = undefined;
+ }
+ };
+
+ useEffect(() => {
+ return onDestroy;
+ }, []);
+
+ return [connector, initialize];
+}
diff --git a/npaw/src/index.ts b/npaw/src/index.ts
new file mode 100644
index 00000000..be0b4f27
--- /dev/null
+++ b/npaw/src/index.ts
@@ -0,0 +1,7 @@
+import NpawPlugin from 'npaw-plugin-react-native';
+import { LogLevel } from './api/LogLevel';
+
+export { NpawConnector } from './api/NpawConnector';
+export type { NpawConnectorConfig, NpawAnalyticsOptions } from './api/NpawConnector';
+export { useNpaw } from './api/hooks/useNpaw';
+export { NpawPlugin, LogLevel };
diff --git a/npaw/src/internal/NpawConnectorAdapter.ts b/npaw/src/internal/NpawConnectorAdapter.ts
new file mode 100644
index 00000000..8955a4de
--- /dev/null
+++ b/npaw/src/internal/NpawConnectorAdapter.ts
@@ -0,0 +1,63 @@
+import type { THEOplayer } from 'react-native-theoplayer';
+import NpawPlugin from 'npaw-plugin-react-native';
+import type { NpawAnalyticsOptions } from '../api/NpawConnector';
+import type { NpawConnectorConfig } from '../api/NpawConnector';
+import type { LogLevel } from '../api/LogLevel';
+import { TheoplayerAdsAdapter } from './TheoplayerAdsAdapter';
+import { TheoplayerAdapter } from './TheoplayerAdapter';
+
+const TAG = 'NpawConnector';
+const ERROR_MSG = 'NpawConnectorAdapter Error';
+
+export class NpawConnectorAdapter {
+ private plugin?: NpawPlugin;
+
+ constructor(player: THEOplayer, config: NpawConnectorConfig) {
+ try {
+ this.plugin = new NpawPlugin(config.accountCode, config.plugin);
+ if (config.logLevel != null) this.plugin.setLogLevel(config.logLevel);
+ this.plugin.registerAdapterFromClass(player, TheoplayerAdapter, config.analytics);
+ this.plugin.registerAdsAdapterFromClass(player, TheoplayerAdsAdapter, config.analytics);
+ } catch (error: unknown) {
+ console.error(TAG, `${ERROR_MSG}: ${error}`);
+ }
+ }
+
+ setLogLevel(level: LogLevel) {
+ try {
+ this.plugin?.setLogLevel(level);
+ } catch (error: unknown) {
+ console.error(TAG, `${ERROR_MSG}: ${error}`);
+ }
+ }
+
+ setVideoOptions(options: NpawAnalyticsOptions, videoKey?: string): void {
+ try {
+ this.plugin?.setVideoOptions(options, videoKey);
+ } catch (error: unknown) {
+ console.error(TAG, `${ERROR_MSG}: ${error}`);
+ }
+ }
+
+ setAnalyticsOptions(options: NpawAnalyticsOptions): void {
+ try {
+ this.plugin?.setAnalyticsOptions(options);
+ } catch (error: unknown) {
+ console.error(TAG, `${ERROR_MSG}: ${error}`);
+ }
+ }
+
+ destroy(): void {
+ try {
+ // NPAW's removeAdapter only disposes the adapter; end any in-progress view first.
+ this.plugin?.getAdsAdapter()?.fireStop();
+ this.plugin?.getAdapter()?.fireStop();
+ this.plugin?.removeAdapter();
+ this.plugin?.removeAdsAdapter();
+ this.plugin?.destroy();
+ } catch (error: unknown) {
+ console.error(TAG, `${ERROR_MSG}: ${error}`);
+ }
+ this.plugin = undefined;
+ }
+}
diff --git a/npaw/src/internal/TheoplayerAdapter.ts b/npaw/src/internal/TheoplayerAdapter.ts
new file mode 100644
index 00000000..39ebf3fc
--- /dev/null
+++ b/npaw/src/internal/TheoplayerAdapter.ts
@@ -0,0 +1,247 @@
+// noinspection JSUnusedGlobalSymbols
+
+import {
+ type EventListener,
+ isVideoQuality,
+ type PlayerEventMap,
+ PlayerEventType,
+ TextTrackKind,
+ TextTrackMode,
+ type SourceDescription,
+ type THEOplayer,
+ type VideoQuality,
+} from 'react-native-theoplayer';
+import manifest from '../manifest.json';
+
+interface NpawPlaybackFlags {
+ isStarted: boolean;
+ isJoined: boolean;
+ isPaused: boolean;
+ isSeeking: boolean;
+ isBuffering: boolean;
+ reset(): void;
+}
+
+type PlayerEvent = PlayerEventMap[PlayerEventType];
+type PlayerListener = EventListener;
+type PlayerReference = {
+ type: PlayerEventType;
+ listener: PlayerListener;
+};
+
+export class TheoplayerAdapter {
+ /**
+ * Provided at runtime by NPAW's base video adapter; declared for typing only.
+ * Implementations are copied onto the instance during registerAdapterFromClass.
+ */
+ protected player!: THEOplayer;
+ protected flags!: NpawPlaybackFlags;
+ protected references!: PlayerReference[];
+
+ protected fireStart!: (params?: object, triggeredEvent?: string) => void;
+ protected fireJoin!: (params?: object, triggeredEvent?: string) => void;
+ protected firePause!: (params?: object, triggeredEvent?: string) => void;
+ protected fireResume!: (params?: object, triggeredEvent?: string) => void;
+ protected fireBufferBegin!: (params?: object, convertFromSeek?: boolean, triggeredEvent?: string) => void;
+ protected fireBufferEnd!: (params?: object, triggeredEvent?: string) => void;
+ protected fireStop!: (params?: object, triggeredEvent?: string) => void;
+ protected fireError!: (code?: string | object, msg?: string, metadata?: object, level?: string, triggeredEvent?: string) => void;
+ protected fireSeekBegin!: (params?: object, convertFromBuffer?: boolean, triggeredEvent?: string) => void;
+ protected fireSeekEnd!: (params?: object, triggeredEvent?: string) => void;
+ protected getNpawUtils!: () => {
+ buildRenditionString(width?: number, height?: number, bitrate?: number): string;
+ };
+
+ private lastPlayhead?: number;
+ private currentSource?: SourceDescription;
+
+ getPlayhead(): number | undefined {
+ try {
+ this.lastPlayhead = this.player.currentTime / 1000;
+ return this.lastPlayhead;
+ } catch {
+ return undefined;
+ }
+ }
+
+ getDuration(): number | undefined {
+ const duration = this.player.duration;
+ return Number.isFinite(duration) ? duration / 1000 : undefined;
+ }
+
+ getIsLive(): boolean {
+ return this.player.duration === Infinity;
+ }
+
+ getResource(): string | undefined {
+ const sources = this.player.source?.sources;
+ const source = Array.isArray(sources) ? sources[0] : sources;
+ return source?.src;
+ }
+
+ getSubtitles(): string | undefined {
+ const track = this.player?.textTracks?.find(
+ (textTrack) =>
+ textTrack.mode === TextTrackMode.showing && (textTrack.kind === TextTrackKind.subtitles || textTrack.kind === TextTrackKind.captions),
+ );
+ return track?.language || track?.label || undefined;
+ }
+
+ getVideoLanguage(): string | undefined {
+ const track = this.player?.audioTracks?.find((audioTrack) => audioTrack.enabled);
+ return track?.language || track?.label || undefined;
+ }
+
+ getRendition(): string | undefined {
+ const quality = this.getActiveQuality();
+ if (!quality || !isVideoQuality(quality)) {
+ return undefined;
+ }
+ return this.getNpawUtils().buildRenditionString(quality.width, quality.height, quality.bandwidth);
+ }
+
+ getBitrate(): number | undefined {
+ return this.getActiveQuality()?.bandwidth;
+ }
+
+ getPlayerName(): string {
+ return 'THEOplayer';
+ }
+
+ getPlayerVersion(): string | undefined {
+ return this.player?.version?.version;
+ }
+
+ getVersion(): string {
+ return `${manifest.version}-react-native-theoplayerclass`;
+ }
+
+ registerListeners(): void {
+ this.references = [
+ this.reference(PlayerEventType.PLAY, this.onPlay),
+ this.reference(PlayerEventType.PLAYING, this.onPlaying),
+ this.reference(PlayerEventType.PAUSE, this.onPause),
+ this.reference(PlayerEventType.WAITING, this.onWaiting),
+ this.reference(PlayerEventType.SEEKING, this.onSeeking),
+ this.reference(PlayerEventType.SEEKED, this.onSeeked),
+ this.reference(PlayerEventType.TIME_UPDATE, this.onTimeUpdate),
+ this.reference(PlayerEventType.SOURCE_CHANGE, this.onSourceChange),
+ this.reference(PlayerEventType.ENDED, this.onEnded),
+ this.reference(PlayerEventType.ERROR, this.onError),
+ ];
+ this.currentSource = this.player.source;
+
+ for (const reference of this.references) {
+ this.player.addEventListener(reference.type, reference.listener);
+ }
+ }
+
+ unregisterListeners(): void {
+ for (const reference of this.references) {
+ this.player.removeEventListener(reference.type, reference.listener);
+ }
+ this.references = [];
+ this.currentSource = undefined;
+ }
+
+ private reference(type: PlayerEventType, listener: PlayerListener): PlayerReference {
+ return {
+ type,
+ listener: listener.bind(this),
+ };
+ }
+
+ private getActiveQuality(): VideoQuality | undefined {
+ return this.player.videoTracks.find((track) => track.enabled && isVideoQuality(track.activeQuality))?.activeQuality as VideoQuality | undefined;
+ }
+
+ private onPlay(): void {
+ if (!this.flags.isStarted) {
+ this.fireStart({}, 'onPlay');
+ }
+ }
+
+ private onPlaying(): void {
+ if (this.flags.isSeeking && !this.flags.isPaused) {
+ this.fireSeekEnd({}, 'onPlaying');
+ }
+ if (this.flags.isBuffering && !this.flags.isPaused) {
+ this.fireBufferEnd({}, 'onPlaying');
+ }
+ this.fireResume({}, 'onPlaying');
+ if (!this.flags.isStarted) {
+ this.fireStart({}, 'onPlaying');
+ }
+ if (!this.flags.isJoined) {
+ this.fireJoin({}, 'onPlaying');
+ }
+ }
+
+ private onPause(): void {
+ if (!this.flags.isSeeking) {
+ this.firePause({}, 'onPause');
+ }
+ }
+
+ private onWaiting(): void {
+ if (!this.flags.isPaused && !this.flags.isSeeking) {
+ this.fireBufferBegin({}, false, 'onWaiting');
+ }
+ }
+
+ private onSeeking(): void {
+ if (!this.flags.isSeeking) {
+ this.fireSeekBegin({}, false, 'onSeeking');
+ }
+ }
+
+ private onSeeked(): void {
+ if (this.flags.isSeeking) {
+ this.fireSeekEnd({}, 'onSeeked');
+ }
+ }
+
+ private onTimeUpdate(event: PlayerEvent): void {
+ this.lastPlayhead = 'currentTime' in event && typeof event.currentTime === 'number' ? event.currentTime / 1000 : this.player.currentTime / 1000;
+ if (!this.flags.isStarted && this.lastPlayhead > 0) {
+ this.fireStart({}, 'onTimeUpdate');
+ this.fireJoin({}, 'onTimeUpdate');
+ }
+ if (!this.flags.isPaused) {
+ this.fireSeekEnd({}, 'onTimeUpdate');
+ this.fireBufferEnd({}, 'onTimeUpdate');
+ }
+ }
+
+ private onSourceChange(): void {
+ const source = this.player.source;
+ if (source === this.currentSource) {
+ return;
+ }
+ this.currentSource = source;
+ if (this.flags.isStarted) {
+ this.fireStop({}, 'onSourceChange');
+ }
+ this.flags.reset();
+ this.lastPlayhead = undefined;
+ }
+
+ private onEnded(): void {
+ if (this.flags.isStarted) {
+ this.fireStop({}, 'onEnded');
+ }
+ }
+
+ private onError(event: PlayerEvent): void {
+ if ('error' in event) {
+ const error = event.error;
+ if (typeof error === 'string') {
+ this.fireError('GENERIC_ERROR', error, undefined, undefined, 'onError');
+ } else if (typeof error === 'object' && error !== null && 'errorCode' in error && 'errorMessage' in error) {
+ this.fireError(String(error.errorCode), String(error.errorMessage), undefined, undefined, 'onError');
+ } else {
+ this.fireError('GENERIC_ERROR', String(error), undefined, undefined, 'onError');
+ }
+ }
+ }
+}
diff --git a/npaw/src/internal/TheoplayerAdsAdapter.ts b/npaw/src/internal/TheoplayerAdsAdapter.ts
new file mode 100644
index 00000000..18f9650a
--- /dev/null
+++ b/npaw/src/internal/TheoplayerAdsAdapter.ts
@@ -0,0 +1,156 @@
+import { AdEventType, PlayerEventType, type Ad, type AdBreak, type AdEvent, type EventListener, type THEOplayer } from 'react-native-theoplayer';
+import manifest from '../manifest.json';
+
+type AdReference = {
+ type: PlayerEventType.AD_EVENT;
+ listener: EventListener;
+};
+
+export class TheoplayerAdsAdapter {
+ /**
+ * Provided at runtime by NPAW's base ads adapter; declared for typing only.
+ * Implementations are copied onto the instance during registerAdsAdapterFromClass.
+ */
+ protected player!: THEOplayer;
+ protected references!: AdReference[];
+
+ protected fireStart!: (params?: object, triggeredEvent?: string) => void;
+ protected fireJoin!: (params?: object, triggeredEvent?: string) => void;
+ protected fireStop!: (params?: object, triggeredEvent?: string) => void;
+ protected fireError!: (
+ code?: string | object,
+ msg?: string,
+ metadata?: object,
+ level?: string,
+ triggeredEvent?: string,
+ fatalError?: boolean,
+ duration?: number,
+ ) => void;
+ protected fireClick!: (params?: object) => void;
+ protected fireQuartile!: (quartile: number) => void;
+ protected fireSkip!: (params?: object) => void;
+ protected fireBreakStart!: (params?: object) => void;
+ protected fireBreakStop!: (params?: object) => void;
+
+ private currentAd?: Ad;
+ private currentAdBreak?: AdBreak;
+
+ getDuration(): number | undefined {
+ return this.currentAd?.duration;
+ }
+
+ getResource(): string | undefined {
+ return this.currentAd?.resourceURI;
+ }
+
+ getTitle(): string | undefined {
+ return this.currentAd?.id;
+ }
+
+ getPosition(): 'pre' | 'mid' | 'post' | undefined {
+ const timeOffset = this.currentAdBreak?.timeOffset;
+ const duration = this.player?.duration;
+ if (timeOffset == null) {
+ return undefined;
+ }
+ if (timeOffset === 0) {
+ return 'pre';
+ }
+ if (timeOffset < 0 || (Number.isFinite(duration) && timeOffset >= duration / 1000)) {
+ return 'post';
+ }
+ return 'mid';
+ }
+
+ getIsSkippable(): boolean {
+ const skipOffset = this.currentAd?.skipOffset;
+ return skipOffset != null && skipOffset >= 0;
+ }
+
+ getPlayerName(): string {
+ return 'THEOplayer';
+ }
+
+ getPlayerVersion(): string | undefined {
+ return this.player?.version?.version;
+ }
+
+ getVersion(): string {
+ return `${manifest.version}-react-native-theoplayeradsclass`;
+ }
+
+ registerListeners(): void {
+ this.references = [
+ {
+ type: PlayerEventType.AD_EVENT,
+ listener: this.onAdEvent.bind(this),
+ },
+ ];
+
+ for (const reference of this.references) {
+ this.player.addEventListener(reference.type, reference.listener);
+ }
+ }
+
+ unregisterListeners(): void {
+ for (const reference of this.references) {
+ this.player?.removeEventListener(reference.type, reference.listener);
+ }
+ this.references = [];
+ }
+
+ private onAdEvent(event: AdEvent): void {
+ switch (event.subType) {
+ case AdEventType.AD_BREAK_BEGIN:
+ if (this.isAdBreak(event.ad)) {
+ this.currentAdBreak = event.ad;
+ this.fireBreakStart();
+ }
+ break;
+ case AdEventType.AD_BEGIN:
+ if (this.isAd(event.ad)) {
+ this.currentAd = event.ad;
+ this.fireStart();
+ this.fireJoin();
+ }
+ break;
+ case AdEventType.AD_FIRST_QUARTILE:
+ this.fireQuartile(1);
+ break;
+ case AdEventType.AD_MIDPOINT:
+ this.fireQuartile(2);
+ break;
+ case AdEventType.AD_THIRD_QUARTILE:
+ this.fireQuartile(3);
+ break;
+ case AdEventType.AD_SKIP:
+ this.fireSkip();
+ break;
+ case AdEventType.AD_CLICKED:
+ this.fireClick();
+ break;
+ case AdEventType.AD_END:
+ this.fireStop();
+ this.currentAd = undefined;
+ break;
+ case AdEventType.AD_BREAK_END:
+ this.fireBreakStop();
+ this.currentAdBreak = undefined;
+ break;
+ case AdEventType.AD_ERROR:
+ this.fireError('AD_ERROR', 'NPAW ad error', undefined, undefined, 'onAdError');
+ break;
+ case AdEventType.AD_BUFFERING:
+ // THEOplayer does not provide a reliable matching ad buffer-end event.
+ break;
+ }
+ }
+
+ private isAd(value: Ad | AdBreak): value is Ad {
+ return 'adBreak' in value;
+ }
+
+ private isAdBreak(value: Ad | AdBreak): value is AdBreak {
+ return 'ads' in value || 'timeOffset' in value;
+ }
+}
diff --git a/npaw/tsconfig.build.json b/npaw/tsconfig.build.json
new file mode 100644
index 00000000..f929194b
--- /dev/null
+++ b/npaw/tsconfig.build.json
@@ -0,0 +1,5 @@
+
+{
+ "extends": "./tsconfig",
+ "exclude": ["example", "ios", "android"]
+}
diff --git a/npaw/tsconfig.json b/npaw/tsconfig.json
new file mode 100644
index 00000000..a4c934d8
--- /dev/null
+++ b/npaw/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": "./",
+ "paths": {
+ "@theoplayer/react-native-analytics-npaw": [
+ "./src/index"
+ ]
+ }
+ }
+}
diff --git a/npaw/typedoc.json b/npaw/typedoc.json
new file mode 100644
index 00000000..ae2d161b
--- /dev/null
+++ b/npaw/typedoc.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://typedoc.org/schema.json",
+ "extends": [
+ "../typedoc.base.json"
+ ],
+ "entryPoints": [
+ "src/index.ts"
+ ],
+ "tsconfig": "tsconfig.json",
+ "readme": "README.md",
+ "name": "NPAW Connector"
+}
diff --git a/package-lock.json b/package-lock.json
index d664e005..369de221 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"engage",
"mux",
"nielsen",
+ "npaw",
"yospace",
"youbora",
"gemius"
@@ -137,7 +138,7 @@
},
"bitmovin": {
"name": "@theoplayer/react-native-analytics-bitmovin",
- "version": "1.4.1",
+ "version": "1.5.0",
"license": "BSD-3-Clause-Clear",
"dependencies": {
"bitmovin-analytics": "^2.51.0"
@@ -5725,6 +5726,10 @@
"resolved": "nielsen",
"link": true
},
+ "node_modules/@theoplayer/react-native-analytics-npaw": {
+ "resolved": "npaw",
+ "link": true
+ },
"node_modules/@theoplayer/react-native-analytics-youbora": {
"resolved": "youbora",
"link": true
@@ -6482,6 +6487,20 @@
"win32"
]
},
+ "node_modules/@videojs/vhs-utils": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.2.tgz",
+ "integrity": "sha512-1Dsv5dtWOq0LP1G37O46FQFA2tKAZON5UMe6dFPxT7JvPhEx8QIXcFQxan8+hHI3fX6aomFRFrUBJNgRYQDSXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "global": "^4.4.0"
+ },
+ "engines": {
+ "node": ">=8",
+ "npm": ">=5"
+ }
+ },
"node_modules/@vitest/coverage-v8": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.6.tgz",
@@ -6640,6 +6659,15 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.13",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
+ "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -8249,6 +8277,17 @@
"node": ">=10"
}
},
+ "node_modules/core-js": {
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
+ "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/core-js-compat": {
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz",
@@ -8691,6 +8730,11 @@
"node": ">=6.0.0"
}
},
+ "node_modules/dom-walk": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
+ "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -10244,6 +10288,16 @@
"node": ">=10.13.0"
}
},
+ "node_modules/global": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
+ "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
+ "license": "MIT",
+ "dependencies": {
+ "min-document": "^2.19.0",
+ "process": "^0.11.10"
+ }
+ },
"node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@@ -10471,6 +10525,12 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/hls-parser": {
+ "version": "0.10.6",
+ "resolved": "https://registry.npmjs.org/hls-parser/-/hls-parser-0.10.6.tgz",
+ "integrity": "sha512-t9bKB1fDDVajh5CY/qeZanNMnCm4hAWMroDlflnuboOskYR26z29bEyOYwQRP1MxP2Y83UC2J93DQCC1AMhBCA==",
+ "license": "MIT"
+ },
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -13875,6 +13935,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/min-document": {
+ "version": "2.19.2",
+ "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz",
+ "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==",
+ "license": "MIT",
+ "dependencies": {
+ "dom-walk": "^0.1.0"
+ }
+ },
"node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
@@ -13930,6 +13999,21 @@
"node": "*"
}
},
+ "node_modules/mpd-parser": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.4.0.tgz",
+ "integrity": "sha512-blbA7XpAaOdC/PR0Tu8GiUxlx6CpBuRowQPYV7lhi9Yr9G9+dkPXIUjqjzc/NCn/uZZPopluaJlIYntIEI/VLw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "@videojs/vhs-utils": "^4.1.2",
+ "@xmldom/xmldom": "^0.8.3",
+ "global": "^4.4.0"
+ },
+ "bin": {
+ "mpd-to-m3u8-json": "bin/parse.js"
+ }
+ },
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -14078,6 +14162,20 @@
"node": ">=0.10.0"
}
},
+ "node_modules/npaw-plugin-react-native": {
+ "version": "7.3.33",
+ "resolved": "https://registry.npmjs.org/npaw-plugin-react-native/-/npaw-plugin-react-native-7.3.33.tgz",
+ "integrity": "sha512-dkkq1z1ducvf/l4/rPcKKOqpKlBrJ+rGlv/OyOhHCDV/5siafYyPLirmUpBaIzMv/LCwpA9nhz/SUO7fGhrcPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "core-js": "^3.33.0",
+ "hls-parser": "0.10.6",
+ "mpd-parser": "^1.3.1"
+ },
+ "engines": {
+ "node": ">= 10.13"
+ }
+ },
"node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@@ -14812,6 +14910,15 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -19050,6 +19157,25 @@
"zod": "^3.25.0 || ^4.0.0"
}
},
+ "npaw": {
+ "name": "@theoplayer/react-native-analytics-npaw",
+ "version": "1.0.0",
+ "license": "BSD-3-Clause-Clear",
+ "dependencies": {
+ "npaw-plugin-react-native": "^7.3.33"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*",
+ "react-native-theoplayer": "^7 || ^8 || ^9 || ^10 || ^11",
+ "theoplayer": "^7 || ^8 || ^9 || ^10 || ^11"
+ },
+ "peerDependenciesMeta": {
+ "theoplayer": {
+ "optional": true
+ }
+ }
+ },
"yospace": {
"name": "@theoplayer/react-native-yospace",
"version": "1.7.0",
diff --git a/package.json b/package.json
index 5baa9c64..4a6b9ad4 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"engage",
"mux",
"nielsen",
+ "npaw",
"yospace",
"youbora",
"gemius"