From 080bc832233a7d8e7056c57fccf689d40e5ac7d3 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 5 Dec 2025 13:55:51 +0100 Subject: [PATCH 01/70] Add alloy dependency --- adobe-edge/package.json | 4 + package-lock.json | 2880 +++++++++++++++++++++++++++++++++++---- 2 files changed, 2590 insertions(+), 294 deletions(-) diff --git a/adobe-edge/package.json b/adobe-edge/package.json index 1075f217..41a87142 100644 --- a/adobe-edge/package.json +++ b/adobe-edge/package.json @@ -84,6 +84,10 @@ ] }, "dependencies": { + "@adobe/alloy": "^2.30.0", "openapi-fetch": "^0.9.8" + }, + "devDependencies": { + "metro-react-native-babel-preset": "^0.77.0" } } diff --git a/package-lock.json b/package-lock.json index 3d729ddf..994ca668 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,8 +73,12 @@ "version": "0.7.0", "license": "SEE LICENSE AT https://www.theoplayer.com/terms", "dependencies": { + "@adobe/alloy": "^2.30.0", "openapi-fetch": "^0.9.8" }, + "devDependencies": { + "metro-react-native-babel-preset": "^0.77.0" + }, "peerDependencies": { "react": "*", "react-native": "*", @@ -145,7 +149,7 @@ }, "conviva": { "name": "@theoplayer/react-native-analytics-conviva", - "version": "1.11.0", + "version": "1.11.1", "license": "SEE LICENSE AT https://www.theoplayer.com/terms", "dependencies": { "@convivainc/conviva-js-coresdk": "^4.8.0", @@ -238,6 +242,73 @@ } } }, + "node_modules/@adobe/aep-rules-engine": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@adobe/aep-rules-engine/-/aep-rules-engine-3.1.1.tgz", + "integrity": "sha512-YeFDDSEM4wjwTNM4drKTIbYvfSdYS3DmeOgb1SxZfSXTcVg8vdxcd5blE2wKO42Qa4KUJK+ziK7ABvbHoO7T8Q==", + "license": "Apache-2.0", + "dependencies": { + "@vitest/coverage-v8": "^3.1.4" + } + }, + "node_modules/@adobe/alloy": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@adobe/alloy/-/alloy-2.30.0.tgz", + "integrity": "sha512-A2MFFUB2aurYdGt12q3Vov263Gu2KSvMO+CgWxufsK/W7dQIaJfUuu02qQrxtul/Ar7JK8FsIwNWR/ct9fjJjA==", + "license": "Apache-2.0", + "dependencies": { + "@adobe/aep-rules-engine": "^3.1.1", + "@adobe/reactor-query-string": "^2.0.0", + "@babel/core": "^7.28.4", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/preset-env": "^7.28.3", + "@inquirer/prompts": "^7.8.4", + "@rollup/plugin-babel": "^6.0.4", + "@rollup/plugin-commonjs": "^28.0.6", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", + "commander": "^14.0.0", + "css.escape": "^1.5.1", + "js-cookie": "3.0.5", + "rollup": "^4.50.1", + "rollup-plugin-license": "^3.6.0", + "uuid": "^11.1.0" + }, + "bin": { + "alloyBuilder": "scripts/alloyBuilder.js" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.50.1" + } + }, + "node_modules/@adobe/alloy/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@adobe/reactor-query-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@adobe/reactor-query-string/-/reactor-query-string-2.0.0.tgz", + "integrity": "sha512-rGNnmKjpjA898mOHP5xU05geL50uwQDCxx6Ekh8C+l4Pem5OJIZJN/weqTgzVVxp9F+mRdPixFW5PqCEZrnTuA==", + "license": "Apache-2.0" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "license": "MIT", @@ -333,7 +404,6 @@ }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.27.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" @@ -365,7 +435,6 @@ }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.28.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -385,7 +454,6 @@ }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { "version": "6.3.1", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -393,7 +461,6 @@ }, "node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", @@ -409,7 +476,6 @@ }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { "version": "6.3.1", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -417,7 +483,6 @@ }, "node_modules/@babel/helper-define-polyfill-provider": { "version": "0.6.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", @@ -432,7 +497,6 @@ }, "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { "version": "1.22.10", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -449,6 +513,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "license": "MIT", @@ -458,7 +535,6 @@ }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -496,7 +572,6 @@ }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.1" @@ -514,7 +589,6 @@ }, "node_modules/@babel/helper-remap-async-to-generator": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", @@ -530,7 +604,6 @@ }, "node_modules/@babel/helper-replace-supers": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", @@ -546,7 +619,6 @@ }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -579,7 +651,6 @@ }, "node_modules/@babel/helper-wrap-function": { "version": "7.28.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", @@ -616,7 +687,6 @@ }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -631,7 +701,6 @@ }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -645,7 +714,6 @@ }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -659,7 +727,6 @@ }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -675,7 +742,6 @@ }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { "version": "7.28.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -688,9 +754,156 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz", + "integrity": "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -742,6 +955,35 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.27.1.tgz", + "integrity": "sha512-eBC/3KSekshx19+N40MzjWqJd7KTEdOoLesAfa4IDFI8eRz5a47i5Oszus6zG/cwIXN63YhgLOMSSNJx49sENg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-flow": { "version": "7.27.1", "dev": true, @@ -758,7 +1000,6 @@ }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -919,7 +1160,6 @@ }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -934,7 +1174,6 @@ }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -948,7 +1187,6 @@ }, "node_modules/@babel/plugin-transform-async-generator-functions": { "version": "7.28.0", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -964,7 +1202,6 @@ }, "node_modules/@babel/plugin-transform-async-to-generator": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", @@ -980,7 +1217,6 @@ }, "node_modules/@babel/plugin-transform-block-scoped-functions": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -994,7 +1230,6 @@ }, "node_modules/@babel/plugin-transform-block-scoping": { "version": "7.28.4", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1008,7 +1243,6 @@ }, "node_modules/@babel/plugin-transform-class-properties": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", @@ -1023,7 +1257,6 @@ }, "node_modules/@babel/plugin-transform-class-static-block": { "version": "7.28.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", @@ -1038,7 +1271,6 @@ }, "node_modules/@babel/plugin-transform-classes": { "version": "7.28.4", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -1057,7 +1289,6 @@ }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1072,7 +1303,6 @@ }, "node_modules/@babel/plugin-transform-destructuring": { "version": "7.28.0", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1087,7 +1317,6 @@ }, "node_modules/@babel/plugin-transform-dotall-regex": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1102,7 +1331,6 @@ }, "node_modules/@babel/plugin-transform-duplicate-keys": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1116,7 +1344,6 @@ }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1131,7 +1358,6 @@ }, "node_modules/@babel/plugin-transform-dynamic-import": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1145,7 +1371,6 @@ }, "node_modules/@babel/plugin-transform-explicit-resource-management": { "version": "7.28.0", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1160,7 +1385,6 @@ }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1174,7 +1398,6 @@ }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1203,7 +1426,6 @@ }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1218,7 +1440,6 @@ }, "node_modules/@babel/plugin-transform-function-name": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", @@ -1234,7 +1455,6 @@ }, "node_modules/@babel/plugin-transform-json-strings": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1248,7 +1468,6 @@ }, "node_modules/@babel/plugin-transform-literals": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1262,7 +1481,6 @@ }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1276,7 +1494,6 @@ }, "node_modules/@babel/plugin-transform-member-expression-literals": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1290,7 +1507,6 @@ }, "node_modules/@babel/plugin-transform-modules-amd": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1305,7 +1521,6 @@ }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1320,7 +1535,6 @@ }, "node_modules/@babel/plugin-transform-modules-systemjs": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1337,7 +1551,6 @@ }, "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1352,7 +1565,6 @@ }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1367,7 +1579,6 @@ }, "node_modules/@babel/plugin-transform-new-target": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1381,7 +1592,6 @@ }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1395,7 +1605,6 @@ }, "node_modules/@babel/plugin-transform-numeric-separator": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1409,7 +1618,6 @@ }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.28.4", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", @@ -1427,7 +1635,6 @@ }, "node_modules/@babel/plugin-transform-object-super": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1442,7 +1649,6 @@ }, "node_modules/@babel/plugin-transform-optional-catch-binding": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1456,7 +1662,6 @@ }, "node_modules/@babel/plugin-transform-optional-chaining": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1471,7 +1676,6 @@ }, "node_modules/@babel/plugin-transform-parameters": { "version": "7.27.7", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1485,7 +1689,6 @@ }, "node_modules/@babel/plugin-transform-private-methods": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", @@ -1500,7 +1703,6 @@ }, "node_modules/@babel/plugin-transform-private-property-in-object": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", @@ -1516,7 +1718,6 @@ }, "node_modules/@babel/plugin-transform-property-literals": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1574,12 +1775,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { + "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { @@ -1589,8 +1791,10 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.4", + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1603,24 +1807,23 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { + "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.27.1", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "dev": true, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1632,27 +1835,25 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-shorthand-properties": { + "node_modules/@babel/plugin-transform-regexp-modifiers": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-spread": { + "node_modules/@babel/plugin-transform-reserved-words": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1661,16 +1862,73 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz", + "integrity": "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, "peerDependencies": { "@babel/core": "^7.0.0-0" } @@ -1691,7 +1949,6 @@ }, "node_modules/@babel/plugin-transform-template-literals": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1705,7 +1962,6 @@ }, "node_modules/@babel/plugin-transform-typeof-symbol": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1737,7 +1993,6 @@ }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1751,7 +2006,6 @@ }, "node_modules/@babel/plugin-transform-unicode-property-regex": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1766,7 +2020,6 @@ }, "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1781,7 +2034,6 @@ }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { "version": "7.27.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1796,7 +2048,6 @@ }, "node_modules/@babel/preset-env": { "version": "7.28.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.0", @@ -1879,7 +2130,6 @@ }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1903,7 +2153,6 @@ }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -2014,6 +2263,15 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@changesets/apply-release-plan": { "version": "7.0.13", "dev": true, @@ -2409,13 +2667,26 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@inquirer/external-editor": { + "node_modules/@inquirer/ansi": { "version": "1.0.2", - "dev": true, + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", "license": "MIT", "dependencies": { - "chardet": "^2.1.0", - "iconv-lite": "^0.7.0" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -2429,153 +2700,490 @@ } } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "dev": true, - "license": "ISC", + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "dev": true, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, "engines": { - "node": ">=12" + "node": ">=18" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "dev": true, + "node_modules/@inquirer/core/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/@inquirer/core/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=8" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "dev": true, + "node_modules/@inquirer/core/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "dev": true, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "dev": true, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": ">=12" + "node": ">=18" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/ttlcache": { - "version": "1.4.1", - "license": "ISC", + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "license": "ISC", + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@jest/create-cache-key-function": { - "version": "29.7.0", + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jest/environment": { - "version": "29.7.0", + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", "license": "MIT", "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" @@ -2785,7 +3393,6 @@ }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -2924,64 +3531,531 @@ "version": "0.79.6", "dev": true, "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/eslint-parser": "^7.25.1", - "@react-native/eslint-plugin": "0.79.6", - "@typescript-eslint/eslint-plugin": "^7.1.1", - "@typescript-eslint/parser": "^7.1.1", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-ft-flow": "^2.0.1", - "eslint-plugin-jest": "^27.9.0", - "eslint-plugin-react": "^7.30.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-native": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": ">=8", - "prettier": ">=2" - } - }, - "node_modules/@react-native/eslint-config/node_modules/eslint-config-prettier": { - "version": "8.10.2", - "dev": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/eslint-parser": "^7.25.1", + "@react-native/eslint-plugin": "0.79.6", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-ft-flow": "^2.0.1", + "eslint-plugin-jest": "^27.9.0", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-native": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": ">=8", + "prettier": ">=2" + } + }, + "node_modules/@react-native/eslint-config/node_modules/eslint-config-prettier": { + "version": "8.10.2", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/@react-native/eslint-plugin": { + "version": "0.79.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.79.6", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.79.6", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.79.6", + "license": "MIT" + }, + "node_modules/@rollup/plugin-babel": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz", + "integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.9", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.9.tgz", + "integrity": "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "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/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/@react-native/eslint-plugin": { - "version": "0.79.6", - "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-native/gradle-plugin": { - "version": "0.79.6", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-native/js-polyfills": { - "version": "0.79.6", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-native/normalize-colors": { - "version": "0.79.6", - "license": "MIT" + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@sinclair/typebox": { "version": "0.27.8", @@ -3121,6 +4195,30 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "license": "MIT", @@ -3166,6 +4264,12 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "license": "MIT" + }, "node_modules/@types/semver": { "version": "7.7.1", "dev": true, @@ -3379,6 +4483,207 @@ "dev": true, "license": "ISC" }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "license": "MIT", + "peer": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "license": "MIT", + "peer": true, + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "license": "MIT", @@ -3536,6 +4841,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-includes": { "version": "3.1.9", "dev": true, @@ -3657,6 +4971,42 @@ "version": "2.0.6", "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", + "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "dev": true, @@ -3793,7 +5143,6 @@ }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.14", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.27.7", @@ -3806,7 +5155,6 @@ }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { "version": "6.3.1", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3814,7 +5162,6 @@ }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.13.0", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", @@ -3826,7 +5173,6 @@ }, "node_modules/babel-plugin-polyfill-regenerator": { "version": "0.6.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" @@ -3842,6 +5188,16 @@ "hermes-parser": "0.25.1" } }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, "node_modules/babel-preset-current-node-syntax": { "version": "1.2.0", "license": "MIT", @@ -3922,7 +5278,6 @@ }, "node_modules/brace-expansion": { "version": "2.0.2", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4002,6 +5357,16 @@ "version": "1.1.2", "license": "MIT" }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "dev": true, @@ -4106,6 +5471,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "license": "MIT", + "peer": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "license": "MIT", @@ -4121,10 +5503,21 @@ } }, "node_modules/chardet": { - "version": "2.1.0", - "dev": true, + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "license": "MIT" }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/chrome-launcher": { "version": "0.15.2", "license": "Apache-2.0", @@ -4216,6 +5609,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "8.0.1", "license": "ISC", @@ -4293,6 +5695,18 @@ "node": ">=18" } }, + "node_modules/commenting": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commenting/-/commenting-1.1.0.tgz", + "integrity": "sha512-YeNK4tavZwtH7jEgK1ZINXzLKm6DZdEMfsaaieOsCAN0S8vsY7UeuO3Q7d/M018EFgE+IeUAuBOKkFccBZsUZA==", + "license": "MIT" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "license": "MIT" @@ -4442,7 +5856,6 @@ }, "node_modules/core-js-compat": { "version": "3.45.1", - "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.25.3" @@ -4490,7 +5903,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -4501,6 +5913,12 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "license": "MIT" + }, "node_modules/csstype": { "version": "3.1.3", "devOptional": true, @@ -4574,11 +5992,30 @@ "dev": true, "license": "MIT" }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "dev": true, @@ -4725,7 +6162,6 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", - "dev": true, "license": "MIT" }, "node_modules/ee-first": { @@ -4902,6 +6338,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT", + "peer": true + }, "node_modules/es-object-atoms": { "version": "1.1.1", "dev": true, @@ -4954,6 +6397,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "license": "MIT", @@ -5555,9 +7040,14 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -5604,6 +7094,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.2", "license": "Apache-2.0" @@ -5792,7 +7292,6 @@ }, "node_modules/foreground-child": { "version": "3.3.1", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -5842,7 +7341,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6154,7 +7652,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -6174,6 +7671,12 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.0", "license": "MIT", @@ -6238,7 +7741,6 @@ }, "node_modules/iconv-lite": { "version": "0.7.0", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -6452,7 +7954,6 @@ }, "node_modules/is-core-module": { "version": "2.16.1", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -6810,6 +8311,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "license": "MIT" + }, "node_modules/is-negative-zero": { "version": "2.0.3", "dev": true, @@ -6866,6 +8373,15 @@ "node": ">=8" } }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/is-regex": { "version": "1.2.1", "dev": true, @@ -7062,7 +8578,6 @@ }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -7093,6 +8608,47 @@ "semver": "bin/semver.js" } }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "dev": true, @@ -7111,7 +8667,6 @@ }, "node_modules/jackspeak": { "version": "3.4.3", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -7271,6 +8826,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -7488,12 +9052,10 @@ }, "node_modules/lodash": { "version": "4.17.21", - "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", - "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -7603,6 +9165,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "license": "MIT", + "peer": true + }, "node_modules/lru-cache": { "version": "5.1.1", "license": "ISC", @@ -7615,6 +9184,41 @@ "dev": true, "license": "MIT" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/makeerror": { "version": "1.0.12", "license": "BSD-3-Clause", @@ -7828,6 +9432,71 @@ "node": ">=18.18" } }, + "node_modules/metro-react-native-babel-preset": { + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.77.0.tgz", + "integrity": "sha512-HPPD+bTxADtoE4y/4t1txgTQ1LVR6imOBy7RMHUsqMVTbekoi8Ph5YI9vKX2VMPtVWeFt0w9YnCSLPa76GcXsA==", + "deprecated": "Use @react-native/babel-preset instead", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/plugin-proposal-async-generator-functions": "^7.0.0", + "@babel/plugin-proposal-class-properties": "^7.18.0", + "@babel/plugin-proposal-export-default-from": "^7.0.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.0", + "@babel/plugin-proposal-numeric-separator": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.20.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-default-from": "^7.0.0", + "@babel/plugin-syntax-flow": "^7.18.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0", + "@babel/plugin-syntax-optional-chaining": "^7.0.0", + "@babel/plugin-transform-arrow-functions": "^7.0.0", + "@babel/plugin-transform-async-to-generator": "^7.20.0", + "@babel/plugin-transform-block-scoping": "^7.0.0", + "@babel/plugin-transform-classes": "^7.0.0", + "@babel/plugin-transform-computed-properties": "^7.0.0", + "@babel/plugin-transform-destructuring": "^7.20.0", + "@babel/plugin-transform-flow-strip-types": "^7.20.0", + "@babel/plugin-transform-function-name": "^7.0.0", + "@babel/plugin-transform-literals": "^7.0.0", + "@babel/plugin-transform-modules-commonjs": "^7.0.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0", + "@babel/plugin-transform-parameters": "^7.0.0", + "@babel/plugin-transform-react-display-name": "^7.0.0", + "@babel/plugin-transform-react-jsx": "^7.0.0", + "@babel/plugin-transform-react-jsx-self": "^7.0.0", + "@babel/plugin-transform-react-jsx-source": "^7.0.0", + "@babel/plugin-transform-runtime": "^7.0.0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0", + "@babel/plugin-transform-spread": "^7.0.0", + "@babel/plugin-transform-sticky-regex": "^7.0.0", + "@babel/plugin-transform-typescript": "^7.5.0", + "@babel/plugin-transform-unicode-regex": "^7.0.0", + "@babel/template": "^7.0.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.4.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/metro-react-native-babel-preset/node_modules/react-refresh": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz", + "integrity": "sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/metro-resolver": { "version": "0.82.5", "license": "MIT", @@ -8019,7 +9688,6 @@ }, "node_modules/minimatch": { "version": "9.0.5", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -8049,6 +9717,15 @@ "node": ">=10" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/mri": { "version": "1.2.0", "dev": true, @@ -8061,10 +9738,38 @@ "version": "2.1.3", "license": "MIT" }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/mux-embed": { "version": "4.27.0", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "dev": true, @@ -8404,7 +10109,6 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/package-manager-detector": { @@ -8415,6 +10119,18 @@ "quansync": "^0.2.7" } }, + "node_modules/package-name-regex": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/package-name-regex/-/package-name-regex-2.0.6.tgz", + "integrity": "sha512-gFL35q7kbE/zBaPA3UKhp2vSzcPYx2ecbYuwv1ucE9Il6IIgBDweBlH8D68UFGZic2MkllKa2KHCfC1IQBQUYA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/dword-design" + } + }, "node_modules/parent-module": { "version": "1.0.1", "dev": true, @@ -8460,7 +10176,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8468,12 +10183,10 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -8488,12 +10201,10 @@ }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", - "dev": true, "license": "ISC" }, "node_modules/path-scurry/node_modules/minipass": { "version": "7.1.2", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -8507,6 +10218,23 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT", + "peer": true + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "license": "ISC" @@ -8616,6 +10344,35 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -8775,6 +10532,15 @@ ], "license": "MIT" }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "license": "MIT", @@ -9432,12 +11198,10 @@ }, "node_modules/regenerate": { "version": "1.4.2", - "dev": true, "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.2", - "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -9471,7 +11235,6 @@ }, "node_modules/regexpu-core": { "version": "6.3.1", - "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", @@ -9487,12 +11250,10 @@ }, "node_modules/regjsgen": { "version": "0.8.0", - "dev": true, "license": "MIT" }, "node_modules/regjsparser": { "version": "0.12.0", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.0.2" @@ -9503,7 +11264,6 @@ }, "node_modules/regjsparser/node_modules/jsesc": { "version": "3.0.2", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -9631,6 +11391,86 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-license": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-3.6.0.tgz", + "integrity": "sha512-1ieLxTCaigI5xokIfszVDRoy6c/Wmlot1fDEnea7Q/WXSR8AqOjYljHDLObAx7nFxHC2mbxT3QnTSPhaic2IYw==", + "license": "MIT", + "dependencies": { + "commenting": "~1.1.0", + "fdir": "^6.4.3", + "lodash": "~4.17.21", + "magic-string": "~0.30.0", + "moment": "~2.30.1", + "package-name-regex": "~2.0.6", + "spdx-expression-validate": "~2.0.0", + "spdx-satisfies": "~5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/rollup-plugin-license/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/run-parallel": { "version": "1.2.0", "dev": true, @@ -9673,7 +11513,6 @@ }, "node_modules/safe-buffer": { "version": "5.1.2", - "dev": true, "license": "MIT" }, "node_modules/safe-push-apply": { @@ -9709,7 +11548,6 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "dev": true, "license": "MIT" }, "node_modules/scheduler": { @@ -9783,6 +11621,15 @@ "node": ">=0.10.0" } }, + "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==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-static": { "version": "1.16.2", "license": "MIT", @@ -9852,7 +11699,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -9863,7 +11709,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9958,9 +11803,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "license": "ISC", + "peer": true + }, "node_modules/signal-exit": { "version": "4.1.0", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -10007,6 +11858,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "license": "MIT" + }, "node_modules/source-map": { "version": "0.5.7", "license": "BSD-3-Clause", @@ -10014,6 +11871,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "license": "MIT", @@ -10038,6 +11904,65 @@ "signal-exit": "^4.0.1" } }, + "node_modules/spdx-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", + "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", + "license": "MIT", + "dependencies": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-expression-validate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-validate/-/spdx-expression-validate-2.0.0.tgz", + "integrity": "sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg==", + "license": "(MIT AND CC-BY-3.0)", + "dependencies": { + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "license": "CC0-1.0" + }, + "node_modules/spdx-ranges": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", + "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", + "license": "(MIT AND CC-BY-3.0)" + }, + "node_modules/spdx-satisfies": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-5.0.1.tgz", + "integrity": "sha512-Nwor6W6gzFp8XX4neaKQ7ChV4wmpSh2sSDemMFSzHxpTw460jxFYeOn+jq4ybnSSw/5sc3pjka9MQPouksQNpw==", + "license": "MIT", + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "license": "BSD-3-Clause" @@ -10059,6 +11984,13 @@ "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "license": "MIT", + "peer": true + }, "node_modules/stackframe": { "version": "1.3.4", "license": "MIT" @@ -10087,6 +12019,12 @@ "node": ">= 0.6" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "dev": true, @@ -10146,7 +12084,6 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -10159,12 +12096,10 @@ }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10296,7 +12231,6 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -10335,6 +12269,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "license": "MIT", + "peer": true + }, "node_modules/supports-color": { "version": "7.2.0", "license": "MIT", @@ -10347,7 +12301,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10453,6 +12406,97 @@ "xtend": "~4.0.1" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "license": "MIT", + "peer": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "license": "MIT", + "peer": true + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "license": "BSD-3-Clause" @@ -10683,7 +12727,6 @@ }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -10691,7 +12734,6 @@ }, "node_modules/unicode-match-property-ecmascript": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", @@ -10703,7 +12745,6 @@ }, "node_modules/unicode-match-property-value-ecmascript": { "version": "2.2.1", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -10711,7 +12752,6 @@ }, "node_modules/unicode-property-aliases-ecmascript": { "version": "2.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -10788,6 +12828,234 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vite": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "license": "MIT", + "peer": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vlq": { "version": "1.0.1", "license": "MIT" @@ -10815,7 +13083,6 @@ }, "node_modules/which": { "version": "2.0.2", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -10908,6 +13175,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "license": "MIT", + "peer": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "dev": true, @@ -10935,7 +13219,6 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -10951,12 +13234,10 @@ }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10964,7 +13245,6 @@ }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11058,7 +13338,7 @@ }, "node_modules/yaml": { "version": "2.8.1", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -11124,6 +13404,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/youbora-adapter-theoplayer2": { "version": "6.8.10", "license": "MIT", From 58fab1379df58b332d34d0790ff57be3c9b1f48a Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 5 Dec 2025 14:10:59 +0100 Subject: [PATCH 02/70] Use alloy sdk for Web --- adobe-edge/src/api/AdobeConnector.ts | 20 +-- ...pter.ts => AdobeConnectorAdapterNative.ts} | 2 +- ...Adapter.ts => AdobeConnectorAdapterWeb.ts} | 11 +- .../src/internal/media-edge/MediaEdgeAPI.ts | 151 +++++++++--------- 4 files changed, 85 insertions(+), 99 deletions(-) rename adobe-edge/src/internal/{NativeAdobeConnectorAdapter.ts => AdobeConnectorAdapterNative.ts} (97%) rename adobe-edge/src/internal/{DefaultAdobeConnectorAdapter.ts => AdobeConnectorAdapterWeb.ts} (97%) diff --git a/adobe-edge/src/api/AdobeConnector.ts b/adobe-edge/src/api/AdobeConnector.ts index 9fbe2010..95b4c821 100644 --- a/adobe-edge/src/api/AdobeConnector.ts +++ b/adobe-edge/src/api/AdobeConnector.ts @@ -1,28 +1,20 @@ import type { THEOplayer } from 'react-native-theoplayer'; -import { NativeAdobeConnectorAdapter } from '../internal/NativeAdobeConnectorAdapter'; +import { AdobeConnectorAdapterNative } from '../internal/AdobeConnectorAdapterNative'; import type { AdobeCustomMetadataDetails } from './details/AdobeCustomMetadataDetails'; import type { AdobeErrorDetails } from './details/AdobeErrorDetails'; import { Platform } from 'react-native'; import { AdobeConnectorAdapter } from '../internal/AdobeConnectorAdapter'; -import { DefaultAdobeConnectorAdapter } from '../internal/DefaultAdobeConnectorAdapter'; +import { AdobeConnectorAdapterWeb } from '../internal/AdobeConnectorAdapterWeb'; export class AdobeConnector { private connectorAdapter: AdobeConnectorAdapter; - constructor( - player: THEOplayer, - baseUrl: string, - dataStreamId: string, - userAgent?: string, - useDebug?: boolean, - debugSessionId?: string, - useNative: boolean = false, - ) { + constructor(player: THEOplayer, edgeBasePath: string, datastreamId: string, orgId: string, debugEnabled?: boolean, debugSessionId?: string) { // By default, use a default typescript connector on all platforms, unless explicitly requested. - if (['ios', 'android'].includes(Platform.OS) && useNative) { - this.connectorAdapter = new NativeAdobeConnectorAdapter(player, baseUrl, dataStreamId, userAgent, useDebug, debugSessionId); + if (['ios', 'android'].includes(Platform.OS)) { + this.connectorAdapter = new AdobeConnectorAdapterNative(player, edgeBasePath, datastreamId, orgId, debugEnabled, debugSessionId); } else { - this.connectorAdapter = new DefaultAdobeConnectorAdapter(player, baseUrl, dataStreamId, userAgent, useDebug, debugSessionId); + this.connectorAdapter = new AdobeConnectorAdapterWeb(player, edgeBasePath, datastreamId, orgId, debugEnabled, debugSessionId); } } diff --git a/adobe-edge/src/internal/NativeAdobeConnectorAdapter.ts b/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts similarity index 97% rename from adobe-edge/src/internal/NativeAdobeConnectorAdapter.ts rename to adobe-edge/src/internal/AdobeConnectorAdapterNative.ts index 8161d7bd..d14b5893 100644 --- a/adobe-edge/src/internal/NativeAdobeConnectorAdapter.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts @@ -6,7 +6,7 @@ import { AdobeConnectorAdapter } from './AdobeConnectorAdapter'; const TAG = 'AdobeEdgeConnector'; const ERROR_MSG = 'AdobeConnectorAdapter Error'; -export class NativeAdobeConnectorAdapter implements AdobeConnectorAdapter { +export class AdobeConnectorAdapterNative implements AdobeConnectorAdapter { private readonly nativeHandle: NativeHandleType; constructor( diff --git a/adobe-edge/src/internal/DefaultAdobeConnectorAdapter.ts b/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts similarity index 97% rename from adobe-edge/src/internal/DefaultAdobeConnectorAdapter.ts rename to adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts index 6d5fdb1a..377ebf71 100644 --- a/adobe-edge/src/internal/DefaultAdobeConnectorAdapter.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts @@ -22,7 +22,7 @@ const TAG = 'AdobeConnector'; const CONTENT_PING_INTERVAL = 10000; const AD_PING_INTERVAL = 1000; -export class DefaultAdobeConnectorAdapter implements AdobeConnectorAdapter { +export class AdobeConnectorAdapterWeb implements AdobeConnectorAdapter { private player: THEOplayer; /** Timer handling the ping event request */ @@ -47,14 +47,14 @@ export class DefaultAdobeConnectorAdapter implements AdobeConnectorAdapter { constructor( player: THEOplayer, - baseUrl: string, - configId: string, - userAgent?: string, + edgeBasePath: string, + datastreamId: string, + orgId: string, debug = false, debugSessionId: string | undefined = undefined, ) { this.player = player; - this.mediaApi = new MediaEdgeAPI(baseUrl, configId, userAgent, debugSessionId); + this.mediaApi = new MediaEdgeAPI(edgeBasePath, datastreamId, orgId, debug, debugSessionId); this.debug = debug; this.addEventListeners(); this.logDebug('Initialized connector'); @@ -62,6 +62,7 @@ export class DefaultAdobeConnectorAdapter implements AdobeConnectorAdapter { setDebug(debug: boolean) { this.debug = debug; + this.mediaApi.setDebug(debug); } setDebugSessionId(id: string | undefined) { diff --git a/adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts b/adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts index 3caa4208..91fffb87 100644 --- a/adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts +++ b/adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts @@ -1,6 +1,4 @@ import type { paths } from './MediaEdge'; -import createClient, { type ClientMethod } from 'openapi-fetch'; -import type { MediaType } from 'openapi-typescript-helpers'; import type { AdobeAdvertisingDetails, AdobeAdvertisingPodDetails, @@ -14,37 +12,65 @@ import type { import { pathToEventTypeMap } from './PathToEventTypeMap'; import type { AdobePlayerStateData } from '../../api/details/AdobePlayerStateData'; import { sanitisePlayhead } from '../../utils/Utils'; -import { buildUserAgent } from '../../utils/UserAgent'; - -// eslint-disable-next-line @typescript-eslint/ban-types -type PostRequestType = ClientMethod; - -// eslint-disable-next-line @typescript-eslint/ban-types -interface MediaEdgeClient { - /** Call a POST endpoint */ - POST: PostRequestType; -} +import { createInstance } from '@adobe/alloy'; const TAG = 'AdobeEdge'; export class MediaEdgeAPI { - private readonly _client: MediaEdgeClient; - private readonly _configId: string; + // eslint-disable-next-line @typescript-eslint/ban-types + private readonly _alloyClient: Function; + private _debugSessionId: string | undefined; private _sessionId: string | undefined; private _hasSessionFailed: boolean; private _eventQueue: (() => Promise)[] = []; - constructor(baseUrl: string, configId: string, userAgent?: string, debugSessionId?: string) { - this._configId = configId; + constructor(edgeBasePath: string, datastreamId: string, orgId: string, debugEnabled?: boolean, debugSessionId?: string) { this._debugSessionId = debugSessionId; this._hasSessionFailed = false; - this._client = createClient({ - baseUrl, - headers: { 'User-Agent': userAgent || buildUserAgent() }, + this._alloyClient = createInstance({ name: 'alloy' }); + this._alloyClient('configure', { + /** + * The datastreamId property is a string that determines which datastream in Adobe Experience Platform you want + * to send data to. + * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/datastreamid + */ + datastreamId, + + /** + * The orgId property is a string that tells Adobe which organization that data is sent to. This property is + * required for all data sent using the Web SDK. + * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/orgid + */ + orgId, + + /** + * The edgeBasePath property alters the destination path when you interact with Adobe services. + * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/edgebasepath + */ + edgeBasePath, + + /** + * The debugEnabled property allows you to enable or disable debugging using Web SDK code. + * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/debugenabled + */ + debugEnabled: debugEnabled ?? false, + + /** + * + * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/streamingmedia + */ + streamingMedia: { + channel: 'Video channel', + playerName: 'THEOplayer', + }, }); } + setDebug(debug: boolean) { + this._alloyClient('setDebug', { enabled: debug }); + } + setDebugSessionId(id: string | undefined) { this._debugSessionId = id; } @@ -168,50 +194,25 @@ export class MediaEdgeAPI { return this.maybeQueueEvent('/adComplete', { playhead, qoeDataDetails }); } - private createClientParams() { - const params = { - query: { - configId: this._configId, - } as { configId: string; debugSessionID?: string }, - }; - if (this._debugSessionId) { - params.query.debugSessionID = this._debugSessionId; - } - return params; - } - + /** + * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/createmediasession + */ async startSession(sessionDetails: AdobeSessionDetails, customMetadata?: AdobeCustomMetadataDetails[], qoeDataDetails?: AdobeQoeDataDetails) { try { - const result = await this._client.POST('/sessionStart', { - params: this.createClientParams(), - body: { - events: [ - { - xdm: { - eventType: pathToEventTypeMap['/sessionStart'], - timestamp: new Date().toISOString(), - mediaCollection: { - playhead: 0, - sessionDetails, - qoeDataDetails, - customMetadata, - }, - }, - }, - ], + const result = await this._alloyClient('createMediaSession', { + xdm: { + eventType: pathToEventTypeMap['/sessionStart'], + timestamp: new Date().toISOString(), + mediaCollection: { + playhead: 0, + sessionDetails, + qoeDataDetails, + customMetadata, + }, }, }); - // @ts-ignore - const error = result.error || result.data.errors; - if (error) { - // noinspection ExceptionCaughtLocallyJS - throw Error(error); - } - // @ts-ignore - this._sessionId = result.data?.handle?.find((h: { type: string }) => { - return h.type === 'media-analytics:new-session'; - })?.payload?.[0]?.sessionId; + this._sessionId = result.sessionId; // empty queue if (this._sessionId && this._eventQueue.length !== 0) { @@ -247,30 +248,22 @@ export class MediaEdgeAPI { console.error(TAG, 'Invalid sessionID'); return; } + try { - const result = await this._client.POST(path, { - params: this.createClientParams(), - body: { - events: [ - { - xdm: { - eventType: pathToEventTypeMap[path], - timestamp: new Date().toISOString(), - mediaCollection: { - ...mediaDetails, - playhead: sanitisePlayhead(mediaDetails.playhead), - sessionID: this._sessionId, - }, - }, - }, - ], + const eventType = pathToEventTypeMap[path]; + /** + * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/sendmediaevent + */ + this._alloyClient('sendMediaEvent', { + xdm: { + eventType, + mediaCollection: { + ...mediaDetails, + playhead: sanitisePlayhead(mediaDetails.playhead), + sessionID: this._sessionId, + }, }, }); - // @ts-ignore - const error = result.error || result.data.errors; - if (error) { - console.error(TAG, `Failed to send event. ${JSON.stringify(error)}`); - } } catch (e) { console.error(TAG, `Failed to send event: ${JSON.stringify(e)}`); } From 78264dc16484f079e667ce7f6262910a1c11c448 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 5 Dec 2025 14:14:22 +0100 Subject: [PATCH 03/70] Update hook --- adobe-edge/src/api/hooks/useAdobe.ts | 9 ++++----- adobe-edge/src/api/hooks/useAdobeNative.ts | 14 -------------- adobe-edge/src/index.ts | 1 - 3 files changed, 4 insertions(+), 20 deletions(-) delete mode 100644 adobe-edge/src/api/hooks/useAdobeNative.ts diff --git a/adobe-edge/src/api/hooks/useAdobe.ts b/adobe-edge/src/api/hooks/useAdobe.ts index 225ba312..e9200ef4 100644 --- a/adobe-edge/src/api/hooks/useAdobe.ts +++ b/adobe-edge/src/api/hooks/useAdobe.ts @@ -3,12 +3,11 @@ import { RefObject, useEffect, useRef } from 'react'; import { AdobeConnector } from '../AdobeConnector'; export function useAdobe( - baseUrl: string, - dataStreamId: string, - userAgent?: string, + edgeBasePath: string, + datastreamId: string, + orgId: string, useDebug?: boolean, debugSessionId?: string, - useNative: boolean = true, ): [RefObject, (player: THEOplayer | undefined) => void] { const connector = useRef(undefined); const theoPlayer = useRef(undefined); @@ -19,7 +18,7 @@ export function useAdobe( theoPlayer.current = player; if (player) { - connector.current = new AdobeConnector(player, baseUrl, dataStreamId, userAgent, useDebug, debugSessionId, useNative); + connector.current = new AdobeConnector(player, edgeBasePath, datastreamId, orgId, useDebug, debugSessionId); player.addEventListener(PlayerEventType.DESTROY, onDestroy); } else { throw new Error('Invalid THEOplayer instance'); diff --git a/adobe-edge/src/api/hooks/useAdobeNative.ts b/adobe-edge/src/api/hooks/useAdobeNative.ts deleted file mode 100644 index 452ad3d1..00000000 --- a/adobe-edge/src/api/hooks/useAdobeNative.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { THEOplayer } from 'react-native-theoplayer'; -import { RefObject } from 'react'; -import { AdobeConnector } from '../AdobeConnector'; -import { useAdobe } from './useAdobe'; - -export function useAdobeNative( - baseUrl: string, - dataStreamId: string, - userAgent?: string, - useDebug?: boolean, - debugSessionId?: string, -): [RefObject, (player: THEOplayer | undefined) => void] { - return useAdobe(baseUrl, dataStreamId, userAgent, useDebug, debugSessionId, true); -} diff --git a/adobe-edge/src/index.ts b/adobe-edge/src/index.ts index aa36dcbb..712421ca 100644 --- a/adobe-edge/src/index.ts +++ b/adobe-edge/src/index.ts @@ -1,5 +1,4 @@ export { AdobeConnector } from './api/AdobeConnector'; export * from './api/details/barrel'; export { useAdobe } from './api/hooks/useAdobe'; -export { useAdobeNative } from './api/hooks/useAdobeNative'; export { sdkVersions } from './internal/version/Version'; From 599aaf80e1724296c942a370ff878556752a96c4 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 5 Dec 2025 17:32:59 +0100 Subject: [PATCH 04/70] Reuse alloy clients --- .../src/internal/media-edge/MediaEdgeAPI.ts | 101 +++++++++++------- 1 file changed, 63 insertions(+), 38 deletions(-) diff --git a/adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts b/adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts index 91fffb87..ae9ef820 100644 --- a/adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts +++ b/adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts @@ -16,55 +16,80 @@ import { createInstance } from '@adobe/alloy'; const TAG = 'AdobeEdge'; -export class MediaEdgeAPI { - // eslint-disable-next-line @typescript-eslint/ban-types - private readonly _alloyClient: Function; +// eslint-disable-next-line @typescript-eslint/ban-types +type AlloyClient = Function; + +/** + * Alloy globally stores clients by name. We are allowed create clients with the same config only once. + */ +interface ClientDescription { + datastreamId: string; + orgId: string; + client: AlloyClient; +} +const createdClients: ClientDescription[] = []; + +function findAlloyClient(datastreamId: string, orgId: string): AlloyClient | undefined { + return createdClients.find((client) => client.datastreamId === datastreamId && client.orgId === orgId)?.client; +} +export class MediaEdgeAPI { private _debugSessionId: string | undefined; private _sessionId: string | undefined; private _hasSessionFailed: boolean; private _eventQueue: (() => Promise)[] = []; + private readonly _alloyClient: AlloyClient; constructor(edgeBasePath: string, datastreamId: string, orgId: string, debugEnabled?: boolean, debugSessionId?: string) { this._debugSessionId = debugSessionId; this._hasSessionFailed = false; - this._alloyClient = createInstance({ name: 'alloy' }); - this._alloyClient('configure', { - /** - * The datastreamId property is a string that determines which datastream in Adobe Experience Platform you want - * to send data to. - * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/datastreamid - */ - datastreamId, - - /** - * The orgId property is a string that tells Adobe which organization that data is sent to. This property is - * required for all data sent using the Web SDK. - * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/orgid - */ - orgId, - /** - * The edgeBasePath property alters the destination path when you interact with Adobe services. - * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/edgebasepath - */ - edgeBasePath, - - /** - * The debugEnabled property allows you to enable or disable debugging using Web SDK code. - * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/debugenabled - */ - debugEnabled: debugEnabled ?? false, + this._alloyClient = findAlloyClient(datastreamId, orgId); + if (!this._alloyClient) { + this._alloyClient = createInstance({ + name: 'alloy', + monitors: [ + { + // Optionally configure callbacks. + // onInstanceConfigured: function (data: any) {}, + }, + ], + }); + this._alloyClient('configure', { + /** + * The datastreamId property is a string that determines which datastream in Adobe Experience Platform you want + * to send data to. + * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/datastreamid + */ + datastreamId, + + /** + * The orgId property is a string that tells Adobe which organization that data is sent to. This property is + * required for all data sent using the Web SDK. + * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/orgid + */ + orgId, + + /** + * The edgeBasePath property alters the destination path when you interact with Adobe services. + * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/edgebasepath + */ + edgeBasePath, + + /** + * + * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/streamingmedia + */ + streamingMedia: { + channel: 'Video channel', + playerName: 'THEOplayer', + }, + }); - /** - * - * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/streamingmedia - */ - streamingMedia: { - channel: 'Video channel', - playerName: 'THEOplayer', - }, - }); + // Store created client to prevent creating duplicates. + createdClients.push({ datastreamId, orgId, client: this._alloyClient }); + } + this.setDebug(debugEnabled); } setDebug(debug: boolean) { From 9810cb8fc7a1a6622721e92a25db632381755067 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Mon, 8 Dec 2025 12:56:09 +0100 Subject: [PATCH 05/70] Use adobe edge mobile sdk for android --- adobe-edge/android/build.gradle | 12 + .../android/src/main/AndroidManifest.xml | 5 +- .../adobe/edge/AdobeEdgeConnector.kt | 151 +++---- .../edge/ReactTHEOplayerAdobeEdgeModule.kt | 17 +- .../adobe/edge/api/MediaEdgeAPI.kt | 414 ++++++------------ .../internal/AdobeConnectorAdapterNative.ts | 8 +- 6 files changed, 224 insertions(+), 383 deletions(-) diff --git a/adobe-edge/android/build.gradle b/adobe-edge/android/build.gradle index 37d0e3d3..9247a85c 100644 --- a/adobe-edge/android/build.gradle +++ b/adobe-edge/android/build.gradle @@ -80,6 +80,18 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-process:2.5.1' implementation("com.google.code.gson:gson:$gsonVersion") + implementation(platform("com.adobe.marketing.mobile:sdk-bom:3.+")) + implementation("com.adobe.marketing.mobile:core") + implementation("com.adobe.marketing.mobile:edge") + + /** + * The Identity framework lets your app use Experience Cloud ID (ECID). Using ECIDs improves + * synchronization with Adobe and other customer identifiers. + * https://developer.adobe.com/client-sdks/home/base/mobile-core/identity/ + */ + implementation("com.adobe.marketing.mobile:edgeidentity") + implementation("com.adobe.marketing.mobile:edgemedia") + // THEOplayer dependencies compileOnly "com.theoplayer.theoplayer-sdk-android:core:$theoplayer_sdk_version" compileOnly "com.theoplayer.theoplayer-sdk-android:integration-ads-ima:$theoplayer_sdk_version" diff --git a/adobe-edge/android/src/main/AndroidManifest.xml b/adobe-edge/android/src/main/AndroidManifest.xml index cc947c56..99e3702d 100644 --- a/adobe-edge/android/src/main/AndroidManifest.xml +++ b/adobe-edge/android/src/main/AndroidManifest.xml @@ -1 +1,4 @@ - + + + + diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt index 20ad99bc..d93f4f0f 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt @@ -2,6 +2,12 @@ package com.theoplayer.reactnative.adobe.edge +import com.adobe.marketing.mobile.LoggingMode +import com.adobe.marketing.mobile.MobileCore +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.theoplayer.android.api.ads.Ad +import com.theoplayer.android.api.ads.LinearAd import com.theoplayer.android.api.event.EventListener import com.theoplayer.android.api.event.ads.AdBeginEvent import com.theoplayer.android.api.event.ads.AdBreakBeginEvent @@ -14,7 +20,10 @@ import com.theoplayer.android.api.event.player.ErrorEvent import com.theoplayer.android.api.event.player.PauseEvent import com.theoplayer.android.api.event.player.PlayerEventTypes import com.theoplayer.android.api.event.player.PlayingEvent +import com.theoplayer.android.api.event.player.SeekedEvent +import com.theoplayer.android.api.event.player.SeekingEvent import com.theoplayer.android.api.event.player.SourceChangeEvent +import com.theoplayer.android.api.event.player.TimeUpdateEvent import com.theoplayer.android.api.event.player.WaitingEvent import com.theoplayer.android.api.event.track.mediatrack.video.ActiveQualityChangedEvent import com.theoplayer.android.api.event.track.mediatrack.video.VideoTrackEventTypes @@ -26,6 +35,7 @@ import com.theoplayer.android.api.event.track.texttrack.list.TextTrackListEventT import com.theoplayer.android.api.player.Player import com.theoplayer.android.api.player.track.texttrack.TextTrackKind import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCue +import com.theoplayer.reactnative.adobe.edge.api.AdobeAdvertisingDetails import com.theoplayer.reactnative.adobe.edge.api.AdobeCustomMetadataDetails import com.theoplayer.reactnative.adobe.edge.api.AdobeErrorDetails import com.theoplayer.reactnative.adobe.edge.api.AdobeQoeDataDetails @@ -33,15 +43,10 @@ import com.theoplayer.reactnative.adobe.edge.api.AdobeSessionDetails import com.theoplayer.reactnative.adobe.edge.api.ContentType import com.theoplayer.reactnative.adobe.edge.api.ErrorSource import com.theoplayer.reactnative.adobe.edge.api.MediaEdgeAPI -import com.theoplayer.reactnative.adobe.edge.api.buildUserAgent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient typealias AddTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.AddTrackEvent typealias RemoveTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.RemoveTrackEvent @@ -49,20 +54,13 @@ typealias AddVideoTrackEvent = com.theoplayer.android.api.event.track.mediatrack typealias RemoveVideoTrackEvent = com.theoplayer.android.api.event.track.mediatrack.video.list.RemoveTrackEvent private const val TAG = "AdobeEdgeConnector" -private const val CONTENT_PING_INTERVAL = 10000L -private const val AD_PING_INTERVAL = 1000L private val JSON_MEDIA_TYPE = "application/json".toMediaType() class AdobeEdgeConnector( private val player: Player, - baseUrl: String, - configId: String, - userAgent: String?, debug: Boolean? = false, debugSessionId: String? = null ) { - private var pingJob: Job? = null - private var sessionInProgress = false private var adBreakPodIndex = 0 @@ -79,38 +77,32 @@ class AdobeEdgeConnector( private val scope = CoroutineScope(Dispatchers.Main) - private val client = OkHttpClient() - - private val onPlaying: EventListener = EventListener { handlePlaying() } - private val onPause: EventListener = EventListener { handlePause() } - private val onEnded: EventListener = EventListener { handleEnded() } - private val onWaiting: EventListener = EventListener { handleWaiting() } - private val onSourceChange: EventListener = - EventListener { handleSourceChange() } - private val onAddTextTrack: EventListener = - EventListener { handleAddTextTrack(it) } - private val onRemoveTextTrack: EventListener = - EventListener { handleRemoveTextTrack(it) } - private val onAddVideoTrack: EventListener = - EventListener { handleAddVideoTrack(it) } - private val onRemoveVideoTrack: EventListener = - EventListener { handleRemoveVideoTrack(it) } - private val onActiveVideoQualityChanged: EventListener = - EventListener { handleQualityChanged(it) } - private val onEnterCue: EventListener = EventListener { handleEnterCue(it) } - private val onExitCue: EventListener = EventListener { handleExitCue(it) } - private val onError: EventListener = EventListener { handleError(it) } - private val onAdBreakBegin: EventListener = + private val onPlaying = EventListener { handlePlaying() } + private val onPause = EventListener { handlePause() } + private val onEnded = EventListener { handleEnded() } + private val onTimeUpdate = EventListener { handleTimeUpdate(it) } + private val onWaiting = EventListener { handleWaiting() } + private val onSeeking = EventListener { handleSeeking() } + private val onSeeked = EventListener { handleSeeked() } + private val onSourceChange = EventListener { handleSourceChange() } + private val onAddTextTrack = EventListener { handleAddTextTrack(it) } + private val onRemoveTextTrack = EventListener { handleRemoveTextTrack(it) } + private val onAddVideoTrack = EventListener { handleAddVideoTrack(it) } + private val onRemoveVideoTrack = + EventListener { handleRemoveVideoTrack(it) } + private val onActiveVideoQualityChanged = + EventListener { handleQualityChanged(it) } + private val onEnterCue = EventListener { handleEnterCue(it) } + private val onExitCue = EventListener { handleExitCue(it) } + private val onError = EventListener { handleError(it) } + private val onAdBreakBegin = EventListener { event -> handleAdBreakBegin(event) } - private val onAdBreakEnd: EventListener = - EventListener { event -> handleAdBreakEnd() } - private val onAdBegin: EventListener = - EventListener { event -> handleAdBegin(event) } - private val onAdEnd: EventListener = EventListener { handleAdEnd(it) } - private val onAdSkip: EventListener = EventListener { event -> handleAdSkip() } + private val onAdBreakEnd = EventListener { event -> handleAdBreakEnd() } + private val onAdBegin = EventListener { event -> handleAdBegin(event) } + private val onAdEnd = EventListener { handleAdEnd(it) } + private val onAdSkip = EventListener { event -> handleAdSkip() } - private val mediaApi: MediaEdgeAPI = - MediaEdgeAPI(baseUrl, configId, userAgent ?: buildUserAgent(), debugSessionId) + private val mediaApi: MediaEdgeAPI = MediaEdgeAPI("N/A", debugSessionId) init { setDebug(debug ?: false) @@ -120,6 +112,7 @@ class AdobeEdgeConnector( fun setDebug(debug: Boolean) { Logger.debug = debug + MobileCore.setLogLevel(if (debug) LoggingMode.DEBUG else LoggingMode.ERROR) } fun setDebugSessionId(id: String?) { @@ -131,7 +124,7 @@ class AdobeEdgeConnector( } fun setError(errorDetails: AdobeErrorDetails) { - mediaApi.error(player.currentTime, errorDetails) + mediaApi.error(errorDetails) } fun stopAndStartNewSession(metadata: List?) { @@ -154,6 +147,9 @@ class AdobeEdgeConnector( player.addEventListener(PlayerEventTypes.PAUSE, onPause) player.addEventListener(PlayerEventTypes.ENDED, onEnded) player.addEventListener(PlayerEventTypes.WAITING, onWaiting) + player.addEventListener(PlayerEventTypes.SEEKING, onSeeking) + player.addEventListener(PlayerEventTypes.SEEKED, onSeeked) + player.addEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) player.addEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) player.textTracks.addEventListener(TextTrackListEventTypes.ADDTRACK, onAddTextTrack) player.textTracks.addEventListener(TextTrackListEventTypes.REMOVETRACK, onRemoveTextTrack) @@ -173,6 +169,9 @@ class AdobeEdgeConnector( player.removeEventListener(PlayerEventTypes.PAUSE, onPause) player.removeEventListener(PlayerEventTypes.ENDED, onEnded) player.removeEventListener(PlayerEventTypes.WAITING, onWaiting) + player.removeEventListener(PlayerEventTypes.SEEKING, onSeeking) + player.removeEventListener(PlayerEventTypes.SEEKED, onSeeked) + player.removeEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) player.removeEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) player.textTracks.removeEventListener(TextTrackListEventTypes.ADDTRACK, onAddTextTrack) player.textTracks.removeEventListener( @@ -196,23 +195,38 @@ class AdobeEdgeConnector( Logger.debug("onPlaying") scope.launch { maybeStartSession(player.duration) - mediaApi.play(player.currentTime) + mediaApi.play() } } private fun handlePause() { Logger.debug("onPause") - mediaApi.pause(player.currentTime) + mediaApi.pause() + } + + private fun handleTimeUpdate(event: TimeUpdateEvent) { + Logger.debug("onWaiting") + mediaApi.updateCurrentPlayhead(event.currentTime) } private fun handleWaiting() { Logger.debug("onWaiting") - mediaApi.bufferStart(player.currentTime) + mediaApi.bufferStart() + } + + private fun handleSeeking() { + Logger.debug("handleSeeking") + mediaApi.seekStart() + } + + private fun handleSeeked() { + Logger.debug("handleSeeked") + mediaApi.seekComplete() } private fun handleEnded() { Logger.debug("onEnded") - mediaApi.sessionComplete(player.currentTime) + mediaApi.sessionComplete() reset() } @@ -223,7 +237,7 @@ class AdobeEdgeConnector( private fun handleQualityChanged(event: ActiveQualityChangedEvent) { mediaApi.bitrateChange( - player.currentTime, AdobeQoeDataDetails( + AdobeQoeDataDetails( bitrate = event.quality?.bandwidth?.toInt() ?: 0, ) ) @@ -263,22 +277,22 @@ class AdobeEdgeConnector( Logger.debug("onEnterCue") val chapterCue = event.cue if (currentChapter != null && currentChapter?.endTime != chapterCue.startTime) { - mediaApi.chapterSkip(this.player.currentTime) + mediaApi.chapterSkip() } val chapterDetails = calculateChapterDetails(chapterCue) - mediaApi.chapterStart(this.player.currentTime, chapterDetails, customMetadata) + mediaApi.chapterStart(chapterDetails, customMetadata) currentChapter = chapterCue } private fun handleExitCue(event: ExitCueEvent) { Logger.debug("onExitCue") - mediaApi.chapterComplete(player.currentTime) + mediaApi.chapterComplete() } private fun handleError(event: ErrorEvent) { Logger.debug("onError") mediaApi.error( - player.currentTime, AdobeErrorDetails( + AdobeErrorDetails( name = event.errorObject.code.toString(), source = ErrorSource.PLAYER ) ) @@ -287,9 +301,8 @@ class AdobeEdgeConnector( private fun handleAdBreakBegin(event: AdBreakBeginEvent) { Logger.debug("onAdBreakBegin") isPlayingAd = true - startPinger(AD_PING_INTERVAL) val podDetails = calculateAdvertisingPodDetails(event.adBreak, adBreakPodIndex) - mediaApi.adBreakStart(player.currentTime, podDetails) + mediaApi.adBreakStart(podDetails) if (podDetails.index > adBreakPodIndex) { adBreakPodIndex++ } @@ -299,32 +312,29 @@ class AdobeEdgeConnector( Logger.debug("onAdBreakEnd") isPlayingAd = false adPodPosition = 1 - startPinger(CONTENT_PING_INTERVAL) - mediaApi.adBreakComplete(player.currentTime) + mediaApi.adBreakComplete() } private fun handleAdBegin(event: AdBeginEvent) { Logger.debug("onAdBegin") - mediaApi.adStart( - player.currentTime, calculateAdvertisingDetails(event.ad, adPodPosition), customMetadata - ) + mediaApi.adStart(calculateAdvertisingDetails(event.ad, adPodPosition), customMetadata) adPodPosition++ } private fun handleAdEnd(event: AdEndEvent) { Logger.debug("onAdEnd") - mediaApi.adComplete(player.currentTime) + mediaApi.adComplete() } private fun handleAdSkip() { Logger.debug("onAdSkip") - mediaApi.adSkip(player.currentTime) + mediaApi.adSkip() } private fun maybeEndSession() { Logger.debug("maybeEndSession") if (mediaApi.hasSessionStarted()) { - mediaApi.sessionEnd(player.currentTime) + mediaApi.sessionEnd() } reset() } @@ -382,22 +392,6 @@ class AdobeEdgeConnector( sessionInProgress = true Logger.debug("maybeStartSession - STARTED sessionId: ${mediaApi.sessionId}") - - if (!isPlayingAd) { - startPinger(CONTENT_PING_INTERVAL) - } else { - startPinger(AD_PING_INTERVAL) - } - } - - private fun startPinger(intervalMs: Long) { - pingJob?.cancel() - pingJob = scope.launch { - while (isActive) { - mediaApi.ping(player.currentTime) - delay(intervalMs) - } - } } private fun getContentLength(mediaLengthSec: Double?): Int { @@ -415,7 +409,6 @@ class AdobeEdgeConnector( adPodPosition = 1 isPlayingAd = false sessionInProgress = false - pingJob?.cancel() currentChapter = null } diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt index 38a9e7ed..a177ee40 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt @@ -1,6 +1,7 @@ - package com.theoplayer.reactnative.adobe.edge +import android.app.Application +import com.adobe.marketing.mobile.MobileCore import com.facebook.react.bridge.* import com.theoplayer.ReactTHEOplayerView import com.theoplayer.util.ViewResolver @@ -22,20 +23,22 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : @ReactMethod fun initialize( tag: Int, - baseUrl: String, - configId: String, - userAgent: String?, + edgeBasePath: String, + datastreamId: String, + orgId: String?, debug: Boolean?, debugSessionId: String? ) { + MobileCore.initialize( + reactApplicationContext.applicationContext as Application, + datastreamId + ) + viewResolver.resolveViewByTag(tag) { view: ReactTHEOplayerView? -> view?.playerContext?.playerView?.let { playerView -> adobeConnectors[tag] = AdobeEdgeConnector( player = playerView.player, - baseUrl = baseUrl, - configId = configId, - userAgent = userAgent, debug = debug, debugSessionId = debugSessionId ) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt index 33a368cf..cdc7234b 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt @@ -1,50 +1,34 @@ package com.theoplayer.reactnative.adobe.edge.api -import com.theoplayer.reactnative.adobe.edge.Logger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.Response -import java.io.IOException -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.TimeZone -import com.google.gson.Gson -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import org.json.JSONObject +import com.adobe.marketing.mobile.edge.media.Media +import com.adobe.marketing.mobile.edge.media.MediaConstants +import kotlin.Any +import kotlin.String -data class QueuedEvent( - val path: String, - val mediaDetails: Map -) +private const val MAIN_PING_INTERVAL = 10000L +private const val AD_PING_INTERVAL = 1000L class MediaEdgeAPI( - private val baseUrl: String, - private val configId: String, - private val userAgent: String, + channel: String, private var debugSessionId: String? = null ) { - private val client = OkHttpClient() - private val gson = Gson() + /** + * https://developer.adobe.com/client-sdks/edge/media-for-edge-network/api-reference/#createtrackerwithconfig + */ + private val tracker = Media.createTracker( + mutableMapOf( + MediaConstants.TrackerConfig.CHANNEL to channel, + MediaConstants.TrackerConfig.MAIN_PING_INTERVAL to MAIN_PING_INTERVAL, + MediaConstants.TrackerConfig.AD_PING_INTERVAL to AD_PING_INTERVAL, + ) + ) + var sessionId: String? = null private set var hasSessionFailed = false private set - private val eventQueue = mutableListOf() - - private val scope = CoroutineScope(Dispatchers.Main) - fun setDebugSessionId(id: String?) { debugSessionId = id } @@ -54,313 +38,159 @@ class MediaEdgeAPI( fun reset() { sessionId = null hasSessionFailed = false - eventQueue.clear() } - fun play(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent("/play", mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails)) + fun play() { + tracker.trackPlay() } - fun pause(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/pauseStart", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) + fun pause() { + tracker.trackPause() } - fun error( - playhead: Double?, - errorDetails: AdobeErrorDetails, - qoeDataDetails: AdobeQoeDataDetails? = null - ) { - maybeQueueEvent( - "/error", - mapOf( - "playhead" to sanitisePlayhead(playhead), - "qoeDataDetails" to qoeDataDetails, - "errorDetails" to errorDetails - ) - ) + fun error(errorDetails: AdobeErrorDetails) { + tracker.trackError(errorDetails.name) } - fun ping(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - scope.launch { - sessionId?.let { sessionId -> - postEvent( - sessionId, - "/ping", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) - } - } + fun bufferStart() { + tracker.trackEvent(Media.Event.BufferStart, null, null) } - fun bufferStart(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/bufferStart", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) + fun bufferComplete() { + tracker.trackEvent(Media.Event.BufferComplete, null, null) } - fun sessionComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/sessionComplete", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) + fun seekComplete() { + tracker.trackEvent(Media.Event.SeekStart, null, null) } - fun sessionEnd(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/sessionEnd", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) - sessionId = null + fun seekStart() { + tracker.trackEvent(Media.Event.SeekComplete, null, null) } - fun statesUpdate( - playhead: Double?, - statesStart: List? = null, - statesEnd: List? = null, - qoeDataDetails: AdobeQoeDataDetails? = null - ) { - maybeQueueEvent( - "/statesUpdate", - mapOf( - "playhead" to sanitisePlayhead(playhead), - "statesStart" to statesStart, - "statesEnd" to statesEnd, - "qoeDataDetails" to qoeDataDetails - ) - ) + /** + * Tracks the completion of the media playback session. Call this method only when the media has + * been completely viewed. If the viewing session is ended before the media is completely viewed, + * use trackSessionEnd instead. + */ + fun sessionComplete() { + tracker.trackComplete() } - fun bitrateChange(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails) { - maybeQueueEvent( - "/bitrateChange", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) + /** + * Tracks the end of a media playback session. Call this method when the viewing session ends, + * even if the user has not viewed the media to completion. If the media is viewed to completion, + * use trackComplete instead. + */ + fun sessionEnd() { + tracker.trackSessionEnd() + } + + /** + * Provides the current media playhead value to the MediaTracker instance. For accurate tracking, + * call this method every time the playhead value changes. If the player does not notify playhead + * value changes, call this method once every second with the most recent playhead value. + */ + fun updateCurrentPlayhead(playhead: Double?) { + tracker.updateCurrentPlayhead(sanitisePlayhead(playhead)) } - fun chapterSkip(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/chapterSkip", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) + fun statesUpdate() { + // TODO +// Media.createStateObject(MediaConstants.PlayerState.FULLSCREEN) + } + + fun bitrateChange(qoeDataDetails: AdobeQoeDataDetails) { + tracker.updateQoEObject( + Media.createQoEObject( + qoeDataDetails.bitrate ?: 0, + qoeDataDetails.timeToStart ?: 0, + qoeDataDetails.framesPerSecond ?: 0, + qoeDataDetails.droppedFrames ?: 0 + ) ) } + fun chapterSkip() { + tracker.trackEvent(Media.Event.ChapterSkip, null, null) + } + fun chapterStart( - playhead: Double?, chapterDetails: AdobeChapterDetails, - customMetadata: List? = null, - qoeDataDetails: AdobeQoeDataDetails? = null + customMetadata: List? = null ) { - maybeQueueEvent( - "/chapterStart", - mapOf( - "playhead" to sanitisePlayhead(playhead), - "chapterDetails" to chapterDetails, - "customMetadata" to customMetadata, - "qoeDataDetails" to qoeDataDetails - ) + tracker.trackEvent( + Media.Event.ChapterStart, + Media.createChapterObject( + chapterDetails.friendlyName ?: "", + chapterDetails.index, + chapterDetails.length, + chapterDetails.offset + ), + customMetadata?.associate { it.name to (it.value ?: "") } ) } - fun chapterComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/chapterComplete", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) + fun chapterComplete() { + tracker.trackEvent(Media.Event.ChapterComplete, null, null) } - fun adBreakStart( - playhead: Double, - advertisingPodDetails: AdobeAdvertisingPodDetails, - qoeDataDetails: AdobeQoeDataDetails? = null - ) { - maybeQueueEvent( - "/adBreakStart", - mapOf( - "playhead" to sanitisePlayhead(playhead), - "advertisingPodDetails" to advertisingPodDetails, - "qoeDataDetails" to qoeDataDetails - ) + fun adBreakStart(advertisingPodDetails: AdobeAdvertisingPodDetails) { + tracker.trackEvent( + Media.Event.AdBreakStart, + Media.createAdBreakObject( + advertisingPodDetails.friendlyName ?: "", + advertisingPodDetails.index, + advertisingPodDetails.offset + ), + null ) } - fun adBreakComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/adBreakComplete", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) + fun adBreakComplete() { + tracker.trackEvent(Media.Event.AdBreakComplete, null, null) } fun adStart( - playhead: Double, advertisingDetails: AdobeAdvertisingDetails, customMetadata: List? = null, - qoeDataDetails: AdobeQoeDataDetails? = null ) { - maybeQueueEvent( - "/adStart", - mapOf( - "playhead" to sanitisePlayhead(playhead), - "advertisingDetails" to advertisingDetails, - "customMetadata" to customMetadata, - "qoeDataDetails" to qoeDataDetails - ) + tracker.trackEvent( + Media.Event.AdStart, + Media.createAdObject( + advertisingDetails.name, + advertisingDetails.id ?: "", + advertisingDetails.podPosition, + advertisingDetails.length + ), + customMetadata?.associate { it.name to (it.value ?: "") } ) } - fun adSkip(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent("/adSkip", mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails)) + fun adSkip() { + tracker.trackEvent(Media.Event.AdSkip, null, null) } - fun adComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = null) { - maybeQueueEvent( - "/adComplete", - mapOf("playhead" to sanitisePlayhead(playhead), "qoeDataDetails" to qoeDataDetails) - ) + fun adComplete() { + tracker.trackEvent(Media.Event.AdComplete, null, null) } - private fun createUrlWithClientParams(baseUrl: String): HttpUrl { - return baseUrl.toHttpUrl().newBuilder().apply { - addQueryParameter("configId", configId) - debugSessionId?.let { addQueryParameter("debugSessionId", it) } - }.build() - } - - private suspend fun sendRequest( - url: String, - body: String - ): Response? = withContext(Dispatchers.IO) { - return@withContext try { - val request = Request.Builder() - .url(createUrlWithClientParams(url)) - .post(body.toRequestBody("application/json".toMediaType())) - .header("User-Agent", userAgent) - .build() - - val response = client.newCall(request).execute() - if (!response.isSuccessful) { - throw IOException("Unexpected code $response") - } else - response - } catch (e: Exception) { - throw e - } - } - - suspend fun startSession( + fun startSession( sessionDetails: AdobeSessionDetails, - customMetadata: List? = null, - qoeDataDetails: AdobeQoeDataDetails? = null + customMetadata: List? = null ) { - try { - val body = JsonObject().apply { - add("events", JsonArray().apply { - add(JsonObject().apply { - add("xdm", JsonObject().apply { - addProperty("eventType", EventType.SESSION_START.value) - addProperty("timestamp", Date().toISOString()) - add("mediaCollection", JsonObject().apply { - addProperty("playhead", 0) - add("sessionDetails", gson.toJsonTree(sessionDetails)) - qoeDataDetails?.let { - add("qoeDataDetails", gson.toJsonTree(qoeDataDetails)) - } - customMetadata?.let { - add("customMetadata", JsonArray().apply { - it.forEach { metadata -> - add(gson.toJsonTree(metadata)) - } - }) - } - }) - }) - }) - }) - } - - val response = sendRequest("$baseUrl/sessionStart", body.toString()) - - val responseBody = response?.body?.string() ?: throw IOException("Empty response body") - val jsonResponse = JSONObject(responseBody) - val error = jsonResponse.optJSONObject("error") ?: jsonResponse.optJSONObject("data") - ?.optJSONArray("errors") - if (error != null) { - throw Exception(error.toString()) - } - - val handle = jsonResponse.optJSONArray("handle") - sessionId = handle?.let { array -> - (0 until array.length()).firstNotNullOfOrNull { i -> - array.optJSONObject(i)?.takeIf { it.optString("type") == "media-analytics:new-session" } - ?.optJSONArray("payload")?.optJSONObject(0)?.optString("sessionId") + tracker.trackSessionStart( + Media.createMediaObject( + sessionDetails.friendlyName ?: "", + sessionDetails.assetID ?: "", + sessionDetails.length, + sessionDetails.contentType.name, + when (sessionDetails.streamType) { + StreamType.AUDIO ->Media.MediaType.Audio + else -> Media.MediaType.Video } - } - - if (eventQueue.isNotEmpty()) { - sessionId?.let { sessionId -> - eventQueue.forEach { event -> postEvent(sessionId, event.path, event.mediaDetails) } - } - eventQueue.clear() - } - } catch (e: Exception) { - Logger.error("Failed to start session. ${e.message}") - hasSessionFailed = true - } - } - - private fun maybeQueueEvent(path: String, mediaDetails: Map) { - if (hasSessionFailed) return - sessionId?.let { sessionId -> - scope.launch { - postEvent(sessionId, path, mediaDetails) - } - } ?: eventQueue.add(QueuedEvent(path, mediaDetails)) - } - - private suspend fun postEvent(sessionId: String, path: String, mediaDetails: Map) { - try { - val body = JsonObject().apply { - add("events", JsonArray().apply { - add(JsonObject().apply { - add("xdm", JsonObject().apply { - addProperty("eventType", pathToEventTypeMap[path]?.value) - addProperty("timestamp", Date().toISOString()) - add("mediaCollection", JsonObject().apply { - mediaDetails.forEach { (key, value) -> - add(key, gson.toJsonTree(value)) - } - addProperty("sessionID", sessionId) - }) - }) - }) - }) - }.toString() - - Logger.debug("postEvent - $path $body") - - val response = sendRequest("$baseUrl$path", body) - val responseBody = response?.body?.string() ?: throw IOException("Empty response body") - - // Optionally parse errors - if (responseBody.isNotEmpty()) { - val jsonResponse = JSONObject(responseBody) - val error = jsonResponse.optJSONObject("error") ?: jsonResponse.optJSONObject("data") - ?.optJSONArray("errors") - if (error != null) { - Logger.error("Failed to send event. $error") - } - } - } catch (e: Exception) { - Logger.error("Failed to send event. ${e.message}") - } + ), + customMetadata?.associate { it.name to (it.value ?: "") } + ) } } - -fun Date.toISOString(): String { - return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).apply { - timeZone = TimeZone.getTimeZone("UTC") - }.format(this) -} diff --git a/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts b/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts index d14b5893..cd4f99d4 100644 --- a/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts @@ -11,15 +11,15 @@ export class AdobeConnectorAdapterNative implements AdobeConnectorAdapter { constructor( player: THEOplayer, - baseUrl: string, - configId: string, - userAgent?: string, + edgeBasePath: string, + datastreamId: string, + orgId: string, debug = false, debugSessionId: string | undefined = undefined, ) { this.nativeHandle = player.nativeHandle || -1; try { - NativeModules.AdobeEdgeModule.initialize(this.nativeHandle, baseUrl, configId, userAgent, debug, debugSessionId); + NativeModules.AdobeEdgeModule.initialize(this.nativeHandle, edgeBasePath, datastreamId, orgId, debug, debugSessionId); } catch (error: unknown) { console.error(TAG, `${ERROR_MSG}: ${error}`); } From bef53a91e299b9d7cba0d017ff798a582b7edcac Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Wed, 10 Dec 2025 15:54:06 +0100 Subject: [PATCH 06/70] Use Mobile core --- .../adobe/edge/AdobeEdgeConnector.kt | 19 +- .../edge/ReactTHEOplayerAdobeEdgeModule.kt | 29 +-- .../adobe/edge/api/MediaEdgeAPI.kt | 8 +- adobe-edge/package.json | 2 +- adobe-edge/src/api/AdobeConnector.ts | 35 +-- adobe-edge/src/api/AdobeEdgeConfig.ts | 13 + adobe-edge/src/api/AdobeEdgeMobileConfig.ts | 5 + adobe-edge/src/api/AdobeEdgeWebConfig.ts | 228 ++++++++++++++++++ adobe-edge/src/api/hooks/useAdobe.ts | 3 +- .../src/internal/AdobeConnectorAdapter.ts | 2 - .../internal/AdobeConnectorAdapterNative.ts | 20 +- .../src/internal/AdobeConnectorAdapterWeb.ts | 35 +-- adobe-edge/src/internal/AdobeIdentityMap.ts | 8 - adobe-edge/src/internal/AdobeRequestBody.ts | 107 -------- adobe-edge/src/internal/AdobeResponseBody.ts | 12 - adobe-edge/src/internal/AdobeTimeSeries.ts | 26 -- .../{media-edge => web}/MediaEdge.d.ts | 0 .../{media-edge => web}/MediaEdgeAPI.ts | 103 ++++---- .../{media-edge => web}/PathToEventTypeMap.ts | 0 .../src/{utils => internal/web}/Utils.ts | 0 .../{media-edge => web}/media-edge-0.1.json | 0 adobe-edge/src/utils/UserAgent.ts | 32 --- adobe-edge/src/utils/UserAgent.web.ts | 3 - apps/e2e/android/app/build.gradle | 10 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- 25 files changed, 347 insertions(+), 355 deletions(-) create mode 100644 adobe-edge/src/api/AdobeEdgeConfig.ts create mode 100644 adobe-edge/src/api/AdobeEdgeMobileConfig.ts create mode 100644 adobe-edge/src/api/AdobeEdgeWebConfig.ts delete mode 100644 adobe-edge/src/internal/AdobeIdentityMap.ts delete mode 100644 adobe-edge/src/internal/AdobeRequestBody.ts delete mode 100644 adobe-edge/src/internal/AdobeResponseBody.ts delete mode 100644 adobe-edge/src/internal/AdobeTimeSeries.ts rename adobe-edge/src/internal/{media-edge => web}/MediaEdge.d.ts (100%) rename adobe-edge/src/internal/{media-edge => web}/MediaEdgeAPI.ts (77%) rename adobe-edge/src/internal/{media-edge => web}/PathToEventTypeMap.ts (100%) rename adobe-edge/src/{utils => internal/web}/Utils.ts (100%) rename adobe-edge/src/internal/{media-edge => web}/media-edge-0.1.json (100%) delete mode 100644 adobe-edge/src/utils/UserAgent.ts delete mode 100644 adobe-edge/src/utils/UserAgent.web.ts diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt index d93f4f0f..517a4953 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt @@ -4,10 +4,6 @@ package com.theoplayer.reactnative.adobe.edge import com.adobe.marketing.mobile.LoggingMode import com.adobe.marketing.mobile.MobileCore -import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.ReadableMap -import com.theoplayer.android.api.ads.Ad -import com.theoplayer.android.api.ads.LinearAd import com.theoplayer.android.api.event.EventListener import com.theoplayer.android.api.event.ads.AdBeginEvent import com.theoplayer.android.api.event.ads.AdBreakBeginEvent @@ -35,7 +31,6 @@ import com.theoplayer.android.api.event.track.texttrack.list.TextTrackListEventT import com.theoplayer.android.api.player.Player import com.theoplayer.android.api.player.track.texttrack.TextTrackKind import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCue -import com.theoplayer.reactnative.adobe.edge.api.AdobeAdvertisingDetails import com.theoplayer.reactnative.adobe.edge.api.AdobeCustomMetadataDetails import com.theoplayer.reactnative.adobe.edge.api.AdobeErrorDetails import com.theoplayer.reactnative.adobe.edge.api.AdobeQoeDataDetails @@ -56,11 +51,7 @@ typealias RemoveVideoTrackEvent = com.theoplayer.android.api.event.track.mediatr private const val TAG = "AdobeEdgeConnector" private val JSON_MEDIA_TYPE = "application/json".toMediaType() -class AdobeEdgeConnector( - private val player: Player, - debug: Boolean? = false, - debugSessionId: String? = null -) { +class AdobeEdgeConnector(private val player: Player, debug: Boolean? = false) { private var sessionInProgress = false private var adBreakPodIndex = 0 @@ -102,7 +93,7 @@ class AdobeEdgeConnector( private val onAdEnd = EventListener { handleAdEnd(it) } private val onAdSkip = EventListener { event -> handleAdSkip() } - private val mediaApi: MediaEdgeAPI = MediaEdgeAPI("N/A", debugSessionId) + private val mediaApi: MediaEdgeAPI = MediaEdgeAPI("N/A") init { setDebug(debug ?: false) @@ -115,10 +106,6 @@ class AdobeEdgeConnector( MobileCore.setLogLevel(if (debug) LoggingMode.DEBUG else LoggingMode.ERROR) } - fun setDebugSessionId(id: String?) { - mediaApi.setDebugSessionId(id) - } - fun updateMetadata(metadata: List) { customMetadata.addAll(metadata) } @@ -349,7 +336,7 @@ class AdobeEdgeConnector( * @param mediaLengthSec * @private */ - private suspend fun maybeStartSession(mediaLengthSec: Double? = null) { + private fun maybeStartSession(mediaLengthSec: Double? = null) { val mediaLength = getContentLength(mediaLengthSec) val hasValidSource = player.source !== null val hasValidDuration = isValidDuration(mediaLengthSec) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt index a177ee40..3aba1bf8 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt @@ -8,6 +8,9 @@ import com.theoplayer.util.ViewResolver private const val TAG = "AdobeEdgeModule" +private const val PROP_APP_ID = "appId" +private const val PROP_DEBUG_ENABLED = "debugEnabled" + @Suppress("unused") class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { @@ -23,25 +26,19 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : @ReactMethod fun initialize( tag: Int, - edgeBasePath: String, - datastreamId: String, - orgId: String?, - debug: Boolean?, - debugSessionId: String? + config: ReadableMap, ) { MobileCore.initialize( reactApplicationContext.applicationContext as Application, - datastreamId + config.getString(PROP_APP_ID) ?: "N/A" ) viewResolver.resolveViewByTag(tag) { view: ReactTHEOplayerView? -> view?.playerContext?.playerView?.let { playerView -> - adobeConnectors[tag] = - AdobeEdgeConnector( - player = playerView.player, - debug = debug, - debugSessionId = debugSessionId - ) + adobeConnectors[tag] = AdobeEdgeConnector( + player = playerView.player, + debug = if (config.hasKey(PROP_DEBUG_ENABLED)) config.getBoolean(PROP_DEBUG_ENABLED) else false, + ) } } } @@ -56,14 +53,6 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : adobeConnectors[tag]?.setDebug(debug) } - /** - * Set a debugSessionID query parameter that is added to all outgoing requests. - */ - @ReactMethod - fun setDebugSessionId(tag: Int, id: String?) { - adobeConnectors[tag]?.setDebugSessionId(id) - } - /** * Sets customMetadataDetails which will be passed for the session start request. */ diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt index cdc7234b..2add1822 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt @@ -8,10 +8,7 @@ import kotlin.String private const val MAIN_PING_INTERVAL = 10000L private const val AD_PING_INTERVAL = 1000L -class MediaEdgeAPI( - channel: String, - private var debugSessionId: String? = null -) { +class MediaEdgeAPI(channel: String) { /** * https://developer.adobe.com/client-sdks/edge/media-for-edge-network/api-reference/#createtrackerwithconfig */ @@ -29,9 +26,6 @@ class MediaEdgeAPI( var hasSessionFailed = false private set - fun setDebugSessionId(id: String?) { - debugSessionId = id - } fun hasSessionStarted(): Boolean = sessionId != null diff --git a/adobe-edge/package.json b/adobe-edge/package.json index 41a87142..133aaf44 100644 --- a/adobe-edge/package.json +++ b/adobe-edge/package.json @@ -34,7 +34,7 @@ "manifest": "node manifest.js > src/manifest.json", "prepare": "npm run manifest && npm run build", "clean": "rimraf lib android/build example/android/build example/android/app/build example/ios/build", - "generate-types": "openapi-typescript ./src/internal/media-edge/media-edge-0.1.json -o ./src/internal/media-edge/MediaEdge.d.ts" + "generate-types": "openapi-typescript src/internal/web/media-edge-0.1.json -o src/internal/web/MediaEdge.d.ts" }, "keywords": [ "react-native", diff --git a/adobe-edge/src/api/AdobeConnector.ts b/adobe-edge/src/api/AdobeConnector.ts index 95b4c821..954b1275 100644 --- a/adobe-edge/src/api/AdobeConnector.ts +++ b/adobe-edge/src/api/AdobeConnector.ts @@ -5,16 +5,24 @@ import type { AdobeErrorDetails } from './details/AdobeErrorDetails'; import { Platform } from 'react-native'; import { AdobeConnectorAdapter } from '../internal/AdobeConnectorAdapter'; import { AdobeConnectorAdapterWeb } from '../internal/AdobeConnectorAdapterWeb'; +import { AdobeEdgeConfig } from './AdobeEdgeConfig'; export class AdobeConnector { - private connectorAdapter: AdobeConnectorAdapter; + private connectorAdapter?: AdobeConnectorAdapter; - constructor(player: THEOplayer, edgeBasePath: string, datastreamId: string, orgId: string, debugEnabled?: boolean, debugSessionId?: string) { - // By default, use a default typescript connector on all platforms, unless explicitly requested. + constructor(player: THEOplayer, config: AdobeEdgeConfig) { if (['ios', 'android'].includes(Platform.OS)) { - this.connectorAdapter = new AdobeConnectorAdapterNative(player, edgeBasePath, datastreamId, orgId, debugEnabled, debugSessionId); + if (config.mobileConfig) { + this.connectorAdapter = new AdobeConnectorAdapterNative(player, config.mobileConfig); + } else { + console.error('AdobeConnector Error: Missing mobileConfig for mobile platform'); + } } else { - this.connectorAdapter = new AdobeConnectorAdapterWeb(player, edgeBasePath, datastreamId, orgId, debugEnabled, debugSessionId); + if (config.webConfig) { + this.connectorAdapter = new AdobeConnectorAdapterWeb(player, config.webConfig); + } else { + console.error('AdobeConnector Error: Missing webConfig for Web platform'); + } } } @@ -22,14 +30,14 @@ export class AdobeConnector { * Sets customMetadataDetails which will be passed for the session start request. */ updateMetadata(customMetadataDetails: AdobeCustomMetadataDetails[]): void { - this.connectorAdapter.updateMetadata(customMetadataDetails); + this.connectorAdapter?.updateMetadata(customMetadataDetails); } /** * Dispatch error event to adobe */ setError(errorDetails: AdobeErrorDetails): void { - this.connectorAdapter.setError(errorDetails); + this.connectorAdapter?.setError(errorDetails); } /** @@ -38,14 +46,7 @@ export class AdobeConnector { * @param debug whether to write debug info or not. */ setDebug(debug: boolean) { - this.connectorAdapter.setDebug(debug); - } - - /** - * Set a debugSessionID query parameter that is added to all outgoing requests. - */ - setDebugSessionId(id: string | undefined) { - this.connectorAdapter.setDebugSessionId(id); + this.connectorAdapter?.setDebug(debug); } /** @@ -57,13 +58,13 @@ export class AdobeConnector { * @param customMetadataDetails media details information. */ stopAndStartNewSession(customMetadataDetails: AdobeCustomMetadataDetails[]): void { - void this.connectorAdapter.stopAndStartNewSession(customMetadataDetails); + void this.connectorAdapter?.stopAndStartNewSession(customMetadataDetails); } /** * Stops video and ad analytics and closes all sessions. */ destroy(): void { - void this.connectorAdapter.destroy(); + void this.connectorAdapter?.destroy(); } } diff --git a/adobe-edge/src/api/AdobeEdgeConfig.ts b/adobe-edge/src/api/AdobeEdgeConfig.ts new file mode 100644 index 00000000..11c9f237 --- /dev/null +++ b/adobe-edge/src/api/AdobeEdgeConfig.ts @@ -0,0 +1,13 @@ +import { AdobeEdgeMobileConfig } from './AdobeEdgeMobileConfig'; +import { AdobeEdgeWebConfig } from './AdobeEdgeWebConfig'; + +export interface AdobeEdgeConfig { + /** + */ + mobileConfig?: AdobeEdgeMobileConfig; + + /** + * + */ + webConfig?: AdobeEdgeWebConfig; +} diff --git a/adobe-edge/src/api/AdobeEdgeMobileConfig.ts b/adobe-edge/src/api/AdobeEdgeMobileConfig.ts new file mode 100644 index 00000000..fdd0205f --- /dev/null +++ b/adobe-edge/src/api/AdobeEdgeMobileConfig.ts @@ -0,0 +1,5 @@ +export interface AdobeEdgeMobileConfig { + appId?: string; + + debugEnabled?: boolean; +} diff --git a/adobe-edge/src/api/AdobeEdgeWebConfig.ts b/adobe-edge/src/api/AdobeEdgeWebConfig.ts new file mode 100644 index 00000000..ccae1df2 --- /dev/null +++ b/adobe-edge/src/api/AdobeEdgeWebConfig.ts @@ -0,0 +1,228 @@ +export type AutoCollect = 'always' | 'never' | 'decoratedElementsOnly'; + +export type Context = 'web' | 'device' | 'placeContext' | 'timestamp' | 'highEntropyUserAgentHints'; + +export type Consent = 'in' | 'out' | 'pending'; + +export interface AdobeEdgeWebConfig { + /** + * The datastreamId property is a string that determines which datastream in Adobe Experience Platform you want + * to send data to. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/datastreamid} + */ + datastreamId: string; + + /** + * The orgId property is a string that tells Adobe which organization that data is sent to. This property is + * required for all data sent using the SDK. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/orgid} + */ + orgId: string; + + /** + * The edgeBasePath property alters the destination path when you interact with Adobe services. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/edgebasepath} + */ + edgeBasePath: string; + + /** + * The autoCollectPropositionInteractions property is an optional setting that determines if the SDK automatically + * collects proposition interactions. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/autocollectpropositioninteractions} + */ + autoCollectPropositionInteractions?: { + /** + * Adobe Journey Optimizer. + */ + AJO?: AutoCollect; + /** + * Adobe Target. + */ + TGT?: AutoCollect; + }; + + /** + * The clickCollection object contains several variables that help you control automatically collected link data. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/clickcollection} + */ + clickCollection?: { + /** + * Determines if links within the current domain are automatically tracked. + */ + internalLinkEnabled?: boolean; + + /** + * Determines if the library tracks links that qualify as downloads based on the downloadLinkQualifier property. + */ + downloadLinkEnabled?: boolean; + + /** + * Determines if links to external domains are automatically tracked. + */ + externalLinkEnabled?: boolean; + + /** + * Determines if the library waits until the next “page view” event to send link tracking data. + */ + eventGroupingEnabled?: boolean; + + /** + * Determines if link tracking data is stored in session storage instead of local variables. + */ + sessionStorageEnabled?: boolean; + + /** + * A callback function that provides full controls over link tracking data that you collect. + */ + filterClickDetails?: (content: object) => void; + }; + + /** + * The clickCollectionEnabled property is a boolean that determines if the SDK automatically collects link data. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/clickcollectionenabled} + * + * @default `true`. + */ + clickCollectionEnabled?: boolean; + + /** + * The context property is an array of strings that determines what the SDK can automatically collect. + */ + context?: Context[]; + + /** + * The debugEnabled property allows you to enable or disable debugging using SDK code. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/debugenabled} + * + * @default `false`. + */ + debugEnabled?: boolean; + + /** + * The defaultConsent property determines how you handle data collection consent before you call the setConsent + * command. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/defaultconsent} + * + * @default `in`. + */ + defaultConsent?: Consent; + + /** + * If you enable automatic link tracking using {@link clickCollectionEnabled}, the downloadLinkQualifier property + * helps determine the criteria for a URL to be considered a download link. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/downloadlinkqualifier} + * + * @default `'\.(exe|zip|wav|mp3|mov|mpg|avi|wmv|pdf|doc|docx|xls|xlsx|ppt|pptx)$'`. + */ + downloadLinkQualifier?: string; + + /** + * The edgeConfigOverrides object allows you to override configuration settings for commands run on the current page. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/edgeconfigoverrides} + */ + edgeConfigOverrides?: object; + + /** + * The edgeDomain property allows you to change the domain where the SDK sends data. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/edgedomain} + * + * @default `'edge.adobedc.net'`. + */ + edgeDomain?: string; + + /** + * The idMigrationEnabled property allows the SDK to read AMCV cookies set by previous Adobe Experience Cloud + * implementations. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/idmigrationenabled} + * + * @default `true`. + */ + idMigrationEnabled?: boolean; + + /** + * The onBeforeEventSend callback allows you to register a JavaScript function that can alter the data you send just + * before that data is sent to Adobe. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/onbeforeeventsend} + */ + onBeforeEventSend?: (content: object) => void; + + /** + * The prehidingStyle property allows you to define a CSS selector to hide personalized content until it loads. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/prehidingstyle} + */ + prehidingStyle?: string; + + /** + * The pushNotifications property lets you configure push notifications for web applications. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/pushnotifications} + */ + pushNotifications?: { + /** + * The VAPID public key used for push subscriptions. Must be a Base64-encoded string. + */ + vapidPublicKey: string; + + /** + * The application ID associated with the VAPID public key. + */ + applicationId: string; + + /** + * The system dataset ID used for push notification tracking. + */ + trackingDatasetId: string; + }; + + /** + * The streamingMedia component helps you collect data related to media sessions on your website. + *{@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/streamingmedia} + */ + streamingMedia?: { + /** + * The name of the channel where streaming media collection occurs. + */ + channel?: string; + + /** + * The name of the media player. + */ + playerName?: string; + + /** + * The version of the media player application. + */ + appVersion?: string; + + /** + * Frequency of pings for main content, in seconds. + * + * @default `10`. + */ + mainPingInterval?: number; + + /** + * Frequency of pings for ad content, in seconds. + * + * @default `10`. + */ + adPingInterval?: number; + }; + + /** + * The targetMigrationEnabled property is a boolean that allows the SDK to read and write the mbox and + * mboxEdgeCluster cookies that the Adobe Target 1.x and 2.x libraries use. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/targetmigrationenabled} + * + * @default `false`. + */ + targetMigrationEnabled?: boolean; + + /** + * The thirdPartyCookiesEnabled property is a boolean that determines if the SDK sets cookies in a third-party + * context. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/thirdpartycookiesenabled} + * + * @default `true`. + */ + thirdPartyCookiesEnabled?: boolean; +} diff --git a/adobe-edge/src/api/hooks/useAdobe.ts b/adobe-edge/src/api/hooks/useAdobe.ts index e9200ef4..dede303f 100644 --- a/adobe-edge/src/api/hooks/useAdobe.ts +++ b/adobe-edge/src/api/hooks/useAdobe.ts @@ -7,7 +7,6 @@ export function useAdobe( datastreamId: string, orgId: string, useDebug?: boolean, - debugSessionId?: string, ): [RefObject, (player: THEOplayer | undefined) => void] { const connector = useRef(undefined); const theoPlayer = useRef(undefined); @@ -18,7 +17,7 @@ export function useAdobe( theoPlayer.current = player; if (player) { - connector.current = new AdobeConnector(player, edgeBasePath, datastreamId, orgId, useDebug, debugSessionId); + connector.current = new AdobeConnector(player, edgeBasePath, datastreamId, orgId, useDebug); player.addEventListener(PlayerEventType.DESTROY, onDestroy); } else { throw new Error('Invalid THEOplayer instance'); diff --git a/adobe-edge/src/internal/AdobeConnectorAdapter.ts b/adobe-edge/src/internal/AdobeConnectorAdapter.ts index 20239b26..98161e9b 100644 --- a/adobe-edge/src/internal/AdobeConnectorAdapter.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapter.ts @@ -7,8 +7,6 @@ export interface AdobeConnectorAdapter { setError(metadata: AdobeErrorDetails): void; - setDebugSessionId(id: string | undefined): void; - stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]): Promise; destroy(): Promise; diff --git a/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts b/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts index cd4f99d4..ccaf5b3b 100644 --- a/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts @@ -2,6 +2,7 @@ import type { NativeHandleType, THEOplayer } from 'react-native-theoplayer'; import { NativeModules } from 'react-native'; import type { AdobeCustomMetadataDetails, AdobeErrorDetails } from '@theoplayer/react-native-analytics-adobe-edge'; import { AdobeConnectorAdapter } from './AdobeConnectorAdapter'; +import { AdobeEdgeMobileConfig } from '../api/AdobeEdgeMobileConfig'; const TAG = 'AdobeEdgeConnector'; const ERROR_MSG = 'AdobeConnectorAdapter Error'; @@ -9,17 +10,10 @@ const ERROR_MSG = 'AdobeConnectorAdapter Error'; export class AdobeConnectorAdapterNative implements AdobeConnectorAdapter { private readonly nativeHandle: NativeHandleType; - constructor( - player: THEOplayer, - edgeBasePath: string, - datastreamId: string, - orgId: string, - debug = false, - debugSessionId: string | undefined = undefined, - ) { + constructor(player: THEOplayer, config: AdobeEdgeMobileConfig) { this.nativeHandle = player.nativeHandle || -1; try { - NativeModules.AdobeEdgeModule.initialize(this.nativeHandle, edgeBasePath, datastreamId, orgId, debug, debugSessionId); + NativeModules.AdobeEdgeModule.initialize(this.nativeHandle, config); } catch (error: unknown) { console.error(TAG, `${ERROR_MSG}: ${error}`); } @@ -49,14 +43,6 @@ export class AdobeConnectorAdapterNative implements AdobeConnectorAdapter { } } - setDebugSessionId(id: string | undefined) { - try { - NativeModules.AdobeEdgeModule.setDebugSessionId(this.nativeHandle || -1, id); - } catch (error: unknown) { - console.error(TAG, `${ERROR_MSG}: ${error}`); - } - } - async stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]) { try { NativeModules.AdobeEdgeModule.stopAndStartNewSession(this.nativeHandle || -1, metadata ?? []); diff --git a/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts b/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts index 377ebf71..1b392931 100644 --- a/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts @@ -10,13 +10,13 @@ import type { THEOplayer, } from 'react-native-theoplayer'; import { AdEventType, MediaTrackEventType, PlayerEventType, TextTrackEventType } from 'react-native-theoplayer'; -import { calculateAdvertisingPodDetails, calculateAdvertisingDetails, calculateChapterDetails, sanitiseContentLength } from '../utils/Utils'; -import { Platform } from 'react-native'; -import { MediaEdgeAPI } from './media-edge/MediaEdgeAPI'; +import { calculateAdvertisingPodDetails, calculateAdvertisingDetails, calculateChapterDetails, sanitiseContentLength } from './web/Utils'; +import { MediaEdgeAPI } from './web/MediaEdgeAPI'; import type { AdobeCustomMetadataDetails, AdobeErrorDetails } from '@theoplayer/react-native-analytics-adobe-edge'; import { ContentType } from '../api/details/AdobeSessionDetails'; import { ErrorSource } from '../api/details/AdobeErrorDetails'; import { AdobeConnectorAdapter } from './AdobeConnectorAdapter'; +import { AdobeEdgeWebConfig } from '../api/AdobeEdgeWebConfig'; const TAG = 'AdobeConnector'; const CONTENT_PING_INTERVAL = 10000; @@ -45,17 +45,10 @@ export class AdobeConnectorAdapterWeb implements AdobeConnectorAdapter { private mediaApi: MediaEdgeAPI; - constructor( - player: THEOplayer, - edgeBasePath: string, - datastreamId: string, - orgId: string, - debug = false, - debugSessionId: string | undefined = undefined, - ) { + constructor(player: THEOplayer, config: AdobeEdgeWebConfig) { this.player = player; - this.mediaApi = new MediaEdgeAPI(edgeBasePath, datastreamId, orgId, debug, debugSessionId); - this.debug = debug; + this.mediaApi = new MediaEdgeAPI(config); + this.debug = config.debugEnabled || false; this.addEventListeners(); this.logDebug('Initialized connector'); } @@ -65,10 +58,6 @@ export class AdobeConnectorAdapterWeb implements AdobeConnectorAdapter { this.mediaApi.setDebug(debug); } - setDebugSessionId(id: string | undefined) { - this.mediaApi.setDebugSessionId(id); - } - updateMetadata(metadata: AdobeCustomMetadataDetails[]): void { this.customMetadata = [...this.customMetadata, ...metadata]; } @@ -101,12 +90,8 @@ export class AdobeConnectorAdapterWeb implements AdobeConnectorAdapter { this.player.addEventListener(PlayerEventType.MEDIA_TRACK, this.onMediaTrackEvent); this.player.addEventListener(PlayerEventType.LOADED_METADATA, this.onLoadedMetadata); this.player.addEventListener(PlayerEventType.ERROR, this.onError); - this.player.addEventListener(PlayerEventType.AD_EVENT, this.onAdEvent); - - if (Platform.OS === 'web') { - window.addEventListener('beforeunload', this.onBeforeUnload); - } + window.addEventListener('beforeunload', this.onBeforeUnload); } private removeEventListeners(): void { @@ -119,12 +104,8 @@ export class AdobeConnectorAdapterWeb implements AdobeConnectorAdapter { this.player.removeEventListener(PlayerEventType.MEDIA_TRACK, this.onMediaTrackEvent); this.player.removeEventListener(PlayerEventType.LOADED_METADATA, this.onLoadedMetadata); this.player.removeEventListener(PlayerEventType.ERROR, this.onError); - this.player.removeEventListener(PlayerEventType.AD_EVENT, this.onAdEvent); - - if (Platform.OS === 'web') { - window.removeEventListener('beforeunload', this.onBeforeUnload); - } + window.removeEventListener('beforeunload', this.onBeforeUnload); } private onLoadedMetadata = (e: LoadedMetadataEvent) => { diff --git a/adobe-edge/src/internal/AdobeIdentityMap.ts b/adobe-edge/src/internal/AdobeIdentityMap.ts deleted file mode 100644 index 625cde92..00000000 --- a/adobe-edge/src/internal/AdobeIdentityMap.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Defines a map containing a set of end user identities, keyed on either namespace integration code or the namespace - * ID of the identity. The values of the map are an array, meaning that more than one identity of each namespace may - * be carried. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/mixins/shared/identitymap.schema.md - */ -export type AdobeIdentityMap = { [key: string]: object[] }; diff --git a/adobe-edge/src/internal/AdobeRequestBody.ts b/adobe-edge/src/internal/AdobeRequestBody.ts deleted file mode 100644 index 2cb1336f..00000000 --- a/adobe-edge/src/internal/AdobeRequestBody.ts +++ /dev/null @@ -1,107 +0,0 @@ -export interface AdobeRequestBody { - events?: { - xdm: { - mediaCollection: { - playhead: number; - sessionDetails: { - adLoad?: string; - /** @description The SDK version used by the player. This could have any custom value that makes sense for your player */ - appVersion?: string; - artist?: string; - /** @description Rating as defined by TV Parental Guidelines */ - rating?: string; - /** @description Program/Series Name. Program Name is required only if the show is part of a series. */ - show?: string; - /** @description Distribution Station/Channels or where the content is played. Any string value is accepted here */ - channel: string; - /** @description The number of the episode */ - episode?: string; - /** @description Creator of the content */ - originator?: string; - /** @description The date when the content first aired on television. Any date format is acceptable, but Adobe recommends: YYYY-MM-DD */ - firstAirDate?: string; - /** - * @description Identifies the stream type - * @enum {string} - */ - streamType?: 'audio' | 'video'; - /** @description The user has been authorized via Adobe authentication */ - authorized?: string; - hasResume?: boolean; - /** @description Format of the stream (HD, SD) */ - streamFormat?: string; - /** @description Name / ID of the radio station */ - station?: string; - /** @description Type or grouping of content as defined by content producer. Values should be comma delimited in variable implementation. In reporting, the list eVar will split each value into a line item, with each line item receiving equal metrics weight */ - genre?: string; - /** @description The season number the show belongs to. Season Series is required only if the show is part of a series */ - season?: string; - showType?: string; - /** @description Available values per Stream Type: Audio - "song", "podcast", "audiobook", "radio"; Video: "VoD", "Live", "Linear", "UGC", "DVoD" Customers can provide custom values for this parameter */ - contentType: string; - /** @description This is the "friendly" (human-readable) name of the content */ - friendlyName?: string; - /** @description Name of the player */ - playerName: string; - /** @description Name of the author (of an audiobook) */ - author?: string; - album?: string; - /** @description Clip Length/Runtime - This is the maximum length (or duration) of the content being consumed (in seconds) */ - length: number; - /** @description A property that defines the time of the day when the content was broadcast or played. This could have any value set as necessary by customers */ - dayPart?: string; - /** @description Name of the record label */ - label?: string; - /** @description MVPD provided via Adobe authentication. */ - mvpd?: string; - /** @description Type of feed */ - feed?: string; - /** @description This is the unique identifier for the content of the media asset, such as the TV series episode identifier, movie asset identifier, or live event identifier. Typically these IDs are derived from metadata authorities such as EIDR, TMS/Gracenote, or Rovi. These identifiers can also be from other proprietary or in-house systems. */ - assetID?: string; - /** @description Content ID of the content, which can be used to tie back to other industry / CMS IDs */ - name: string; - /** @description Name of the audio content publisher */ - publisher?: string; - /** @description The date when the content first aired on any digital channel or platform. Any date format is acceptable but Adobe recommends: YYYY-MM-DD */ - firstDigitalDate?: string; - /** @description The network/channel name */ - network?: string; - /** @description Set to true when the hit is generated due to playing a downloaded content media session. Not present when downloaded content is not played. */ - isDownloaded?: boolean; - }; - customMetadata?: { - name?: string; - value?: string; - }[]; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - implementationDetails?: { - version?: string; - }; - identityMap?: { - FPID?: { - id?: string; - /** - * @default ambiguous - * @enum {string} - */ - authenticatedState?: 'ambiguous' | 'authenticated' | 'loggedOut'; - primary?: boolean; - }[]; - }; - /** @default media.sessionStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; -} diff --git a/adobe-edge/src/internal/AdobeResponseBody.ts b/adobe-edge/src/internal/AdobeResponseBody.ts deleted file mode 100644 index 6892b66c..00000000 --- a/adobe-edge/src/internal/AdobeResponseBody.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface AdobeResponse { - status: number; - body?: AdobeResponseBody; -} - -export interface AdobeResponseBody { - requestId: string; - handle?: { - type: string; - payload: { [key: string]: any }[]; - }[]; -} diff --git a/adobe-edge/src/internal/AdobeTimeSeries.ts b/adobe-edge/src/internal/AdobeTimeSeries.ts deleted file mode 100644 index 2d71409c..00000000 --- a/adobe-edge/src/internal/AdobeTimeSeries.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { AdobeIdentityMap } from './AdobeIdentityMap'; -import type { AdobeMediaDetails } from '../api/details/AdobeMediaDetails'; -import type { AdobeImplementationDetails } from '../api/details/AdobeImplementationDetails'; -import type { EventType } from './EventType'; - -/** - * Used to indicate the behavior of time partitioned semantics when composed into data schemas. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/behaviors/time-series.schema.md - */ -export interface AdobeTimeSeries { - // The primary event type for this time-series record. - eventType: EventType; - - // The time when an event or observation occurred. - timestamp: string; - - // Custom metadata details information. - mediaCollection: AdobeMediaDetails; - - // Defines a map containing a set of end user identities - identityMap?: AdobeIdentityMap; - - // Details about the SDK, library, or service used in an application or web page implementation of a service. - implementationDetails?: AdobeImplementationDetails; -} diff --git a/adobe-edge/src/internal/media-edge/MediaEdge.d.ts b/adobe-edge/src/internal/web/MediaEdge.d.ts similarity index 100% rename from adobe-edge/src/internal/media-edge/MediaEdge.d.ts rename to adobe-edge/src/internal/web/MediaEdge.d.ts diff --git a/adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts b/adobe-edge/src/internal/web/MediaEdgeAPI.ts similarity index 77% rename from adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts rename to adobe-edge/src/internal/web/MediaEdgeAPI.ts index ae9ef820..197eb0a5 100644 --- a/adobe-edge/src/internal/media-edge/MediaEdgeAPI.ts +++ b/adobe-edge/src/internal/web/MediaEdgeAPI.ts @@ -11,8 +11,9 @@ import type { } from '@theoplayer/react-native-analytics-adobe-edge'; import { pathToEventTypeMap } from './PathToEventTypeMap'; import type { AdobePlayerStateData } from '../../api/details/AdobePlayerStateData'; -import { sanitisePlayhead } from '../../utils/Utils'; +import { sanitisePlayhead } from './Utils'; import { createInstance } from '@adobe/alloy'; +import { AdobeEdgeWebConfig } from '../../api/AdobeEdgeWebConfig'; const TAG = 'AdobeEdge'; @@ -29,20 +30,25 @@ interface ClientDescription { } const createdClients: ClientDescription[] = []; -function findAlloyClient(datastreamId: string, orgId: string): AlloyClient | undefined { - return createdClients.find((client) => client.datastreamId === datastreamId && client.orgId === orgId)?.client; -} - +/** + * The MediaEdgeAPI class is responsible for communicating media events to Adobe Experience Platform. + * + * Event handling for manually-tracked sessions is used. In this mode you need to pass the sessionID to the media event, + * along with the playhead value (integer value). You could also pass the Quality of Experience data details, if needed. + * + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/js-overview} + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/streamingmedia} + */ export class MediaEdgeAPI { - private _debugSessionId: string | undefined; private _sessionId: string | undefined; private _hasSessionFailed: boolean; private _eventQueue: (() => Promise)[] = []; private readonly _alloyClient: AlloyClient; - constructor(edgeBasePath: string, datastreamId: string, orgId: string, debugEnabled?: boolean, debugSessionId?: string) { - this._debugSessionId = debugSessionId; + constructor(config: AdobeEdgeWebConfig) { this._hasSessionFailed = false; + const sanitisedConfig = sanitiseConfig(config); + const { datastreamId, orgId, debugEnabled } = sanitisedConfig; this._alloyClient = findAlloyClient(datastreamId, orgId); if (!this._alloyClient) { @@ -55,36 +61,7 @@ export class MediaEdgeAPI { }, ], }); - this._alloyClient('configure', { - /** - * The datastreamId property is a string that determines which datastream in Adobe Experience Platform you want - * to send data to. - * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/datastreamid - */ - datastreamId, - - /** - * The orgId property is a string that tells Adobe which organization that data is sent to. This property is - * required for all data sent using the Web SDK. - * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/orgid - */ - orgId, - - /** - * The edgeBasePath property alters the destination path when you interact with Adobe services. - * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/edgebasepath - */ - edgeBasePath, - - /** - * - * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/streamingmedia - */ - streamingMedia: { - channel: 'Video channel', - playerName: 'THEOplayer', - }, - }); + this._alloyClient('configure', sanitisedConfig); // Store created client to prevent creating duplicates. createdClients.push({ datastreamId, orgId, client: this._alloyClient }); @@ -92,12 +69,16 @@ export class MediaEdgeAPI { this.setDebug(debugEnabled); } - setDebug(debug: boolean) { - this._alloyClient('setDebug', { enabled: debug }); + /** + * The appendIdentityToUrl command allows you to add a user identifier to the URL as a query string. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/appendidentitytourl} + */ + appendIdentityToUrl(url: string) { + this._alloyClient('appendIdentityToUrl', { url }); } - setDebugSessionId(id: string | undefined) { - this._debugSessionId = id; + setDebug(debug: boolean) { + this._alloyClient('setDebug', { enabled: debug }); } get sessionId(): string | undefined { @@ -105,7 +86,7 @@ export class MediaEdgeAPI { } hasSessionStarted(): boolean { - return !!this._sessionId; + return this._sessionId !== undefined; } hasSessionFailed(): boolean { @@ -133,7 +114,7 @@ export class MediaEdgeAPI { async ping(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { // Only send pings if the session has started, never queue them. if (this.hasSessionStarted()) { - void this.postEvent('/ping', { playhead, qoeDataDetails }); + void this.sendMediaEvent('/ping', { playhead, qoeDataDetails }); } } @@ -220,7 +201,9 @@ export class MediaEdgeAPI { } /** - * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/createmediasession + * Start a manually-tracked media sessions. + * + * {@link }https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/createmediasession} */ async startSession(sessionDetails: AdobeSessionDetails, customMetadata?: AdobeCustomMetadataDetails[], qoeDataDetails?: AdobeQoeDataDetails) { try { @@ -256,7 +239,7 @@ export class MediaEdgeAPI { return; } const doPostEvent = () => { - return this.postEvent(path, mediaDetails); + return this.sendMediaEvent(path, mediaDetails); }; // If the session has already started, do not queue but send it directly. @@ -267,7 +250,12 @@ export class MediaEdgeAPI { } } - async postEvent(path: keyof paths, mediaDetails: AdobeMediaDetails) { + /** + * Use the sendMediaEvent command to track media playbacks, pauses, completions, player state updates, and other + * related events. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/sendmediaevent} + */ + private async sendMediaEvent(path: keyof paths, mediaDetails: AdobeMediaDetails) { // Make sure we are positing data with a valid sessionID. if (!this._sessionId) { console.error(TAG, 'Invalid sessionID'); @@ -275,13 +263,9 @@ export class MediaEdgeAPI { } try { - const eventType = pathToEventTypeMap[path]; - /** - * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/sendmediaevent - */ this._alloyClient('sendMediaEvent', { xdm: { - eventType, + eventType: pathToEventTypeMap[path], mediaCollection: { ...mediaDetails, playhead: sanitisePlayhead(mediaDetails.playhead), @@ -294,3 +278,18 @@ export class MediaEdgeAPI { } } } + +function findAlloyClient(datastreamId: string, orgId: string): AlloyClient | undefined { + return createdClients.find((client) => client.datastreamId === datastreamId && client.orgId === orgId)?.client; +} + +function sanitiseConfig(config: AdobeEdgeWebConfig): AdobeEdgeWebConfig { + return { + ...config, + streamingMedia: { + ...config.streamingMedia, + channel: config.streamingMedia.channel || 'defaultChannel', + playerName: config.streamingMedia.playerName || 'THEOplayer', + }, + }; +} diff --git a/adobe-edge/src/internal/media-edge/PathToEventTypeMap.ts b/adobe-edge/src/internal/web/PathToEventTypeMap.ts similarity index 100% rename from adobe-edge/src/internal/media-edge/PathToEventTypeMap.ts rename to adobe-edge/src/internal/web/PathToEventTypeMap.ts diff --git a/adobe-edge/src/utils/Utils.ts b/adobe-edge/src/internal/web/Utils.ts similarity index 100% rename from adobe-edge/src/utils/Utils.ts rename to adobe-edge/src/internal/web/Utils.ts diff --git a/adobe-edge/src/internal/media-edge/media-edge-0.1.json b/adobe-edge/src/internal/web/media-edge-0.1.json similarity index 100% rename from adobe-edge/src/internal/media-edge/media-edge-0.1.json rename to adobe-edge/src/internal/web/media-edge-0.1.json diff --git a/adobe-edge/src/utils/UserAgent.ts b/adobe-edge/src/utils/UserAgent.ts deleted file mode 100644 index a349d904..00000000 --- a/adobe-edge/src/utils/UserAgent.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { I18nManager, Platform, Settings } from 'react-native'; -import DeviceInfo from 'react-native-device-info'; - -const USER_AGENT_PREFIX = 'Mozilla/5.0'; -const UNKNOWN = 'unknown'; - -function nonEmptyOrUnknown(str?: string): string { - return str && str !== '' ? str : UNKNOWN; -} - -export function buildUserAgent(): string { - if (Platform.OS === 'android') { - const { Release, Model: deviceName } = Platform.constants; - const localeString = nonEmptyOrUnknown(I18nManager?.getConstants()?.localeIdentifier?.replace('_', '-')); - const operatingSystem = `Android ${Release}`; - const deviceBuildId = nonEmptyOrUnknown(DeviceInfo.getBuildIdSync()); - // operatingSystem: `Android Build.VERSION.RELEASE` - // deviceName: Build.MODEL - // Example: Mozilla/5.0 (Linux; U; Android 7.1.2; en-US; AFTN Build/NS6296) - return `${USER_AGENT_PREFIX} (Linux; U; ${operatingSystem}; ${localeString}; ${deviceName} Build/${deviceBuildId})`; - } else if (Platform.OS === 'ios') { - const localeString = Settings.get('AppleLocale') || Settings.get('AppleLanguages')[0]; - const model = DeviceInfo.getModel(); - const osVersion = DeviceInfo.getSystemVersion().replace('.', '_'); - return `${USER_AGENT_PREFIX} (${model}; CPU OS ${osVersion} like Mac OS X; ${localeString})`; - } else if (Platform.OS === 'web') { - return navigator.userAgent; - } /* if (Platform.OS === 'windows' || Platform.OS === 'macos') */ else { - // Custom User-Agent for Windows and macOS not supported - return 'Unknown'; - } -} diff --git a/adobe-edge/src/utils/UserAgent.web.ts b/adobe-edge/src/utils/UserAgent.web.ts deleted file mode 100644 index 62c690ff..00000000 --- a/adobe-edge/src/utils/UserAgent.web.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function buildUserAgent(): string | undefined { - return navigator.userAgent; -} diff --git a/apps/e2e/android/app/build.gradle b/apps/e2e/android/app/build.gradle index 98920e79..ac364dbb 100644 --- a/apps/e2e/android/app/build.gradle +++ b/apps/e2e/android/app/build.gradle @@ -112,12 +112,12 @@ def safeExtGet(prop, fallback) { } dependencies { - implementation project(path: ':react-native-theoplayer-analytics-adobe') +// implementation project(path: ':react-native-theoplayer-analytics-adobe') implementation project(path: ':react-native-theoplayer-analytics-adobe-edge') - implementation project(path: ':react-native-theoplayer-analytics-comscore') - implementation project(path: ':react-native-theoplayer-analytics-conviva') - implementation project(path: ':react-native-theoplayer-analytics-nielsen') - implementation project(path: ':react-native-theoplayer-yospace') +// implementation project(path: ':react-native-theoplayer-analytics-comscore') +// implementation project(path: ':react-native-theoplayer-analytics-conviva') +// implementation project(path: ':react-native-theoplayer-analytics-nielsen') +// implementation project(path: ':react-native-theoplayer-yospace') // implementation project(path: ':react-native-theoplayer-analytics-adscript') // implementation project(path: ':react-native-theoplayer-analytics-gemius') diff --git a/apps/e2e/android/gradle/wrapper/gradle-wrapper.properties b/apps/e2e/android/gradle/wrapper/gradle-wrapper.properties index 2733ed5d..3ae1e2f1 100644 --- a/apps/e2e/android/gradle/wrapper/gradle-wrapper.properties +++ b/apps/e2e/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 2df9a5cd1b97a1eacc6b39bacdf482bb7a575669 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Thu, 11 Dec 2025 09:32:44 +0100 Subject: [PATCH 07/70] Fix hook properties --- adobe-edge/src/api/hooks/useAdobe.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/adobe-edge/src/api/hooks/useAdobe.ts b/adobe-edge/src/api/hooks/useAdobe.ts index dede303f..6635e965 100644 --- a/adobe-edge/src/api/hooks/useAdobe.ts +++ b/adobe-edge/src/api/hooks/useAdobe.ts @@ -1,13 +1,9 @@ import { PlayerEventType, THEOplayer } from 'react-native-theoplayer'; import { RefObject, useEffect, useRef } from 'react'; import { AdobeConnector } from '../AdobeConnector'; +import { AdobeEdgeConfig } from '../AdobeEdgeConfig'; -export function useAdobe( - edgeBasePath: string, - datastreamId: string, - orgId: string, - useDebug?: boolean, -): [RefObject, (player: THEOplayer | undefined) => void] { +export function useAdobe(config: AdobeEdgeConfig): [RefObject, (player: THEOplayer | undefined) => void] { const connector = useRef(undefined); const theoPlayer = useRef(undefined); @@ -17,7 +13,7 @@ export function useAdobe( theoPlayer.current = player; if (player) { - connector.current = new AdobeConnector(player, edgeBasePath, datastreamId, orgId, useDebug); + connector.current = new AdobeConnector(player, config); player.addEventListener(PlayerEventType.DESTROY, onDestroy); } else { throw new Error('Invalid THEOplayer instance'); From 88dcbd4170da65ded8a140cda35ada3633b6e029 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Thu, 11 Dec 2025 09:59:27 +0100 Subject: [PATCH 08/70] Fix optional properties --- adobe-edge/src/internal/web/MediaEdgeAPI.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adobe-edge/src/internal/web/MediaEdgeAPI.ts b/adobe-edge/src/internal/web/MediaEdgeAPI.ts index 197eb0a5..0dcb6035 100644 --- a/adobe-edge/src/internal/web/MediaEdgeAPI.ts +++ b/adobe-edge/src/internal/web/MediaEdgeAPI.ts @@ -288,8 +288,8 @@ function sanitiseConfig(config: AdobeEdgeWebConfig): AdobeEdgeWebConfig { ...config, streamingMedia: { ...config.streamingMedia, - channel: config.streamingMedia.channel || 'defaultChannel', - playerName: config.streamingMedia.playerName || 'THEOplayer', + channel: config.streamingMedia?.channel || 'defaultChannel', + playerName: config.streamingMedia?.playerName || 'THEOplayer', }, }; } From 36927c1b3e097356d5893f56962536b6519235c9 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Thu, 11 Dec 2025 12:57:05 +0100 Subject: [PATCH 09/70] Fix session start --- .../adobe/edge/AdobeEdgeConnector.kt | 2 +- .../edge/ReactTHEOplayerAdobeEdgeModule.kt | 24 +++++++++++++++---- .../adobe/edge/api/MediaEdgeAPI.kt | 16 +++++-------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt index 517a4953..f848cc11 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt @@ -378,7 +378,7 @@ class AdobeEdgeConnector(private val player: Player, debug: Boolean? = false) { sessionInProgress = true - Logger.debug("maybeStartSession - STARTED sessionId: ${mediaApi.sessionId}") + Logger.debug("maybeStartSession - STARTED") } private fun getContentLength(mediaLengthSec: Double?): Int { diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt index 3aba1bf8..1555a40d 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt @@ -1,6 +1,7 @@ package com.theoplayer.reactnative.adobe.edge import android.app.Application +import com.adobe.marketing.mobile.InitOptions import com.adobe.marketing.mobile.MobileCore import com.facebook.react.bridge.* import com.theoplayer.ReactTHEOplayerView @@ -9,6 +10,7 @@ import com.theoplayer.util.ViewResolver private const val TAG = "AdobeEdgeModule" private const val PROP_APP_ID = "appId" +private const val PROP_CONFIG_ASSET = "configAsset" private const val PROP_DEBUG_ENABLED = "debugEnabled" @Suppress("unused") @@ -28,10 +30,24 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : tag: Int, config: ReadableMap, ) { - MobileCore.initialize( - reactApplicationContext.applicationContext as Application, - config.getString(PROP_APP_ID) ?: "N/A" - ) + /** + * If an asset config file is provided, use it to initialize the MobileCore SDK, otherwise use + * the App ID. + * {@link https://developer.adobe.com/client-sdks/home/base/mobile-core/configuration/api-reference/} + */ + val configAsset = config.getString(PROP_CONFIG_ASSET) + if (configAsset != null) { + MobileCore.initialize( + reactApplicationContext.applicationContext as Application, + InitOptions.configureWithFileInAssets(configAsset), + null + ) + } else { + MobileCore.initialize( + reactApplicationContext.applicationContext as Application, + config.getString(PROP_APP_ID) ?: "MissingAppID" + ) + } viewResolver.resolveViewByTag(tag) { view: ReactTHEOplayerView? -> view?.playerContext?.playerView?.let { playerView -> diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt index 2add1822..dc462da4 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt @@ -20,18 +20,13 @@ class MediaEdgeAPI(channel: String) { ) ) - var sessionId: String? = null + var hasSession = false private set - var hasSessionFailed = false - private set - - - fun hasSessionStarted(): Boolean = sessionId != null + fun hasSessionStarted(): Boolean = hasSession fun reset() { - sessionId = null - hasSessionFailed = false + hasSession = false } fun play() { @@ -175,8 +170,8 @@ class MediaEdgeAPI(channel: String) { ) { tracker.trackSessionStart( Media.createMediaObject( - sessionDetails.friendlyName ?: "", - sessionDetails.assetID ?: "", + sessionDetails.friendlyName ?: "N/A", + sessionDetails.assetID ?: "N/A", sessionDetails.length, sessionDetails.contentType.name, when (sessionDetails.streamType) { @@ -186,5 +181,6 @@ class MediaEdgeAPI(channel: String) { ), customMetadata?.associate { it.name to (it.value ?: "") } ) + hasSession = true } } From 9722da1b41e8ed708b879b3975ac402c67af5024 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Thu, 11 Dec 2025 13:12:57 +0100 Subject: [PATCH 10/70] Update docs --- adobe-edge/src/api/AdobeEdgeMobileConfig.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/adobe-edge/src/api/AdobeEdgeMobileConfig.ts b/adobe-edge/src/api/AdobeEdgeMobileConfig.ts index fdd0205f..594a79b3 100644 --- a/adobe-edge/src/api/AdobeEdgeMobileConfig.ts +++ b/adobe-edge/src/api/AdobeEdgeMobileConfig.ts @@ -1,5 +1,25 @@ export interface AdobeEdgeMobileConfig { + /** + * The unique environment ID that the SDK uses to retrieve your configuration. + * This ID is generated when an app configuration is created and published to a given environment. + * The app is first launched and then the SDK retrieves and uses this Adobe-hosted configuration. + * {@link https://developer.adobe.com/client-sdks/previous-versions/documentation/mobile-core/configuration/} + */ appId?: string; + /** + * On Android, instead of providing an `appId` that is used to retrieve the Adobe-hosted configuration, + * load the configuration from a JSON configuration file in the app's Assets folder. + * + * {@link https://developer.adobe.com/client-sdks/home/base/mobile-core/api-reference/#setloglevel} + * @platform android + */ + configAsset?: string; + + /** + * The debugEnabled property allows you to enable or disable debugging using SDK code. + * + * @default `false`. + */ debugEnabled?: boolean; } From 125e243473340c11e7e8bebbbe96a93d2f7e0f5a Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Thu, 11 Dec 2025 13:31:15 +0100 Subject: [PATCH 11/70] Extract ID & channel from metadata --- .../theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt index f848cc11..e41d417e 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt @@ -362,9 +362,9 @@ class AdobeEdgeConnector(private val player: Player, debug: Boolean? = false) { mediaApi.startSession( AdobeSessionDetails( - ID = "N/A", + ID = player.source?.metadata?.get("id") ?: "N/A", name = player.source?.metadata?.get("title") ?: "N/A", - channel = "N/A", + channel = player.source?.metadata?.get("channel") ?: "N/A", contentType = getContentType(), playerName = "THEOplayer", length = mediaLength From 771bddbe0bb7f8ca44247d078b82c6926a6b4890 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Thu, 11 Dec 2025 15:02:38 +0100 Subject: [PATCH 12/70] Update readme --- adobe-edge/README.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/adobe-edge/README.md b/adobe-edge/README.md index f3ddd848..5af1d6eb 100644 --- a/adobe-edge/README.md +++ b/adobe-edge/README.md @@ -15,13 +15,8 @@ To set up terminology, in chronological order, media tracking solutions were: ## Installation -The `@theoplayer/react-native` package has a peer dependency on `react-native-device-info`, which has to be installed as -well: - ```sh -npm install \ - react-native-device-info \ - @theoplayer/react-native-analytics-adobe-edge +npm install @theoplayer/react-native-analytics-adobe-edge ``` [//]: # (npm install @theoplayer/react-native-analytics-adobe) @@ -30,20 +25,27 @@ npm install \ ### Configuring the connector -Create the connector by providing the `THEOplayer` instance, the Media Collection API's end point, -Visitor Experience Cloud Org ID, Analytics Report Suite ID and the Analytics Tracking Server URL. +Create the connector by providing the `THEOplayer` instance and a configuration object with separate parts for +Web and mobile platforms. ```tsx import { useAdobe } from '@theoplayer/react-native-analytics-adobe-edge'; -const baseUrl = "https://edge.adobedc.net/ee-pre-prd/va/v1"; -const dataStreamId = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"; -const userAgent = ""; // Optionally provide a custom user-agent header value. -const debugSessionID = ""; // Optionally provide a query parameter to be added to outgoing requests. -const useNative = true; // Use a native connector on iOS & Android; `false` by default. +const config = { + webConfig: { + datastreamId: 'abcde123-abcd-1234-abcd-abcde1234567', + orgId: 'ADB3LETTERSANDNUMBERS@AdobeOrg', + edgeBasePath: 'ee', + debugEnabled: true, + }, + mobileConfig: { + appId: 'launch-1234567890abcdef1234567890abcdef12', + debugEnabled: true, + }, +}; const App = () => { - const [adobe, initAdobe] = useAdobe(baseUrl, dataStreamId, userAgent, true, debugSessionID, useNative); + const [adobe, initAdobe] = useAdobe(config); const onPlayerReady = (player: THEOplayer) => { // Initialize Adobe connector From dd5dd0bbafce4ce28a5473c720dc5a7212688c7e Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Thu, 11 Dec 2025 15:09:27 +0100 Subject: [PATCH 13/70] Update properties --- adobe-edge/README.md | 4 ++-- adobe-edge/src/api/AdobeConnector.ts | 12 ++++++------ adobe-edge/src/api/AdobeEdgeConfig.ts | 7 ++++--- adobe-edge/src/index.ts | 3 +++ 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/adobe-edge/README.md b/adobe-edge/README.md index 5af1d6eb..3a12bbc8 100644 --- a/adobe-edge/README.md +++ b/adobe-edge/README.md @@ -32,13 +32,13 @@ Web and mobile platforms. import { useAdobe } from '@theoplayer/react-native-analytics-adobe-edge'; const config = { - webConfig: { + web: { datastreamId: 'abcde123-abcd-1234-abcd-abcde1234567', orgId: 'ADB3LETTERSANDNUMBERS@AdobeOrg', edgeBasePath: 'ee', debugEnabled: true, }, - mobileConfig: { + mobile: { appId: 'launch-1234567890abcdef1234567890abcdef12', debugEnabled: true, }, diff --git a/adobe-edge/src/api/AdobeConnector.ts b/adobe-edge/src/api/AdobeConnector.ts index 954b1275..8845ca05 100644 --- a/adobe-edge/src/api/AdobeConnector.ts +++ b/adobe-edge/src/api/AdobeConnector.ts @@ -12,16 +12,16 @@ export class AdobeConnector { constructor(player: THEOplayer, config: AdobeEdgeConfig) { if (['ios', 'android'].includes(Platform.OS)) { - if (config.mobileConfig) { - this.connectorAdapter = new AdobeConnectorAdapterNative(player, config.mobileConfig); + if (config.mobile) { + this.connectorAdapter = new AdobeConnectorAdapterNative(player, config.mobile); } else { - console.error('AdobeConnector Error: Missing mobileConfig for mobile platform'); + console.error('AdobeConnector Error: Missing config for mobile platform'); } } else { - if (config.webConfig) { - this.connectorAdapter = new AdobeConnectorAdapterWeb(player, config.webConfig); + if (config.web) { + this.connectorAdapter = new AdobeConnectorAdapterWeb(player, config.web); } else { - console.error('AdobeConnector Error: Missing webConfig for Web platform'); + console.error('AdobeConnector Error: Missing config for Web platform'); } } } diff --git a/adobe-edge/src/api/AdobeEdgeConfig.ts b/adobe-edge/src/api/AdobeEdgeConfig.ts index 11c9f237..93f460b2 100644 --- a/adobe-edge/src/api/AdobeEdgeConfig.ts +++ b/adobe-edge/src/api/AdobeEdgeConfig.ts @@ -3,11 +3,12 @@ import { AdobeEdgeWebConfig } from './AdobeEdgeWebConfig'; export interface AdobeEdgeConfig { /** + * Configuration for Adobe Edge on mobile platforms. */ - mobileConfig?: AdobeEdgeMobileConfig; + mobile?: AdobeEdgeMobileConfig; /** - * + * Configuration for Adobe Edge on web platforms. */ - webConfig?: AdobeEdgeWebConfig; + web?: AdobeEdgeWebConfig; } diff --git a/adobe-edge/src/index.ts b/adobe-edge/src/index.ts index 712421ca..02753948 100644 --- a/adobe-edge/src/index.ts +++ b/adobe-edge/src/index.ts @@ -1,4 +1,7 @@ export { AdobeConnector } from './api/AdobeConnector'; +export type { AdobeEdgeConfig } from './api/AdobeEdgeConfig'; +export type { AdobeEdgeMobileConfig } from './api/AdobeEdgeMobileConfig'; +export type { AdobeEdgeWebConfig } from './api/AdobeEdgeWebConfig'; export * from './api/details/barrel'; export { useAdobe } from './api/hooks/useAdobe'; export { sdkVersions } from './internal/version/Version'; From 04357eabad073b4e3e8a1fe4f5a3a9f865652cb6 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Thu, 11 Dec 2025 15:12:34 +0100 Subject: [PATCH 14/70] Fix test --- apps/e2e/src/tests/AdobeEdge.spec.ts | 13 ++++++++++- apps/e2e/src/tests/AdobeEdgeNative.spec.ts | 25 ---------------------- apps/e2e/src/tests/index.ts | 3 +-- 3 files changed, 13 insertions(+), 28 deletions(-) delete mode 100644 apps/e2e/src/tests/AdobeEdgeNative.spec.ts diff --git a/apps/e2e/src/tests/AdobeEdge.spec.ts b/apps/e2e/src/tests/AdobeEdge.spec.ts index 95d69daa..c9a7970a 100644 --- a/apps/e2e/src/tests/AdobeEdge.spec.ts +++ b/apps/e2e/src/tests/AdobeEdge.spec.ts @@ -9,7 +9,18 @@ export default function (spec: TestScope) { testConnector( spec, (player: THEOplayer) => { - connector = new AdobeConnector(player, 'https://edge.adobedc.net/ee-pre-prd/va/v1', 'dataStreamId', undefined, true, undefined, false); + connector = new AdobeConnector(player, { + web: { + datastreamId: 'abcde123-abcd-1234-abcd-abcde1234567', + orgId: 'ADB3LETTERSANDNUMBERS@AdobeOrg', + edgeBasePath: 'ee', + debugEnabled: true, + }, + mobile: { + appId: 'launch-1234567890abcdef1234567890abcdef12', + debugEnabled: true, + }, + }); }, () => { connector.stopAndStartNewSession([ diff --git a/apps/e2e/src/tests/AdobeEdgeNative.spec.ts b/apps/e2e/src/tests/AdobeEdgeNative.spec.ts deleted file mode 100644 index fcaa9b5d..00000000 --- a/apps/e2e/src/tests/AdobeEdgeNative.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { TestScope } from 'cavy'; -import { AdobeConnector } from '@theoplayer/react-native-analytics-adobe-edge'; -import { testConnector } from './ConnectorUtils'; -import { THEOplayer } from 'react-native-theoplayer'; - -export default function (spec: TestScope) { - spec.describe(`Setup Adobe Edge Native connector`, function () { - let connector: AdobeConnector; - testConnector( - spec, - (player: THEOplayer) => { - connector = new AdobeConnector(player, 'https://edge.adobedc.net/ee-pre-prd/va/v1', 'dataStreamId', undefined, true, undefined, true); - }, - () => { - connector.stopAndStartNewSession([ - { name: 'title', value: 'test' }, - { name: 'custom1', value: 'value1' }, - ]); - }, - () => { - connector.destroy(); - }, - ); - }); -} diff --git a/apps/e2e/src/tests/index.ts b/apps/e2e/src/tests/index.ts index e2749b3c..ea2a6c07 100644 --- a/apps/e2e/src/tests/index.ts +++ b/apps/e2e/src/tests/index.ts @@ -1,14 +1,13 @@ import Adobe from './Adobe.spec'; import AdobeNative from './AdobeNative.spec'; import AdobeEdge from './AdobeEdge.spec'; -import AdobeEdgeNative from './AdobeEdgeNative.spec'; import Comscore from './Comscore.spec'; import Conviva from './Conviva.spec'; import Nielsen from './Nielsen.spec'; import Yospace from './Yospace.spec'; import { Platform } from 'react-native'; -const tests = [Adobe, AdobeNative, AdobeEdge, AdobeEdgeNative, Comscore, Conviva, Nielsen]; +const tests = [Adobe, AdobeNative, AdobeEdge, Comscore, Conviva, Nielsen]; if (Platform.OS === 'android') { tests.push(Yospace); } From 8a8d4e490122639548ad541438c366bc4f9a130a Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 12 Dec 2025 14:23:21 +0100 Subject: [PATCH 15/70] Simplify connector --- adobe-edge/README.md | 2 +- .../adobe/edge/AdobeEdgeAdapter.kt | 32 -- .../adobe/edge/AdobeEdgeConnector.kt | 405 +--------------- .../adobe/edge/AdobeEdgeHandler.kt | 437 ++++++++++++++++++ .../reactnative/adobe/edge/Logger.kt | 22 - .../edge/ReactTHEOplayerAdobeEdgeModule.kt | 42 +- .../reactnative/adobe/edge/Utils.kt | 60 +-- .../adobe/edge/api/AdobeAdvertisingDetails.kt | 37 -- .../edge/api/AdobeAdvertisingPodDetails.kt | 17 - .../adobe/edge/api/AdobeChapterDetails.kt | 25 - .../edge/api/AdobeCustomMetadataDetails.kt | 14 - .../adobe/edge/api/AdobeErrorDetails.kt | 18 - .../edge/api/AdobeImplementationDetails.kt | 25 - .../adobe/edge/api/AdobeMediaDetails.kt | 34 -- .../adobe/edge/api/AdobePlayerStateData.kt | 17 - .../adobe/edge/api/AdobeQoeDataDetails.kt | 51 -- .../adobe/edge/api/AdobeSessionDetails.kt | 153 ------ .../reactnative/adobe/edge/api/EventType.kt | 25 - .../adobe/edge/api/MediaEdgeAPI.kt | 186 -------- .../adobe/edge/api/PathToEventTypeMap.kt | 22 - .../reactnative/adobe/edge/api/Utils.kt | 27 -- adobe-edge/src/api/AdobeEdgeMobileConfig.ts | 13 +- 22 files changed, 494 insertions(+), 1170 deletions(-) delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeAdapter.kt create mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Logger.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingDetails.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingPodDetails.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeChapterDetails.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeCustomMetadataDetails.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeErrorDetails.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeImplementationDetails.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeMediaDetails.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobePlayerStateData.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeQoeDataDetails.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeSessionDetails.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/EventType.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/PathToEventTypeMap.kt delete mode 100644 adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/Utils.kt diff --git a/adobe-edge/README.md b/adobe-edge/README.md index 3a12bbc8..9a8ea2ec 100644 --- a/adobe-edge/README.md +++ b/adobe-edge/README.md @@ -39,7 +39,7 @@ const config = { debugEnabled: true, }, mobile: { - appId: 'launch-1234567890abcdef1234567890abcdef12', + environmentId: 'abcdef123456/abcdef123456/launch-1234567890abcdef1234567890abcdef12', debugEnabled: true, }, }; diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeAdapter.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeAdapter.kt deleted file mode 100644 index 0be08a3c..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeAdapter.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge - -import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.ReadableMap -import com.theoplayer.reactnative.adobe.edge.api.AdobeCustomMetadataDetails -import com.theoplayer.reactnative.adobe.edge.api.AdobeErrorDetails -import com.theoplayer.reactnative.adobe.edge.api.ErrorSource - -fun ReadableMap.toAdobeErrorDetails(): AdobeErrorDetails { - return AdobeErrorDetails( - name = this.getString("name") ?: "NA", - source = when (this.getString("source")) { - "player" -> ErrorSource.PLAYER - else -> ErrorSource.EXTERNAL - } - ) -} - -fun ReadableMap.toAdobeCustomMetadataDetails() : AdobeCustomMetadataDetails { - return AdobeCustomMetadataDetails( - name = getString("name"), - value = getString("value") - ) -} - -fun ReadableArray.toAdobeCustomMetadataDetails() : List { - return mutableListOf().apply { - toArrayList() - .map { e -> (e as? ReadableMap)?.toAdobeCustomMetadataDetails() } - .filter { e -> e != null } - } -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt index e41d417e..10f80c90 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt @@ -1,409 +1,32 @@ -@file:Suppress("unused") - package com.theoplayer.reactnative.adobe.edge import com.adobe.marketing.mobile.LoggingMode -import com.adobe.marketing.mobile.MobileCore -import com.theoplayer.android.api.event.EventListener -import com.theoplayer.android.api.event.ads.AdBeginEvent -import com.theoplayer.android.api.event.ads.AdBreakBeginEvent -import com.theoplayer.android.api.event.ads.AdBreakEndEvent -import com.theoplayer.android.api.event.ads.AdEndEvent -import com.theoplayer.android.api.event.ads.AdSkipEvent -import com.theoplayer.android.api.event.ads.AdsEventTypes -import com.theoplayer.android.api.event.player.EndedEvent -import com.theoplayer.android.api.event.player.ErrorEvent -import com.theoplayer.android.api.event.player.PauseEvent -import com.theoplayer.android.api.event.player.PlayerEventTypes -import com.theoplayer.android.api.event.player.PlayingEvent -import com.theoplayer.android.api.event.player.SeekedEvent -import com.theoplayer.android.api.event.player.SeekingEvent -import com.theoplayer.android.api.event.player.SourceChangeEvent -import com.theoplayer.android.api.event.player.TimeUpdateEvent -import com.theoplayer.android.api.event.player.WaitingEvent -import com.theoplayer.android.api.event.track.mediatrack.video.ActiveQualityChangedEvent -import com.theoplayer.android.api.event.track.mediatrack.video.VideoTrackEventTypes -import com.theoplayer.android.api.event.track.mediatrack.video.list.VideoTrackListEventTypes -import com.theoplayer.android.api.event.track.texttrack.EnterCueEvent -import com.theoplayer.android.api.event.track.texttrack.ExitCueEvent -import com.theoplayer.android.api.event.track.texttrack.TextTrackEventTypes -import com.theoplayer.android.api.event.track.texttrack.list.TextTrackListEventTypes import com.theoplayer.android.api.player.Player -import com.theoplayer.android.api.player.track.texttrack.TextTrackKind -import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCue -import com.theoplayer.reactnative.adobe.edge.api.AdobeCustomMetadataDetails -import com.theoplayer.reactnative.adobe.edge.api.AdobeErrorDetails -import com.theoplayer.reactnative.adobe.edge.api.AdobeQoeDataDetails -import com.theoplayer.reactnative.adobe.edge.api.AdobeSessionDetails -import com.theoplayer.reactnative.adobe.edge.api.ContentType -import com.theoplayer.reactnative.adobe.edge.api.ErrorSource -import com.theoplayer.reactnative.adobe.edge.api.MediaEdgeAPI -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import okhttp3.MediaType.Companion.toMediaType - -typealias AddTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.AddTrackEvent -typealias RemoveTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.RemoveTrackEvent -typealias AddVideoTrackEvent = com.theoplayer.android.api.event.track.mediatrack.video.list.AddTrackEvent -typealias RemoveVideoTrackEvent = com.theoplayer.android.api.event.track.mediatrack.video.list.RemoveTrackEvent - -private const val TAG = "AdobeEdgeConnector" -private val JSON_MEDIA_TYPE = "application/json".toMediaType() - -class AdobeEdgeConnector(private val player: Player, debug: Boolean? = false) { - private var sessionInProgress = false - - private var adBreakPodIndex = 0 - - private var adPodPosition = 1 - - private var isPlayingAd = false - - private var customMetadata: MutableList = mutableListOf() - - private var currentChapter: TextTrackCue? = null - - private var customUserAgent: String? = null - - private val scope = CoroutineScope(Dispatchers.Main) - - private val onPlaying = EventListener { handlePlaying() } - private val onPause = EventListener { handlePause() } - private val onEnded = EventListener { handleEnded() } - private val onTimeUpdate = EventListener { handleTimeUpdate(it) } - private val onWaiting = EventListener { handleWaiting() } - private val onSeeking = EventListener { handleSeeking() } - private val onSeeked = EventListener { handleSeeked() } - private val onSourceChange = EventListener { handleSourceChange() } - private val onAddTextTrack = EventListener { handleAddTextTrack(it) } - private val onRemoveTextTrack = EventListener { handleRemoveTextTrack(it) } - private val onAddVideoTrack = EventListener { handleAddVideoTrack(it) } - private val onRemoveVideoTrack = - EventListener { handleRemoveVideoTrack(it) } - private val onActiveVideoQualityChanged = - EventListener { handleQualityChanged(it) } - private val onEnterCue = EventListener { handleEnterCue(it) } - private val onExitCue = EventListener { handleExitCue(it) } - private val onError = EventListener { handleError(it) } - private val onAdBreakBegin = - EventListener { event -> handleAdBreakBegin(event) } - private val onAdBreakEnd = EventListener { event -> handleAdBreakEnd() } - private val onAdBegin = EventListener { event -> handleAdBegin(event) } - private val onAdEnd = EventListener { handleAdEnd(it) } - private val onAdSkip = EventListener { event -> handleAdSkip() } - - private val mediaApi: MediaEdgeAPI = MediaEdgeAPI("N/A") - - init { - setDebug(debug ?: false) - addEventListeners() - Logger.debug("Initialized connector") - } - - fun setDebug(debug: Boolean) { - Logger.debug = debug - MobileCore.setLogLevel(if (debug) LoggingMode.DEBUG else LoggingMode.ERROR) - } - - fun updateMetadata(metadata: List) { - customMetadata.addAll(metadata) - } - - fun setError(errorDetails: AdobeErrorDetails) { - mediaApi.error(errorDetails) - } - - fun stopAndStartNewSession(metadata: List?) { - scope.launch { - maybeEndSession() - metadata?.let { - updateMetadata(it) - } - maybeStartSession() - if (player.isPaused) { - handlePause() - } else { - handlePlaying() - } - } - } - - private fun addEventListeners() { - player.addEventListener(PlayerEventTypes.PLAYING, onPlaying) - player.addEventListener(PlayerEventTypes.PAUSE, onPause) - player.addEventListener(PlayerEventTypes.ENDED, onEnded) - player.addEventListener(PlayerEventTypes.WAITING, onWaiting) - player.addEventListener(PlayerEventTypes.SEEKING, onSeeking) - player.addEventListener(PlayerEventTypes.SEEKED, onSeeked) - player.addEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) - player.addEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) - player.textTracks.addEventListener(TextTrackListEventTypes.ADDTRACK, onAddTextTrack) - player.textTracks.addEventListener(TextTrackListEventTypes.REMOVETRACK, onRemoveTextTrack) - player.videoTracks.addEventListener(VideoTrackListEventTypes.ADDTRACK, onAddVideoTrack) - player.addEventListener(PlayerEventTypes.ERROR, onError) - player.ads.apply { - addEventListener(AdsEventTypes.AD_BREAK_BEGIN, onAdBreakBegin) - addEventListener(AdsEventTypes.AD_BREAK_END, onAdBreakEnd) - addEventListener(AdsEventTypes.AD_BEGIN, onAdBegin) - addEventListener(AdsEventTypes.AD_END, onAdEnd) - addEventListener(AdsEventTypes.AD_SKIP, onAdSkip) - } - } - - private fun removeEventListeners() { - player.removeEventListener(PlayerEventTypes.PLAYING, onPlaying) - player.removeEventListener(PlayerEventTypes.PAUSE, onPause) - player.removeEventListener(PlayerEventTypes.ENDED, onEnded) - player.removeEventListener(PlayerEventTypes.WAITING, onWaiting) - player.removeEventListener(PlayerEventTypes.SEEKING, onSeeking) - player.removeEventListener(PlayerEventTypes.SEEKED, onSeeked) - player.removeEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) - player.removeEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) - player.textTracks.removeEventListener(TextTrackListEventTypes.ADDTRACK, onAddTextTrack) - player.textTracks.removeEventListener( - TextTrackListEventTypes.REMOVETRACK, onRemoveTextTrack - ) - player.videoTracks.removeEventListener(VideoTrackListEventTypes.ADDTRACK, onAddVideoTrack) - player.removeEventListener(PlayerEventTypes.ERROR, onError) - player.ads.apply { - removeEventListener(AdsEventTypes.AD_BREAK_BEGIN, onAdBreakBegin) - removeEventListener(AdsEventTypes.AD_BREAK_END, onAdBreakEnd) - removeEventListener(AdsEventTypes.AD_BEGIN, onAdBegin) - removeEventListener(AdsEventTypes.AD_END, onAdEnd) - removeEventListener(AdsEventTypes.AD_SKIP, onAdSkip) - } - } - - private fun handlePlaying() { - // NOTE: In case of a pre-roll ad, the `playing` event will be sent twice: once starting the re-roll, and once - // starting content. During the pre-roll, all events will be queued. The session will be started after the pre-roll, - // making sure we can start the session with the correct content duration (not the ad duration). - Logger.debug("onPlaying") - scope.launch { - maybeStartSession(player.duration) - mediaApi.play() - } - } - - private fun handlePause() { - Logger.debug("onPause") - mediaApi.pause() - } - - private fun handleTimeUpdate(event: TimeUpdateEvent) { - Logger.debug("onWaiting") - mediaApi.updateCurrentPlayhead(event.currentTime) - } - - private fun handleWaiting() { - Logger.debug("onWaiting") - mediaApi.bufferStart() - } - - private fun handleSeeking() { - Logger.debug("handleSeeking") - mediaApi.seekStart() - } - - private fun handleSeeked() { - Logger.debug("handleSeeked") - mediaApi.seekComplete() - } - - private fun handleEnded() { - Logger.debug("onEnded") - mediaApi.sessionComplete() - reset() - } - - private fun handleSourceChange() { - Logger.debug("onSourceChange") - maybeEndSession() - } - - private fun handleQualityChanged(event: ActiveQualityChangedEvent) { - mediaApi.bitrateChange( - AdobeQoeDataDetails( - bitrate = event.quality?.bandwidth?.toInt() ?: 0, - ) - ) - } - - private fun handleAddTextTrack(event: AddTextTrackEvent) { - event.track.takeIf { it.kind == TextTrackKind.CHAPTERS.type }?.let { track -> - Logger.debug("onAddTextTrack - add chapter track ${track.uid}") - track.addEventListener(TextTrackEventTypes.ENTERCUE, onEnterCue) - track.addEventListener(TextTrackEventTypes.EXITCUE, onExitCue) - } - } - - private fun handleRemoveTextTrack(event: RemoveTextTrackEvent) { - event.track.takeIf { it.kind == TextTrackKind.CHAPTERS.type }?.let { track -> - Logger.debug("onRemoveTextTrack - remove chapter track ${track.uid}") - track.removeEventListener(TextTrackEventTypes.ENTERCUE, onEnterCue) - track.removeEventListener(TextTrackEventTypes.EXITCUE, onExitCue) - } - } - - private fun handleAddVideoTrack(event: AddVideoTrackEvent) { - Logger.debug("onAddVideoTrack") - event.track.addEventListener( - VideoTrackEventTypes.ACTIVEQUALITYCHANGEDEVENT, onActiveVideoQualityChanged - ) - } - - private fun handleRemoveVideoTrack(event: RemoveVideoTrackEvent) { - Logger.debug("onRemoveVideoTrack") - event.track.removeEventListener( - VideoTrackEventTypes.ACTIVEQUALITYCHANGEDEVENT, onActiveVideoQualityChanged - ) - } - - private fun handleEnterCue(event: EnterCueEvent) { - Logger.debug("onEnterCue") - val chapterCue = event.cue - if (currentChapter != null && currentChapter?.endTime != chapterCue.startTime) { - mediaApi.chapterSkip() - } - val chapterDetails = calculateChapterDetails(chapterCue) - mediaApi.chapterStart(chapterDetails, customMetadata) - currentChapter = chapterCue - } - - private fun handleExitCue(event: ExitCueEvent) { - Logger.debug("onExitCue") - mediaApi.chapterComplete() - } - - private fun handleError(event: ErrorEvent) { - Logger.debug("onError") - mediaApi.error( - AdobeErrorDetails( - name = event.errorObject.code.toString(), source = ErrorSource.PLAYER - ) - ) - } - - private fun handleAdBreakBegin(event: AdBreakBeginEvent) { - Logger.debug("onAdBreakBegin") - isPlayingAd = true - val podDetails = calculateAdvertisingPodDetails(event.adBreak, adBreakPodIndex) - mediaApi.adBreakStart(podDetails) - if (podDetails.index > adBreakPodIndex) { - adBreakPodIndex++ - } - } - - private fun handleAdBreakEnd() { - Logger.debug("onAdBreakEnd") - isPlayingAd = false - adPodPosition = 1 - mediaApi.adBreakComplete() - } - - private fun handleAdBegin(event: AdBeginEvent) { - Logger.debug("onAdBegin") - mediaApi.adStart(calculateAdvertisingDetails(event.ad, adPodPosition), customMetadata) - adPodPosition++ - } - - private fun handleAdEnd(event: AdEndEvent) { - Logger.debug("onAdEnd") - mediaApi.adComplete() - } - - private fun handleAdSkip() { - Logger.debug("onAdSkip") - mediaApi.adSkip() - } - - private fun maybeEndSession() { - Logger.debug("maybeEndSession") - if (mediaApi.hasSessionStarted()) { - mediaApi.sessionEnd() - } - reset() - } - - /** - * Start a new session, but only if: - * - no existing session has is in progress; - * - the player has a valid source; - * - no ad is playing, otherwise the ad's media duration will be picked up; - * - the player's content media duration is known. - * - * @param mediaLengthSec - * @private - */ - private fun maybeStartSession(mediaLengthSec: Double? = null) { - val mediaLength = getContentLength(mediaLengthSec) - val hasValidSource = player.source !== null - val hasValidDuration = isValidDuration(mediaLengthSec) - - Logger.debug( - "maybeStartSession - " + "mediaLength: $mediaLength, " + "hasValidSource: $hasValidSource, " + "hasValidDuration: $hasValidDuration, " + "isPlayingAd: ${player.ads.isPlaying}" - ) - - if (sessionInProgress) { - Logger.debug("maybeStartSession - NOT started: already in progress") - return - } - - if (isPlayingAd) { - Logger.debug("maybeStartSession - NOT started: playing ad") - return - } - - if (!hasValidSource || !hasValidDuration) { - Logger.debug("maybeStartSession - NOT started: invalid ${if (hasValidSource) "duration" else "source"}") - return - } - - mediaApi.startSession( - AdobeSessionDetails( - ID = player.source?.metadata?.get("id") ?: "N/A", - name = player.source?.metadata?.get("title") ?: "N/A", - channel = player.source?.metadata?.get("channel") ?: "N/A", - contentType = getContentType(), - playerName = "THEOplayer", - length = mediaLength - ), this.customMetadata - ) - if (!mediaApi.hasSessionStarted()) { - Logger.debug("maybeStartSession - session was not started") - return - } +class AdobeEdgeConnector( + player: Player, + trackerConfig: Map, +) { - sessionInProgress = true + private val handler = AdobeEdgeHandler(player, trackerConfig) - Logger.debug("maybeStartSession - STARTED") + fun updateMetadata(metadata: HashMap) { + handler.updateMetadata(metadata) } - private fun getContentLength(mediaLengthSec: Double?): Int { - return sanitiseContentLength(mediaLengthSec) + fun stopAndStartNewSession(metadata: Map?) { + handler.stopAndStartNewSession(metadata) } - private fun getContentType(): ContentType { - return if (player.duration == Double.POSITIVE_INFINITY) ContentType.LIVE else ContentType.VOD + fun setLoggingMode(loggingMode: LoggingMode) { + handler.setLoggingMode(loggingMode) } - fun reset() { - Logger.debug("reset") - mediaApi.reset() - adBreakPodIndex = 0 - adPodPosition = 1 - isPlayingAd = false - sessionInProgress = false - currentChapter = null + fun setError(errorId: String) { + handler.setError(errorId) } fun destroy() { - Logger.debug("destroy") - scope.launch { - maybeEndSession() - removeEventListeners() - } + handler.destroy() } } diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt new file mode 100644 index 00000000..c2708e5d --- /dev/null +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -0,0 +1,437 @@ +package com.theoplayer.reactnative.adobe.edge + +import android.util.Log +import com.adobe.marketing.mobile.LoggingMode +import com.adobe.marketing.mobile.MobileCore +import com.adobe.marketing.mobile.edge.media.Media +import com.adobe.marketing.mobile.edge.media.MediaConstants +import com.theoplayer.android.api.ads.LinearAd +import com.theoplayer.android.api.event.EventListener +import com.theoplayer.android.api.event.ads.AdBeginEvent +import com.theoplayer.android.api.event.ads.AdBreakBeginEvent +import com.theoplayer.android.api.event.ads.AdBreakEndEvent +import com.theoplayer.android.api.event.ads.AdEndEvent +import com.theoplayer.android.api.event.ads.AdSkipEvent +import com.theoplayer.android.api.event.ads.AdsEventTypes +import com.theoplayer.android.api.event.player.EndedEvent +import com.theoplayer.android.api.event.player.ErrorEvent +import com.theoplayer.android.api.event.player.PauseEvent +import com.theoplayer.android.api.event.player.PlayerEventTypes +import com.theoplayer.android.api.event.player.PlayingEvent +import com.theoplayer.android.api.event.player.SeekedEvent +import com.theoplayer.android.api.event.player.SeekingEvent +import com.theoplayer.android.api.event.player.SourceChangeEvent +import com.theoplayer.android.api.event.player.TimeUpdateEvent +import com.theoplayer.android.api.event.player.WaitingEvent +import com.theoplayer.android.api.event.track.mediatrack.video.ActiveQualityChangedEvent +import com.theoplayer.android.api.event.track.mediatrack.video.VideoTrackEventTypes +import com.theoplayer.android.api.event.track.mediatrack.video.list.VideoTrackListEventTypes +import com.theoplayer.android.api.event.track.texttrack.EnterCueEvent +import com.theoplayer.android.api.event.track.texttrack.ExitCueEvent +import com.theoplayer.android.api.event.track.texttrack.TextTrackEventTypes +import com.theoplayer.android.api.event.track.texttrack.list.TextTrackListEventTypes +import com.theoplayer.android.api.player.Player +import com.theoplayer.android.api.player.track.texttrack.TextTrackKind +import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCue + +typealias AddTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.AddTrackEvent +typealias RemoveTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.RemoveTrackEvent +typealias AddVideoTrackEvent = com.theoplayer.android.api.event.track.mediatrack.video.list.AddTrackEvent +typealias RemoveVideoTrackEvent = com.theoplayer.android.api.event.track.mediatrack.video.list.RemoveTrackEvent + +private const val TAG = "AdobeEdgeConnector" + +class AdobeEdgeHandler( + private val player: Player, + trackerConfig: Map = emptyMap() +) { + private var sessionInProgress = false + + private var adBreakPodIndex = 0 + + private var adPodPosition = 1 + + private var isPlayingAd = false + + private var customMetadata = mutableMapOf() + + private var currentChapter: TextTrackCue? = null + + private var loggingMode: LoggingMode = LoggingMode.ERROR + + private val onPlaying = EventListener { handlePlaying() } + private val onPause = EventListener { handlePause() } + private val onEnded = EventListener { handleEnded() } + private val onTimeUpdate = EventListener { handleTimeUpdate(it) } + private val onWaiting = EventListener { handleWaiting() } + private val onSeeking = EventListener { handleSeeking() } + private val onSeeked = EventListener { handleSeeked() } + private val onSourceChange = EventListener { handleSourceChange() } + private val onAddTextTrack = EventListener { handleAddTextTrack(it) } + private val onRemoveTextTrack = EventListener { handleRemoveTextTrack(it) } + private val onAddVideoTrack = EventListener { handleAddVideoTrack(it) } + private val onRemoveVideoTrack = + EventListener { handleRemoveVideoTrack(it) } + private val onActiveVideoQualityChanged = + EventListener { handleQualityChanged(it) } + private val onEnterCue = EventListener { handleEnterCue(it) } + private val onExitCue = EventListener { handleExitCue() } + private val onError = EventListener { handleError(it) } + private val onAdBreakBegin = + EventListener { event -> handleAdBreakBegin(event) } + private val onAdBreakEnd = EventListener { handleAdBreakEnd() } + private val onAdBegin = EventListener { event -> handleAdBegin(event) } + private val onAdEnd = EventListener { handleAdEnd() } + private val onAdSkip = EventListener { handleAdSkip() } + + private val tracker = Media.createTracker(trackerConfig) + + private fun logDebug(message: String) { + if (loggingMode >= LoggingMode.DEBUG) { + Log.d(TAG, message) + } + } + + init { + addEventListeners() + logDebug("Initialized connector") + } + + fun setLoggingMode(loggingMode: LoggingMode) { + this.loggingMode = loggingMode + MobileCore.setLogLevel(loggingMode) + } + + fun updateMetadata(metadata: Map) { + customMetadata += metadata + } + + fun setError(errorId: String) { + tracker.trackError(errorId) + } + + fun stopAndStartNewSession(metadata: Map?) { + maybeEndSession() + metadata?.let { + updateMetadata(it) + } + maybeStartSession() + if (player.isPaused) { + handlePause() + } else { + handlePlaying() + } + } + + private fun addEventListeners() { + player.addEventListener(PlayerEventTypes.PLAYING, onPlaying) + player.addEventListener(PlayerEventTypes.PAUSE, onPause) + player.addEventListener(PlayerEventTypes.ENDED, onEnded) + player.addEventListener(PlayerEventTypes.WAITING, onWaiting) + player.addEventListener(PlayerEventTypes.SEEKING, onSeeking) + player.addEventListener(PlayerEventTypes.SEEKED, onSeeked) + player.addEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) + player.addEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) + player.textTracks.addEventListener(TextTrackListEventTypes.ADDTRACK, onAddTextTrack) + player.textTracks.addEventListener(TextTrackListEventTypes.REMOVETRACK, onRemoveTextTrack) + player.videoTracks.addEventListener(VideoTrackListEventTypes.ADDTRACK, onAddVideoTrack) + player.videoTracks.addEventListener(VideoTrackListEventTypes.REMOVETRACK, onRemoveVideoTrack) + player.addEventListener(PlayerEventTypes.ERROR, onError) + player.ads.apply { + addEventListener(AdsEventTypes.AD_BREAK_BEGIN, onAdBreakBegin) + addEventListener(AdsEventTypes.AD_BREAK_END, onAdBreakEnd) + addEventListener(AdsEventTypes.AD_BEGIN, onAdBegin) + addEventListener(AdsEventTypes.AD_END, onAdEnd) + addEventListener(AdsEventTypes.AD_SKIP, onAdSkip) + } + } + + private fun removeEventListeners() { + player.removeEventListener(PlayerEventTypes.PLAYING, onPlaying) + player.removeEventListener(PlayerEventTypes.PAUSE, onPause) + player.removeEventListener(PlayerEventTypes.ENDED, onEnded) + player.removeEventListener(PlayerEventTypes.WAITING, onWaiting) + player.removeEventListener(PlayerEventTypes.SEEKING, onSeeking) + player.removeEventListener(PlayerEventTypes.SEEKED, onSeeked) + player.removeEventListener(PlayerEventTypes.TIMEUPDATE, onTimeUpdate) + player.removeEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) + player.textTracks.removeEventListener(TextTrackListEventTypes.ADDTRACK, onAddTextTrack) + player.textTracks.removeEventListener( + TextTrackListEventTypes.REMOVETRACK, onRemoveTextTrack + ) + player.videoTracks.removeEventListener(VideoTrackListEventTypes.ADDTRACK, onAddVideoTrack) + player.videoTracks.removeEventListener(VideoTrackListEventTypes.REMOVETRACK, onRemoveVideoTrack) + player.removeEventListener(PlayerEventTypes.ERROR, onError) + player.ads.apply { + removeEventListener(AdsEventTypes.AD_BREAK_BEGIN, onAdBreakBegin) + removeEventListener(AdsEventTypes.AD_BREAK_END, onAdBreakEnd) + removeEventListener(AdsEventTypes.AD_BEGIN, onAdBegin) + removeEventListener(AdsEventTypes.AD_END, onAdEnd) + removeEventListener(AdsEventTypes.AD_SKIP, onAdSkip) + } + } + + private fun handlePlaying() { + // NOTE: In case of a pre-roll ad, the `playing` event will be sent twice: once starting the re-roll, and once + // starting content. During the pre-roll, all events will be queued. The session will be started after the pre-roll, + // making sure we can start the session with the correct content duration (not the ad duration). + logDebug("onPlaying") + maybeStartSession(player.duration) + tracker.trackPlay() + } + + private fun handlePause() { + logDebug("onPause") + tracker.trackPause() + } + + private fun handleTimeUpdate(event: TimeUpdateEvent) { + logDebug("onTimeUpdate") + tracker.updateCurrentPlayhead(sanitisePlayhead(event.currentTime)) + } + + private fun handleWaiting() { + logDebug("onWaiting") + tracker.trackEvent(Media.Event.BufferStart, null, null) + } + + private fun handleSeeking() { + logDebug("handleSeeking") + tracker.trackEvent(Media.Event.SeekStart, null, null) + } + + private fun handleSeeked() { + logDebug("handleSeeked") + tracker.trackEvent(Media.Event.SeekComplete, null, null) + } + + private fun handleEnded() { + logDebug("onEnded") + /** + * Tracks the completion of the media playback session. Call this method only when the media has + * been completely viewed. If the viewing session is ended before the media is completely viewed, + * use trackSessionEnd instead. + */ + tracker.trackComplete() + reset() + } + + private fun handleSourceChange() { + logDebug("onSourceChange") + maybeEndSession() + } + + private fun handleQualityChanged(event: ActiveQualityChangedEvent) { + tracker.updateQoEObject( + Media.createQoEObject( + event.quality?.bandwidth?.toInt() ?: 0, + 0, + 0, + 0 + ) + ) + } + + private fun handleAddTextTrack(event: AddTextTrackEvent) { + event.track.takeIf { it.kind == TextTrackKind.CHAPTERS.type }?.let { track -> + logDebug("onAddTextTrack - add chapter track ${track.uid}") + track.addEventListener(TextTrackEventTypes.ENTERCUE, onEnterCue) + track.addEventListener(TextTrackEventTypes.EXITCUE, onExitCue) + } + } + + private fun handleRemoveTextTrack(event: RemoveTextTrackEvent) { + event.track.takeIf { it.kind == TextTrackKind.CHAPTERS.type }?.let { track -> + logDebug("onRemoveTextTrack - remove chapter track ${track.uid}") + track.removeEventListener(TextTrackEventTypes.ENTERCUE, onEnterCue) + track.removeEventListener(TextTrackEventTypes.EXITCUE, onExitCue) + } + } + + private fun handleAddVideoTrack(event: AddVideoTrackEvent) { + logDebug("onAddVideoTrack") + event.track.addEventListener( + VideoTrackEventTypes.ACTIVEQUALITYCHANGEDEVENT, onActiveVideoQualityChanged + ) + } + + private fun handleRemoveVideoTrack(event: RemoveVideoTrackEvent) { + logDebug("onRemoveVideoTrack") + event.track.removeEventListener( + VideoTrackEventTypes.ACTIVEQUALITYCHANGEDEVENT, onActiveVideoQualityChanged + ) + } + + private fun handleEnterCue(event: EnterCueEvent) { + logDebug("onEnterCue") + val chapterCue = event.cue + if (currentChapter != null && currentChapter?.endTime != chapterCue.startTime) { + tracker.trackEvent(Media.Event.ChapterSkip, null, null) + } + tracker.trackEvent( + Media.Event.ChapterStart, + Media.createChapterObject( + chapterCue.id, + chapterCue.id.toIntOrNull() ?: 0, + chapterCue.endTime.toInt(), + (chapterCue.endTime - chapterCue.startTime).toInt() + ), + customMetadata + ) + currentChapter = chapterCue + } + + private fun handleExitCue() { + logDebug("onExitCue") + tracker.trackEvent(Media.Event.ChapterComplete, null, null) + } + + private fun handleError(event: ErrorEvent) { + logDebug("onError") + tracker.trackError(event.errorObject.code.toString()) + } + + private fun handleAdBreakBegin(event: AdBreakBeginEvent) { + logDebug("onAdBreakBegin") + isPlayingAd = true + val currentAdBreakTimeOffset = event.adBreak.timeOffset + val index = when { + currentAdBreakTimeOffset == 0 -> 0 + currentAdBreakTimeOffset < 0 -> -1 + else -> adBreakPodIndex + 1 + } + tracker.trackEvent( + Media.Event.AdBreakStart, + Media.createAdBreakObject( + "NA", + index, + currentAdBreakTimeOffset + ), + null + ) + if (index > adBreakPodIndex) { + adBreakPodIndex++ + } + } + + private fun handleAdBreakEnd() { + logDebug("onAdBreakEnd") + isPlayingAd = false + adPodPosition = 1 + tracker.trackEvent(Media.Event.AdBreakComplete, null, null) + } + + private fun handleAdBegin(event: AdBeginEvent) { + logDebug("onAdBegin") + tracker.trackEvent( + Media.Event.AdStart, + Media.createAdObject( + "NA", + "NA", + adPodPosition, + (event.ad as? LinearAd)?.duration ?: 0 + ), + customMetadata + ) + adPodPosition++ + } + + private fun handleAdEnd() { + logDebug("onAdEnd") + tracker.trackEvent(Media.Event.AdComplete, null, null) + } + + private fun handleAdSkip() { + logDebug("onAdSkip") + tracker.trackEvent(Media.Event.AdSkip, null, null) + } + + private fun maybeEndSession() { + logDebug("maybeEndSession") + if (sessionInProgress) { + /** + * Tracks the end of a media playback session. Call this method when the viewing session ends, + * even if the user has not viewed the media to completion. If the media is viewed to completion, + * use trackComplete instead. + */ + tracker.trackSessionEnd() + } + reset() + } + + /** + * Start a new session, but only if: + * - no existing session has is in progress; + * - the player has a valid source; + * - no ad is playing, otherwise the ad's media duration will be picked up; + * - the player's content media duration is known. + * + * @param mediaLengthSec + * @private + */ + private fun maybeStartSession(mediaLengthSec: Double? = null) { + val mediaLength = getContentLength(mediaLengthSec) + val hasValidSource = player.source !== null + val hasValidDuration = isValidDuration(mediaLengthSec) + + logDebug( + "maybeStartSession - " + "mediaLength: $mediaLength, " + "hasValidSource: $hasValidSource, " + "hasValidDuration: $hasValidDuration, " + "isPlayingAd: ${player.ads.isPlaying}" + ) + + if (sessionInProgress) { + logDebug("maybeStartSession - NOT started: already in progress") + return + } + + if (isPlayingAd) { + logDebug("maybeStartSession - NOT started: playing ad") + return + } + + if (!hasValidSource || !hasValidDuration) { + logDebug("maybeStartSession - NOT started: invalid ${if (hasValidSource) "duration" else "source"}") + return + } + + tracker.trackSessionStart( + Media.createMediaObject( + player.source?.metadata?.get("title") ?: "N/A", + player.source?.metadata?.get("id") ?: "N/A", + mediaLength, + calculateStreamType(), + Media.MediaType.Video, + ), + customMetadata + ) + + sessionInProgress = true + + logDebug("maybeStartSession - STARTED") + } + + private fun getContentLength(mediaLengthSec: Double?): Int { + return sanitiseContentLength(mediaLengthSec) + } + + private fun calculateStreamType(): String { + return if (player.duration == Double.POSITIVE_INFINITY) + MediaConstants.StreamType.LIVE + else + MediaConstants.StreamType.VOD + } + + fun reset() { + logDebug("reset") + adBreakPodIndex = 0 + adPodPosition = 1 + isPlayingAd = false + sessionInProgress = false + currentChapter = null + } + + fun destroy() { + logDebug("destroy") + maybeEndSession() + removeEventListeners() + } +} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Logger.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Logger.kt deleted file mode 100644 index 7233bc14..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Logger.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge - -import android.util.Log - -object Logger { - var debug: Boolean = false - const val TAG = "AdobeEdgeConnector" - - fun debug(message: String) { - if (debug) { - Log.d(TAG, message) - } - } - - fun warn(message: String) { - Log.w(TAG, message) - } - - fun error(message: String) { - Log.e(TAG, message) - } -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt index 1555a40d..65d87c21 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt @@ -1,7 +1,7 @@ package com.theoplayer.reactnative.adobe.edge import android.app.Application -import com.adobe.marketing.mobile.InitOptions +import com.adobe.marketing.mobile.LoggingMode import com.adobe.marketing.mobile.MobileCore import com.facebook.react.bridge.* import com.theoplayer.ReactTHEOplayerView @@ -9,16 +9,15 @@ import com.theoplayer.util.ViewResolver private const val TAG = "AdobeEdgeModule" -private const val PROP_APP_ID = "appId" -private const val PROP_CONFIG_ASSET = "configAsset" +private const val PROP_ENVIRONMENT_ID = "environmentId" private const val PROP_DEBUG_ENABLED = "debugEnabled" +private const val PROP_NAME = "name" @Suppress("unused") class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { private val viewResolver: ViewResolver = ViewResolver(context) - private var adobeConnectors: HashMap = HashMap() override fun getName(): String { @@ -33,28 +32,22 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : /** * If an asset config file is provided, use it to initialize the MobileCore SDK, otherwise use * the App ID. - * {@link https://developer.adobe.com/client-sdks/home/base/mobile-core/configuration/api-reference/} + * {@link https://developer.adobe.com/client-sdks/edge/media-for-edge-network/} */ - val configAsset = config.getString(PROP_CONFIG_ASSET) - if (configAsset != null) { - MobileCore.initialize( - reactApplicationContext.applicationContext as Application, - InitOptions.configureWithFileInAssets(configAsset), - null - ) - } else { - MobileCore.initialize( - reactApplicationContext.applicationContext as Application, - config.getString(PROP_APP_ID) ?: "MissingAppID" - ) - } + MobileCore.initialize( + reactApplicationContext.applicationContext as Application, + config.getString(PROP_ENVIRONMENT_ID) ?: "MissingEnvironmentID" + ) viewResolver.resolveViewByTag(tag) { view: ReactTHEOplayerView? -> view?.playerContext?.playerView?.let { playerView -> adobeConnectors[tag] = AdobeEdgeConnector( player = playerView.player, - debug = if (config.hasKey(PROP_DEBUG_ENABLED)) config.getBoolean(PROP_DEBUG_ENABLED) else false, + trackerConfig = config.toHashMap().mapValues { it.value?.toString() ?: "" } ) + if (config.hasKey(PROP_DEBUG_ENABLED)) { + setDebug(tag, config.getBoolean(PROP_DEBUG_ENABLED)) + } } } } @@ -66,7 +59,10 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : */ @ReactMethod fun setDebug(tag: Int, debug: Boolean) { - adobeConnectors[tag]?.setDebug(debug) + adobeConnectors[tag]?.setLoggingMode(when (debug) { + true -> LoggingMode.DEBUG + false -> LoggingMode.ERROR + }) } /** @@ -74,9 +70,7 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : */ @ReactMethod fun updateMetadata(tag: Int, metadataList: ReadableArray) { - adobeConnectors[tag]?.updateMetadata( - metadataList.toAdobeCustomMetadataDetails() - ) + adobeConnectors[tag]?.updateMetadata(metadataList.toAdobeCustomMetadataDetails()) } /** @@ -84,7 +78,7 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : */ @ReactMethod fun setError(tag: Int, errorDetails: ReadableMap) { - adobeConnectors[tag]?.setError(errorDetails.toAdobeErrorDetails()) + adobeConnectors[tag]?.setError(errorDetails.getString(PROP_NAME) ?: "NA") } /** diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt index 0305dbc1..36b75231 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt @@ -1,51 +1,35 @@ package com.theoplayer.reactnative.adobe.edge -import com.theoplayer.android.api.ads.Ad -import com.theoplayer.android.api.ads.AdBreak -import com.theoplayer.android.api.ads.LinearAd -import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCue -import com.theoplayer.reactnative.adobe.edge.api.AdobeAdvertisingDetails -import com.theoplayer.reactnative.adobe.edge.api.AdobeAdvertisingPodDetails -import com.theoplayer.reactnative.adobe.edge.api.AdobeChapterDetails +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap + +private const val PROP_NAME = "name" +private const val PROP_VALUE = "value" fun sanitiseContentLength(mediaLength: Double?): Int { return if (mediaLength == Double.POSITIVE_INFINITY) { 86400 } else mediaLength?.toInt() ?: 0 } -fun calculateAdvertisingPodDetails(adBreak: AdBreak?, lastPodIndex: Int): AdobeAdvertisingPodDetails { - val currentAdBreakTimeOffset = adBreak?.timeOffset ?: 0 - return AdobeAdvertisingPodDetails( - index = when { - currentAdBreakTimeOffset == 0 -> 0 - currentAdBreakTimeOffset < 0 -> -1 - else ->lastPodIndex + 1 - }, - offset = currentAdBreakTimeOffset - ) -} - -fun calculateAdvertisingDetails(ad: Ad?, podPosition: Int): AdobeAdvertisingDetails { - return AdobeAdvertisingDetails( - podPosition = podPosition, - length = if (ad is LinearAd) ad.duration else 0, - name = "NA", - playerName = "THEOplayer" - ) -} - -fun calculateChapterDetails(cue: TextTrackCue): AdobeChapterDetails { - val index = try { - cue.id.toInt() - } catch (_: NumberFormatException) { - 0 +fun sanitisePlayhead(playhead: Double?): Int { + if (playhead == null) { + return 0 } - return AdobeChapterDetails( - length = (cue.endTime - cue.startTime).toInt(), - offset = cue.endTime.toInt(), - index = index - ) + if (playhead == Double.POSITIVE_INFINITY) { + // If content is live, the playhead must be the current second of the day. + val now = System.currentTimeMillis() + return ((now / 1000) % 86400).toInt() + } + return playhead.toInt() } fun isValidDuration(v: Double?): Boolean { return v != null && !v.isNaN() } + +fun ReadableArray.toAdobeCustomMetadataDetails() : HashMap { + return hashMapOf().apply { + toArrayList() + .map { e -> (e as? ReadableMap) } + .filter { e -> e != null && e.hasKey(PROP_NAME) && e.hasKey(PROP_VALUE) } + } +} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingDetails.kt deleted file mode 100644 index 40516cf9..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingDetails.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Advertising details information. - * - * @see Adobe XDM AdvertisingDetails Schema - */ -data class AdobeAdvertisingDetails( - // ID of the ad. Any integer and/or letter combination. - val id: String? = null, - // Company/Brand whose product is featured in the ad. - val advertiser: String? = null, - // ID of the ad campaign. - val campaignID: String? = null, - // ID of the ad creative. - val creativeID: String? = null, - // URL of the ad creative. - val creativeURL: String? = null, - // Ad is completed. - val isCompleted: Boolean? = null, - // Ad is started. - val isStarted: Boolean? = null, - // Length of video ad in seconds. - val length: Int, - // Friendly name of the ad. In reporting, “Ad Name” is the classification and “Ad Name (variable)” is the eVar. - val name: String, - // Placement ID of the ad. - val placementID: String? = null, - // The name of the player responsible for rendering the ad. - val playerName: String, - // The index of the ad inside the parent ad start, for example, the first ad has index 0 and the second ad has index 1. - val podPosition: Int, - // ID of the ad site. - val siteID: String? = null, - // The total amount of time, in seconds, spent watching the ad (i.e., the number of seconds played). - val timePlayed: Int? = null -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingPodDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingPodDetails.kt deleted file mode 100644 index b70e799b..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeAdvertisingPodDetails.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Advertising Pod details information. - * - * @see Adobe XDM AdvertisingPodDetails Schema - */ -data class AdobeAdvertisingPodDetails( - // The ID of the ad break. - val id: String? = null, - // The friendly name of the Ad Break. - val friendlyName: String? = null, - // The index of the ad inside the parent ad break start, for example, the first ad has index 0 and the second ad has index 1. - val index: Int, - // The offset of the ad break inside the content, in seconds. - val offset: Int -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeChapterDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeChapterDetails.kt deleted file mode 100644 index 5c9c7e1c..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeChapterDetails.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Chapter details information. - * - * @see Adobe XDM ChapterDetails Schema - */ -data class AdobeChapterDetails( - // The ID of the chapter. - val id: String? = null, - // The friendly name of the chapter. - val friendlyName: String? = null, - // The position (index, integer) of the chapter inside the content. - val index: Int, - // Chapter is completed. - val isCompleted: Boolean? = null, - // Chapter is started. - val isStarted: Boolean? = null, - // The length of the chapter, in seconds. - val length: Int, - // The offset of the chapter inside the content (in seconds) from the start. - val offset: Int, - // The time spent on the chapter, in seconds. - val timePlayed: Int? = null -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeCustomMetadataDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeCustomMetadataDetails.kt deleted file mode 100644 index deb74779..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeCustomMetadataDetails.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Custom metadata details information. - * - * @see Adobe XDM CustomMetadataDetails Schema - */ -data class AdobeCustomMetadataDetails( - // The name of the custom field. - val name: String? = null, - // The value of the custom field. - val value: String? = null -) - diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeErrorDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeErrorDetails.kt deleted file mode 100644 index 93676e31..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeErrorDetails.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Error details information. - * - * @see Adobe XDM ErrorDetails Schema - */ -data class AdobeErrorDetails( - // The error ID. - val name: String, - // The error source. - val source: ErrorSource -) - -enum class ErrorSource { - PLAYER, - EXTERNAL -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeImplementationDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeImplementationDetails.kt deleted file mode 100644 index 211915b2..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeImplementationDetails.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Details about the SDK, library, or service used in an application or web page implementation of a service. - * - * @see Adobe XDM ImplementationDetails Schema - */ -data class AdobeImplementationDetails( - // The environment of the implementation - val environment: AdobeEnvironment? = null, - // SDK or endpoint identifier. All SDKs or endpoints are identified through a URI, including extensions. - val name: String? = null, - // The version identifier of the API, e.g h.18. - val version: String? = null -) - -/** - * The environment of the implementation. - */ -enum class AdobeEnvironment { - BROWSER, - APP, - SERVER, - IOT -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeMediaDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeMediaDetails.kt deleted file mode 100644 index 9a7c8c20..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeMediaDetails.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Media details information. - * - * @see Adobe XDM MediaDetails Schema - */ -data class AdobeMediaDetails( - // If the content is live, the playhead must be the current second of the day, 0 <= playhead < 86400. - // If the content is recorded, the playhead must be the current second of content, 0 <= playhead < content length. - val playhead: Int? = null, - // Identifies an instance of a content stream unique to an individual playback. - val sessionID: String? = null, - // Session details information related to the experience event. - val sessionDetails: AdobeSessionDetails? = null, - // Advertising details information related to the experience event. - val advertisingDetails: AdobeAdvertisingDetails? = null, - // Advertising Pod details information - val advertisingPodDetails: AdobeAdvertisingPodDetails? = null, - // Chapter details information related to the experience event. - val chapterDetails: AdobeChapterDetails? = null, - // Error details information related to the experience event. - val errorDetails: AdobeErrorDetails? = null, - // Qoe data details information related to the experience event. - val qoeDataDetails: AdobeQoeDataDetails? = null, - // The list of states start. - val statesStart: List? = null, - // The list of states end. - val statesEnd: List? = null, - // The list of states. - val states: List? = null, - // The list of custom metadata. - val customMetadata: List? = null -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobePlayerStateData.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobePlayerStateData.kt deleted file mode 100644 index 86728f59..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobePlayerStateData.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Player state data information. - * - * @see Adobe XDM PlayerStateData Schema - */ -data class AdobePlayerStateData( - // The name of the player state. - val name: String, - // Whether or not the player state is set on that state. - val isSet: Boolean? = null, - // The number of times that player state was set on the stream. - val count: Int? = null, - // The total duration of that player state. - val time: Int? = null -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeQoeDataDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeQoeDataDetails.kt deleted file mode 100644 index 066815b5..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeQoeDataDetails.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Qoe data details information related to the experience event. - * - * @see Adobe XDM QoeDataDetails Schema - */ -data class AdobeQoeDataDetails( - // The average bitrate (in kbps). The value is predefined buckets at 100kbps intervals. - val bitrateAverage: String? = null, - // The bitrate value (in kbps). - val bitrate: Int? = null, - // The average bitrate (in kbps, integer). - val bitrateAverageBucket: Int? = null, - // The number of streams in which bitrate changes occurred. - val hasBitrateChangeImpactedStreams: Boolean? = null, - // The number of bitrate changes. - val bitrateChangeCount: Int? = null, - // The number of streams in which frames were dropped. - val hasDroppedFrameImpactedStreams: Boolean? = null, - // The number of frames dropped during playback of the main content. - val droppedFrames: Int? = null, - // The number of times a user quit the video before its start. - val isDroppedBeforeStart: Boolean? = null, - // The current value of the stream frame-rate (in frames per second). - val framesPerSecond: Int? = null, - // Describes the duration (in seconds) passed between video load and start. - val timeToStart: Int? = null, - // The number of streams impacted by buffering. - val hasBufferImpactedStreams: Boolean? = null, - // The number of buffer events. - val bufferCount: Int? = null, - // The total amount of time, in seconds, spent buffering. - val bufferTime: Int? = null, - // The number of streams in which an error event occurred. - val hasErrorImpactedStreams: Boolean? = null, - // The number of errors that occurred. - val errorCount: Int? = null, - // The number of streams in which a stalled event occurred. - val hasStallImpactedStreams: Boolean? = null, - // The number of times the playback was stalled during a playback session. - val stallCount: Int? = null, - // The total time (seconds) the playback was stalled during a playback session. - val stallTime: Int? = null, - // The unique error IDs generated by the player SDK. - val playerSdkErrors: List? = null, - // The unique error IDs from any external source, e.g., CDN errors. - val externalErrors: List? = null, - // The unique error IDs generated by Media SDK during playback. - val mediaSdkErrors: List? = null -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeSessionDetails.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeSessionDetails.kt deleted file mode 100644 index f2c77212..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/AdobeSessionDetails.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Session details information related to the experience event. - * - * @see Adobe XDM SessionDetails Schema - */ -data class AdobeSessionDetails( - // This identifies an instance of a content stream unique to an individual playback. - val ID: String? = null, - // The number of ads started during the playback. - val adCount: Int? = null, - // The type of ad loaded as defined by each customer's internal representation. - val adLoad: String? = null, - // The name of the album that the music recording or video belongs to. - val album: String? = null, - // The SDK version used by the player. - val appVersion: String? = null, - // The name of the album artist or group performing the music recording or video. - val artist: String? = null, - // This is the unique identifier for the content of the media asset. - val assetID: String? = null, - // Name of the media author. - val author: String? = null, - // Describes the average content time spent for a specific media item. - val averageMinuteAudience: Int? = null, - // Distribution channel from where the content was played. - val channel: String, - // The number of chapters started during the playback. - val chapterCount: Int? = null, - // The type of the stream delivery. - val contentType: ContentType, - // A property that defines the time of the day when the content was broadcast or played. - val dayPart: String? = null, - // The number of the episode. - val episode: String? = null, - // The estimated number of video or audio streams per each individual content. - val estimatedStreams: Int? = null, - // The type of feed, which can either represent actual feed-related data such as EAST HD or SD, or the source of the feed like a URL. - val feed: String? = null, - // The date when the content first aired on television. - val firstAirDate: String? = null, - // The date when the content first aired on any digital channel or platform. - val firstDigitalDate: String? = null, - // This is the “friendly” (human-readable) name of the content. - val friendlyName: String? = null, - // Type or grouping of content as defined by content producer. - val genre: String? = null, - // Indicates if one or more pauses occurred during the playback of a single media item. - val hasPauseImpactedStreams: Boolean? = null, - // Indicates that the playhead passed the 10% marker of media based on stream length. - val hasProgress10: Boolean? = null, - // Indicates that the playhead passed the 25% marker of media based on stream length. - val hasProgress25: Boolean? = null, - // Indicates that the playhead passed the 50% marker of media based on stream length. - val hasProgress50: Boolean? = null, - // Indicates that the playhead passed the 75% marker of media based on stream length. - val hasProgress75: Boolean? = null, - // Indicates that the playhead passed the 95% marker of media based on stream length. - val hasProgress95: Boolean? = null, - // Marks each playback that was resumed after more than 30 minutes of buffer, pause, or stall period. - val hasResume: Boolean? = null, - // Indicates when at least one frame, not necessarily the first has been viewed. - val hasSegmentView: Boolean? = null, - // The user has been authorized via Adobe authentication. - val isAuthorized: Boolean? = null, - // Indicates if a timed media asset was watched to completion. - val isCompleted: Boolean? = null, - // The stream was played locally on the device after being downloaded. - val isDownloaded: Boolean? = null, - // Set to true when the hit is federated. - val isFederated: Boolean? = null, - // First frame of media is consumed. - val isPlayed: Boolean? = null, - // Load event for the media. - val isViewed: Boolean? = null, - // Name of the record label. - val label: String? = null, - // Clip Length/Runtime - This is the maximum length (or duration) of the content being consumed (in seconds). - val length: Int, - // MVPD provided via Adobe authentication. - val mvpd: String? = null, - // Content ID of the content, which can be used to tie back to other industry / CMS IDs. - val name: String, - // The network/channel name. - val network: String? = null, - // Creator of the content. - val originator: String? = null, - // The number of pause periods that occurred during playback. - val pauseCount: Int? = null, - // Describes the duration in seconds in which playback was paused by the user. - val pauseTime: Int? = null, - // Name of the content player. - val playerName: String, - // Name of the audio content publisher. - val publisher: String? = null, - // Rating as defined by TV Parental Guidelines. - val rating: String? = null, - // The season number the show belongs to. - val season: String? = null, - // Indicates the amount of time, in seconds, that passed between the user's last known interaction and the moment the session was closed. - val secondsSinceLastCall: Int? = null, - // The interval that describes the part of the content that has been viewed in minutes. - val segment: String? = null, - // Program/Series Name. - val show: String? = null, - // The type of content for example, trailer or full episode. - val showType: String? = null, - // The radio station name on which the audio is played. - val station: String? = null, - // Format of the stream (HD, SD). - val streamFormat: String? = null, - // The type of the media stream. - val streamType: StreamType? = null, - // Sums the event duration (in seconds) for all events of type PLAY on the main content. - val timePlayed: Int? = null, - // Describes the total amount of time spent by a user on a specific timed media asset, which includes time spent watching ads. - val totalTimePlayed: Int? = null, - // Describes the sum of the unique intervals seen by a user on a timed media asset. - val uniqueTimePlayed: Int? = null -) - -/** - * The type of the stream delivery. - */ -enum class ContentType { - // Video-on-demand - VOD, - // Live streaming - LIVE, - // Linear playback of the media asset - LINEAR, - // User-generated content - UGC, - // Downloaded video-on-demand - DVOD, - // Radio show - RADIO, - // Audio podcast - PODCAST, - // Audiobook - AUDIOBOOK, - // Song - SONG -} - -/** - * The type of the media stream. - */ -enum class StreamType { - VIDEO, - AUDIO -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/EventType.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/EventType.kt deleted file mode 100644 index 881013e1..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/EventType.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -/** - * Enum representing the types of media events. - */ -enum class EventType(val value: String) { - SESSION_START("media.sessionStart"), - PLAY("media.play"), - PING("media.ping"), - BITRATE_CHANGE("media.bitrateChange"), - BUFFER_START("media.bufferStart"), - PAUSE_START("media.pauseStart"), - AD_BREAK_START("media.adBreakStart"), - AD_START("media.adStart"), - AD_COMPLETE("media.adComplete"), - AD_SKIP("media.adSkip"), - AD_BREAK_COMPLETE("media.adBreakComplete"), - CHAPTER_START("media.chapterStart"), - CHAPTER_SKIP("media.chapterSkip"), - CHAPTER_COMPLETE("media.chapterComplete"), - ERROR("media.error"), - SESSION_END("media.sessionEnd"), - SESSION_COMPLETE("media.sessionComplete"), - STATES_UPDATE("media.statesUpdate") -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt deleted file mode 100644 index dc462da4..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/MediaEdgeAPI.kt +++ /dev/null @@ -1,186 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -import com.adobe.marketing.mobile.edge.media.Media -import com.adobe.marketing.mobile.edge.media.MediaConstants -import kotlin.Any -import kotlin.String - -private const val MAIN_PING_INTERVAL = 10000L -private const val AD_PING_INTERVAL = 1000L - -class MediaEdgeAPI(channel: String) { - /** - * https://developer.adobe.com/client-sdks/edge/media-for-edge-network/api-reference/#createtrackerwithconfig - */ - private val tracker = Media.createTracker( - mutableMapOf( - MediaConstants.TrackerConfig.CHANNEL to channel, - MediaConstants.TrackerConfig.MAIN_PING_INTERVAL to MAIN_PING_INTERVAL, - MediaConstants.TrackerConfig.AD_PING_INTERVAL to AD_PING_INTERVAL, - ) - ) - - var hasSession = false - private set - - fun hasSessionStarted(): Boolean = hasSession - - fun reset() { - hasSession = false - } - - fun play() { - tracker.trackPlay() - } - - fun pause() { - tracker.trackPause() - } - - fun error(errorDetails: AdobeErrorDetails) { - tracker.trackError(errorDetails.name) - } - - fun bufferStart() { - tracker.trackEvent(Media.Event.BufferStart, null, null) - } - - fun bufferComplete() { - tracker.trackEvent(Media.Event.BufferComplete, null, null) - } - - fun seekComplete() { - tracker.trackEvent(Media.Event.SeekStart, null, null) - } - - fun seekStart() { - tracker.trackEvent(Media.Event.SeekComplete, null, null) - } - - /** - * Tracks the completion of the media playback session. Call this method only when the media has - * been completely viewed. If the viewing session is ended before the media is completely viewed, - * use trackSessionEnd instead. - */ - fun sessionComplete() { - tracker.trackComplete() - } - - /** - * Tracks the end of a media playback session. Call this method when the viewing session ends, - * even if the user has not viewed the media to completion. If the media is viewed to completion, - * use trackComplete instead. - */ - fun sessionEnd() { - tracker.trackSessionEnd() - } - - /** - * Provides the current media playhead value to the MediaTracker instance. For accurate tracking, - * call this method every time the playhead value changes. If the player does not notify playhead - * value changes, call this method once every second with the most recent playhead value. - */ - fun updateCurrentPlayhead(playhead: Double?) { - tracker.updateCurrentPlayhead(sanitisePlayhead(playhead)) - } - - fun statesUpdate() { - // TODO -// Media.createStateObject(MediaConstants.PlayerState.FULLSCREEN) - } - - fun bitrateChange(qoeDataDetails: AdobeQoeDataDetails) { - tracker.updateQoEObject( - Media.createQoEObject( - qoeDataDetails.bitrate ?: 0, - qoeDataDetails.timeToStart ?: 0, - qoeDataDetails.framesPerSecond ?: 0, - qoeDataDetails.droppedFrames ?: 0 - ) - ) - } - - fun chapterSkip() { - tracker.trackEvent(Media.Event.ChapterSkip, null, null) - } - - fun chapterStart( - chapterDetails: AdobeChapterDetails, - customMetadata: List? = null - ) { - tracker.trackEvent( - Media.Event.ChapterStart, - Media.createChapterObject( - chapterDetails.friendlyName ?: "", - chapterDetails.index, - chapterDetails.length, - chapterDetails.offset - ), - customMetadata?.associate { it.name to (it.value ?: "") } - ) - } - - fun chapterComplete() { - tracker.trackEvent(Media.Event.ChapterComplete, null, null) - } - - fun adBreakStart(advertisingPodDetails: AdobeAdvertisingPodDetails) { - tracker.trackEvent( - Media.Event.AdBreakStart, - Media.createAdBreakObject( - advertisingPodDetails.friendlyName ?: "", - advertisingPodDetails.index, - advertisingPodDetails.offset - ), - null - ) - } - - fun adBreakComplete() { - tracker.trackEvent(Media.Event.AdBreakComplete, null, null) - } - - fun adStart( - advertisingDetails: AdobeAdvertisingDetails, - customMetadata: List? = null, - ) { - tracker.trackEvent( - Media.Event.AdStart, - Media.createAdObject( - advertisingDetails.name, - advertisingDetails.id ?: "", - advertisingDetails.podPosition, - advertisingDetails.length - ), - customMetadata?.associate { it.name to (it.value ?: "") } - ) - } - - fun adSkip() { - tracker.trackEvent(Media.Event.AdSkip, null, null) - } - - fun adComplete() { - tracker.trackEvent(Media.Event.AdComplete, null, null) - } - - fun startSession( - sessionDetails: AdobeSessionDetails, - customMetadata: List? = null - ) { - tracker.trackSessionStart( - Media.createMediaObject( - sessionDetails.friendlyName ?: "N/A", - sessionDetails.assetID ?: "N/A", - sessionDetails.length, - sessionDetails.contentType.name, - when (sessionDetails.streamType) { - StreamType.AUDIO ->Media.MediaType.Audio - else -> Media.MediaType.Video - } - ), - customMetadata?.associate { it.name to (it.value ?: "") } - ) - hasSession = true - } -} diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/PathToEventTypeMap.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/PathToEventTypeMap.kt deleted file mode 100644 index 50079396..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/PathToEventTypeMap.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -val pathToEventTypeMap: Map = mapOf( - "/adBreakComplete" to EventType.AD_BREAK_COMPLETE, - "/adBreakStart" to EventType.AD_BREAK_START, - "/adComplete" to EventType.AD_COMPLETE, - "/adSkip" to EventType.AD_SKIP, - "/adStart" to EventType.AD_START, - "/bitrateChange" to EventType.BITRATE_CHANGE, - "/bufferStart" to EventType.BUFFER_START, - "/chapterComplete" to EventType.CHAPTER_COMPLETE, - "/chapterSkip" to EventType.CHAPTER_SKIP, - "/chapterStart" to EventType.CHAPTER_START, - "/error" to EventType.ERROR, - "/pauseStart" to EventType.PAUSE_START, - "/ping" to EventType.PING, - "/play" to EventType.PLAY, - "/sessionComplete" to EventType.SESSION_COMPLETE, - "/sessionEnd" to EventType.SESSION_END, - "/sessionStart" to EventType.SESSION_START, - "/statesUpdate" to EventType.STATES_UPDATE, -) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/Utils.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/Utils.kt deleted file mode 100644 index 5bcacfcc..00000000 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/api/Utils.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.theoplayer.reactnative.adobe.edge.api - -import android.os.Build -import android.os.LocaleList -import java.util.Locale - -fun buildUserAgent(): String { - val locale: Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - LocaleList.getDefault().get(0) - } else { - Locale.getDefault() - } - // Example: Mozilla/5.0 (Linux; U; Android 7.1.2; en-US; AFTN Build/NS6296) - return "Mozilla/5.0 (Linux; U; Android ${Build.VERSION.RELEASE}; $locale; ${Build.MODEL} Build/${Build.ID})" -} - -fun sanitisePlayhead(playhead: Double?): Int { - if (playhead == null) { - return 0 - } - if (playhead == Double.POSITIVE_INFINITY) { - // If content is live, the playhead must be the current second of the day. - val now = System.currentTimeMillis() - return ((now / 1000) % 86400).toInt() - } - return playhead.toInt() -} diff --git a/adobe-edge/src/api/AdobeEdgeMobileConfig.ts b/adobe-edge/src/api/AdobeEdgeMobileConfig.ts index 594a79b3..8c619e55 100644 --- a/adobe-edge/src/api/AdobeEdgeMobileConfig.ts +++ b/adobe-edge/src/api/AdobeEdgeMobileConfig.ts @@ -3,18 +3,9 @@ export interface AdobeEdgeMobileConfig { * The unique environment ID that the SDK uses to retrieve your configuration. * This ID is generated when an app configuration is created and published to a given environment. * The app is first launched and then the SDK retrieves and uses this Adobe-hosted configuration. - * {@link https://developer.adobe.com/client-sdks/previous-versions/documentation/mobile-core/configuration/} + * {@link https://developer.adobe.com/client-sdks/edge/media-for-edge-network/#initialize-adobe-experience-platform-sdk-with-media-for-edge-network-extension} */ - appId?: string; - - /** - * On Android, instead of providing an `appId` that is used to retrieve the Adobe-hosted configuration, - * load the configuration from a JSON configuration file in the app's Assets folder. - * - * {@link https://developer.adobe.com/client-sdks/home/base/mobile-core/api-reference/#setloglevel} - * @platform android - */ - configAsset?: string; + environmentId?: string; /** * The debugEnabled property allows you to enable or disable debugging using SDK code. From 4a979aed6537b0302936129504e5f03ab985d94e Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 12 Dec 2025 14:52:05 +0100 Subject: [PATCH 16/70] Fix empty chapter name --- .../com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt index c2708e5d..b7159256 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -271,7 +271,7 @@ class AdobeEdgeHandler( tracker.trackEvent( Media.Event.ChapterStart, Media.createChapterObject( - chapterCue.id, + chapterCue.id.ifEmpty { "NA" }, chapterCue.id.toIntOrNull() ?: 0, chapterCue.endTime.toInt(), (chapterCue.endTime - chapterCue.startTime).toInt() From 776b3f6d2836f7388bb836e24cba3f6d7b09cfa1 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 12 Dec 2025 18:17:12 +0100 Subject: [PATCH 17/70] Create tracker based AdobeEdgeConnector for iOS --- .../ios/Connector/AdobeEdgeHandler.swift | 419 ++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 adobe-edge/ios/Connector/AdobeEdgeHandler.swift diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift new file mode 100644 index 00000000..44b2ab21 --- /dev/null +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -0,0 +1,419 @@ +// +// AdobeEdgeConnector.swift +// + +import Foundation +import THEOplayerSDK +import UIKit +import AEPServices +import AEPCore +import AEPEdgeMedia + +let CONTENT_PING_INTERVAL = 10.0 +let AD_PING_INTERVAL = 1.0 + +class AdobeEdgeHandler { + private weak var player: THEOplayer? + private var trackerConfig: [String:String] + private var sessionInProgress = false + private var adBreakPodIndex: Int = 0 + private var adPodPosition: Int = 0 + private var isPlayingAd = false + private var customMetadata: [String:String] = [:] + private var currentChapter: TextTrackCue? = nil + private var loggingMode: LogLevel = .error + private var tracker: MediaTracker = Media.createTracker() + + // MARK: Player Listeners + private var playingListener: THEOplayerSDK.EventListener? + private var pauseListener: THEOplayerSDK.EventListener? + private var endedListener: THEOplayerSDK.EventListener? + private var timeUpdateListener: THEOplayerSDK.EventListener? + private var waitingListener: THEOplayerSDK.EventListener? + private var seekingListener: THEOplayerSDK.EventListener? + private var seekedListener: THEOplayerSDK.EventListener? + private var sourceChangeListener: THEOplayerSDK.EventListener? + private var errorListener: THEOplayerSDK.EventListener? + private var addTextTrackListener: THEOplayerSDK.EventListener? + private var removeTextTrackListener: THEOplayerSDK.EventListener? + private var addVideoTrackListener: THEOplayerSDK.EventListener? + + // MARK: Ad Listeners + private var adBreakBeginListener: THEOplayerSDK.EventListener? + private var adBreakEndListener: THEOplayerSDK.EventListener? + private var adBeginListener: THEOplayerSDK.EventListener? + private var adEndListener: THEOplayerSDK.EventListener? + private var adSkipListener: THEOplayerSDK.EventListener? + + // MARK: MediaTrack listeners + private var videoAddTrackListener: THEOplayerSDK.EventListener? + private var videoRemoveTrackListener: THEOplayerSDK.EventListener? + private var videoQualityChangeListeners: [Int:THEOplayerSDK.EventListener] = [:] + private var audioQualityChangeListeners: [Int:THEOplayerSDK.EventListener] = [:] + + private func logDebug(_ text: String) { + if self.loggingMode >= .debug { + print("[AdobeEdgeConnector]", text) + } + } + + init(player: THEOplayer, trackerConfig: [String:String]) { + self.player = player + self.trackerConfig = trackerConfig + self.addEventListeners() + self.logDebug("Initialized Connector.") + } + + func setLoggingMode(_ debug: LogLevel) -> Void { + self.loggingMode = debug + MobileCore.setLogLevel(debug) + } + + func updateMetadata(_ metadata: [String:String]) -> Void { + self.customMetadata.merge(metadata) { (_, new) in new } + } + + func setError(_ errorId: String) -> Void { + self.tracker.trackError(errorId: errorId) + } + + func stopAndStartNewSession(_ metadata: [String:String]) -> Void { + guard let player = self.player else {return} + self.maybeEndSession() + self.updateMetadata(metadata) + self.maybeStartSession() + player.paused ? self.onPause() : self.onPlaying() + } + + func addEventListeners() -> Void { + guard let player = self.player else {return} + + // Player events + self.playingListener = player.addEventListener(type: PlayerEventTypes.PLAYING, listener: self.handlePlaying(event:)) + self.pauseListener = player.addEventListener(type: PlayerEventTypes.PAUSE, listener: self.handlePause(event:)) + self.endedListener = player.addEventListener(type: PlayerEventTypes.ENDED, listener: self.handleEnded(event:)) + self.waitingListener = player.addEventListener(type: PlayerEventTypes.WAITING, listener: self.handleWaiting(event:)) + self.seekingListener = player.addEventListener(type: PlayerEventTypes.SEEKING, listener: self.handleSeeking(event:)) + self.seekedListener = player.addEventListener(type: PlayerEventTypes.SEEKED, listener: self.handleSeeked(event:)) + self.timeUpdateListener = player.addEventListener(type: PlayerEventTypes.TIME_UPDATE, listener: self.handleTimeUpdate(event:)) + self.sourceChangeListener = player.addEventListener(type: PlayerEventTypes.SOURCE_CHANGE, listener: self.handleSourceChange(event:)) + self.errorListener = player.addEventListener(type: PlayerEventTypes.ERROR, listener: self.handleError(event:)) + + // Bitrate + self.videoAddTrackListener = player.videoTracks.addEventListener(type: VideoTrackListEventTypes.ADD_TRACK) { [weak self] event in + guard let welf = self else { return } + if let videoTrack = event.track as? VideoTrack { + // start listening for qualityChange events on this track + welf.videoQualityChangeListeners[videoTrack.uid] = videoTrack.addEventListener(type: MediaTrackEventTypes.ACTIVE_QUALITY_CHANGED, listener: welf.handleActiveQualityChange(event:)) + } + } + self.videoRemoveTrackListener = player.videoTracks.addEventListener(type: VideoTrackListEventTypes.REMOVE_TRACK) { [weak self] event in + guard let welf = self else { return } + if let videoTrack = event.track as? VideoTrack { + if let videoQualityChangeListener = welf.videoQualityChangeListeners.removeValue(forKey: videoTrack.uid) { + videoTrack.removeEventListener(type: MediaTrackEventTypes.ACTIVE_QUALITY_CHANGED, listener: videoQualityChangeListener) + } + } + } + + // Ad events + self.adBreakBeginListener = player.ads.addEventListener(type: AdsEventTypes.AD_BREAK_BEGIN, listener: self.handleAdBreakBegin(event:)) + self.adBreakEndListener = player.ads.addEventListener(type: AdsEventTypes.AD_BREAK_END, listener: self.handleAdBreakEnd(event:)) + self.adBeginListener = player.ads.addEventListener(type: AdsEventTypes.AD_BEGIN, listener: self.handleAdBegin(event:)) + self.adEndListener = player.ads.addEventListener(type: AdsEventTypes.AD_END, listener: self.handleAdEnd(event:)) + self.adSkipListener = player.ads.addEventListener(type: AdsEventTypes.AD_SKIP, listener: self.handleAdSkip(event:)) + + self.logDebug("Listeners attached.") + } + + func removeEventListeners() -> Void { + guard let player = self.player else {return} + + // Player events + if let playingListener = self.playingListener { + player.removeEventListener(type: PlayerEventTypes.PLAYING, listener: playingListener) + } + if let pauseListener = self.pauseListener { + player.removeEventListener(type: PlayerEventTypes.PAUSE, listener: pauseListener) + } + if let endedListener = self.endedListener { + player.removeEventListener(type: PlayerEventTypes.ENDED, listener: endedListener) + } + if let waitingListener = self.waitingListener { + player.removeEventListener(type: PlayerEventTypes.WAITING, listener: waitingListener) + } + if let seekingListener = self.seekingListener { + player.removeEventListener(type: PlayerEventTypes.SEEKING, listener: seekingListener) + } + if let seekedListener = self.seekedListener { + player.removeEventListener(type: PlayerEventTypes.SEEKED, listener: seekedListener) + } + if let timeUpdateListener = self.timeUpdateListener { + player.removeEventListener(type: PlayerEventTypes.TIME_UPDATE, listener: timeUpdateListener) + } + if let sourceChangeListener = self.sourceChangeListener { + player.removeEventListener(type: PlayerEventTypes.SOURCE_CHANGE, listener: sourceChangeListener) + } + if let errorListener = self.errorListener { + player.removeEventListener(type: PlayerEventTypes.ERROR, listener: errorListener) + } + + // Bitrate + let videoTrackCount = player.videoTracks.count + if videoTrackCount > 0 { + for i in 0.. Void { + guard let player = self.player else { return } + self.logDebug("onWaiting") + self.tracker.trackEvent(event: MediaEvent.BufferStart, info: nil, metadata: nil) + } + + func handleSeeking(event: SeekingEvent) -> Void { + guard let player = self.player else { return } + self.logDebug("onSeeking") + self.tracker.trackEvent(event: MediaEvent.SeekStart, info: nil, metadata: nil) + } + + func handleSeeked(event: SeekedEvent) -> Void { + guard let player = self.player else { return } + self.logDebug("onSeeked") + self.tracker.trackEvent(event: MediaEvent.SeekComplete, info: nil, metadata: nil) + } + + func handleEnded(event: EndedEvent) -> Void { + guard let player = self.player else { return } + self.logDebug("onEnded") + self.tracker.trackComplete() + self.reset() + } + + func handleSourceChange(event: SourceChangeEvent) -> Void { + self.logDebug("onSourceChange") + self.maybeEndSession() + } + + func handleActiveQualityChange(event: ActiveQualityChangedEvent) -> Void { + guard let player = self.player else { return } + self.logDebug("onActiveQualityChange") + var bitrate = 0 + if let activeTrack = self.activeTrack(tracks: player.videoTracks) { + bitrate = activeTrack.activeQuality?.bandwidth ?? 0 + } + if let qoe = Media.createQoEObjectWith(bitrate: bitrate, startupTime: 0, fps: 0, droppedFrames: 0) { + self.tracker.updateQoEObject(qoe: qoe) + } + } + + private func activeTrack(tracks: THEOplayerSDK.MediaTrackList) -> MediaTrack? { + guard tracks.count > 0 else { + return nil; + } + var track: MediaTrack? + for i in 0...tracks.count-1 { + track = tracks.get(i) + if (track != nil && track!.enabled) { + return track + } + } + return nil; + } + + func handleError(event: ErrorEvent) -> Void { + guard let player = self.player else { return } + self.logDebug("onError") + var errorCodeString = "-1" + if let errorCodeValue = event.errorObject?.code.rawValue as? Int32 { + errorCodeString = String(errorCodeValue) + } + self.tracker.trackError(errorId: errorCodeString) + } + + func handleAdBreakBegin(event: AdBreakBeginEvent) -> Void { + guard let player = self.player else { return } + self.logDebug("onAdBreakBegin") + self.isPlayingAd = true + let currentAdBreakTimeOffset = event.ad?.timeOffset ?? 0 + let breakIndex = currentAdBreakTimeOffset < 0 ? -1 : (currentAdBreakTimeOffset == 0 ? 0 : self.adBreakPodIndex + 1) + let adBreakObject = Media.createAdBreakObjectWith(name: "NA", position: breakIndex, startTime: currentAdBreakTimeOffset) + self.tracker.trackEvent(event: MediaEvent.AdBreakStart, info: adBreakObject, metadata: nil) + if (breakIndex > self.adBreakPodIndex) { + self.adBreakPodIndex += 1 + } + } + + func handleAdBreakEnd(event: AdBreakEndEvent) -> Void { + guard let player = self.player else { return } + self.logDebug("onAdBreakEnd") + self.isPlayingAd = false + self.adPodPosition = 1 + self.tracker.trackEvent(event: MediaEvent.AdBreakComplete, info: nil, metadata: nil) + } + + func handleAdBegin(event: AdBeginEvent) -> Void { + guard let player = self.player else { return } + self.logDebug("onAdBegin") + let duration = event.ad?.duration ?? 0 + let adObject = Media.createAdObjectWith(name: "NA", id: "NA", position: self.adPodPosition, length: duration) + self.tracker.trackEvent(event: MediaEvent.AdBreakStart, info: adObject, metadata: nil) + + self.adPodPosition += 1 + } + + func handleAdEnd(event: AdEndEvent) -> Void { + guard let player = self.player else { return } + self.logDebug("onAdEnd") + self.tracker.trackEvent(event: MediaEvent.AdComplete, info: nil, metadata: nil) + } + + func handleAdSkip(event: AdSkipEvent) -> Void { + guard let player = self.player else { return } + self.logDebug("onAdSkip") + self.tracker.trackEvent(event: MediaEvent.AdSkip, info: nil, metadata: nil) + } + + private func maybeEndSession() -> Void { + guard let player = self.player else { return } + self.logDebug("maybeEndSession") + if (self.sessionInProgress) { + self.tracker.trackSessionEnd() + } + self.reset() + } + + /** + * Start a new session, but only if: + * - no existing session has is in progress; + * - the player has a valid source; + * - no ad is playing, otherwise the ad's media duration will be picked up; + * - the player's content media duration is known. + * + * @param mediaLength + * @private + */ + func maybeStartSession(mediaLengthSec: Double? = nil) -> Void { + guard let player = self.player else { return } + + let mediaLength = AdobeUtils.sanitiseContentLength(mediaLengthSec) + let hasValidSource = player.source != nil + let hasValidDuration = player.duration != nil && !(player.duration!.isNaN) + self.logDebug("maybeStartSession - mediaLength: \(mediaLength)") + self.logDebug("maybeStartSession - hasValidSource: \(hasValidSource)") + self.logDebug("maybeStartSession - hasValidDuration: \(hasValidDuration)") + self.logDebug("maybeStartSession - sessionInProgress: \(self.sessionInProgress)") + self.logDebug("maybeStartSession - isPlayingAd: \(self.isPlayingAd)") + + guard !sessionInProgress else { + self.logDebug("maybeStartSession - NOT started: already in progress") + return + } + + guard !isPlayingAd else { + self.logDebug("maybeStartSession - NOT started: playing ad") + return + } + + guard hasValidSource && hasValidDuration else { + let reason = hasValidSource ? "duration" : "source" + self.logDebug("maybeStartSession - NOT started: invalid \(reason)") + return + } + + + if let mediaObject = Media.createMediaObjectWith( + name: player.source?.metadata?.title ?? "N/A", + id: "N/A", + length: mediaLength, + streamType: self.getStreamType(), + mediaType: MediaType.Video + ) { + self.tracker.trackSessionStart(info: mediaObject, metadata: self.customMetadata) + + sessionInProgress = true + self.logDebug("maybeStartSession - STARTED") + } + } + + private func reset() -> Void { + self.logDebug("reset") + self.adBreakPodIndex = 0 + self.adPodPosition = 1 + self.isPlayingAd = false + self.sessionInProgress = false + self.currentChapter = nil + } + + func destroy() -> Void { + self.logDebug("destroy.") + self.removeEventListeners() + self.maybeEndSession() + } + + private func getStreamType() -> String { + if let player = self.player, + let duration = player.duration { + if duration != Double.infinity { + return MediaConstants.StreamType.VOD + } + } + return MediaConstants.StreamType.LIVE + } +} From 669b7b112869050a10681c87de365ee164b7730f Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 12 Dec 2025 18:17:43 +0100 Subject: [PATCH 18/70] Update AdobeEdgeConnector for iOS to use new handler --- .../ios/Connector/AdobeEdgeConnector.swift | 445 +----------------- adobe-edge/ios/Connector/AdobeUtils.swift | 74 ++- 2 files changed, 49 insertions(+), 470 deletions(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeConnector.swift b/adobe-edge/ios/Connector/AdobeEdgeConnector.swift index 6ccb9f82..9c4644df 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeConnector.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeConnector.swift @@ -5,450 +5,31 @@ import Foundation import THEOplayerSDK import UIKit - -let CONTENT_PING_INTERVAL = 10.0 -let AD_PING_INTERVAL = 1.0 +import AEPServices class AdobeEdgeConnector { - private weak var player: THEOplayer? - private var baseUrl: String - private var configId: String - private var debug: Bool = false - private var debugSessionId: String? - - private var mediaApi: MediaEdgeAPI - - private var customMetadata: [AdobeCustomMetadataDetails] = [] - private var sessionInProgress = false - private var isPlayingAd = false - private var pingTimer: Timer? - private var adBreakPodIndex: Int = 0 - private var adPodPosition: Int = 0 - private var currentChapter: TextTrackCue? = nil - - // MARK: Player Listeners - private var playingListener: EventListener? - private var pauseListener: EventListener? - private var endedListener: EventListener? - private var waitingListener: EventListener? - private var sourceChangeListener: EventListener? - private var loadedMetadataListener: EventListener? - private var errorListener: EventListener? - private var addTextTrackListener: EventListener? - private var removeTextTrackListener: EventListener? - private var addVideoTrackListener: EventListener? - - // MARK: Ad Listeners - private var adBreakBeginListener: EventListener? - private var adBreakEndListener: EventListener? - private var adBeginListener: EventListener? - private var adEndListener: EventListener? - - // MARK: MediaTrack listeners - private var videoAddTrackListener: EventListener? - private var videoRemoveTrackListener: EventListener? - private var videoQualityChangeListeners: [Int:EventListener] = [:] - private var audioQualityChangeListeners: [Int:EventListener] = [:] - - init(player: THEOplayer, baseUrl: String, configId: String, userAgent: String?, debug: Bool, debugSessionId: String?) { - self.player = player - self.baseUrl = baseUrl - self.configId = configId - self.debug = debug - self.debugSessionId = debugSessionId - self.mediaApi = MediaEdgeAPI(baseUrl: baseUrl, configId: configId, userAgent: userAgent ?? AdobeUtils.buildUserAgent(), debugSessionId: debugSessionId) - - self.addEventListeners() - - self.log("Connector initialized.") + private var handler: AdobeEdgeHandler + init(player: THEOplayer, trackerConfig: [String:String]) { + self.handler = AdobeEdgeHandler(player: player, trackerConfig: trackerConfig) } - func setDebug(_ debug: Bool) -> Void { - self.debug = debug + func updateMetadata(_ metadata: [String:String]) -> Void { + self.handler.updateMetadata(metadata) } - func setDebugSessionId(_ debugId: String?) -> Void { - self.mediaApi.setDebugSessionId(debugId: debugId) + func stopAndStartNewSession(_ metadata: [String:String]) -> Void { + self.handler.stopAndStartNewSession(metadata) } - func updateMetadata(_ metadata: [AdobeCustomMetadataDetails]) -> Void { - self.customMetadata.append(contentsOf: metadata) + func setLoggingMode(_ debug: LogLevel) -> Void { + self.handler.setLoggingMode(debug) } - func setError(_ errorDetails: AdobeErrorDetails) -> Void { - guard let player = self.player else {return} - self.mediaApi.error(playhead: player.currentTime, errorDetails: errorDetails) - } - - func stopAndStartNewSession(_ metadata: [AdobeCustomMetadataDetails]) -> Void { - self.maybeEndSession() - self.updateMetadata(metadata) - self.maybeStartSession() - if let player = self.player { - if player.paused { - self.onPause(event: PauseEvent(currentTime: player.currentTime)) - } else { - self.onPlaying(event: PlayingEvent(currentTime: player.currentTime)) - } - } + func setError(_ errorId: String) -> Void { + self.handler.setError(errorId) } func destroy() -> Void { - self.log("destroy.") - self.removeEventListeners() - self.maybeEndSession() - } - - func addEventListeners() -> Void { - guard let player = self.player else {return} - - // Player events - self.playingListener = player.addEventListener(type: PlayerEventTypes.PLAYING, listener: self.onPlaying(event:)) - self.pauseListener = player.addEventListener(type: PlayerEventTypes.PAUSE, listener: self.onPause(event:)) - self.endedListener = player.addEventListener(type: PlayerEventTypes.ENDED, listener: self.onEnded(event:)) - self.waitingListener = player.addEventListener(type: PlayerEventTypes.WAITING, listener: self.onWaiting(event:)) - self.sourceChangeListener = player.addEventListener(type: PlayerEventTypes.SOURCE_CHANGE, listener: self.onSourceChange(event:)) - self.loadedMetadataListener = player.addEventListener(type: PlayerEventTypes.LOADED_META_DATA, listener: self.onLoadedMetadata(event:)) - self.errorListener = player.addEventListener(type: PlayerEventTypes.ERROR, listener: self.onError(event:)) - - // Bitrate - self.videoAddTrackListener = player.videoTracks.addEventListener(type: VideoTrackListEventTypes.ADD_TRACK) { [weak self] event in - guard let welf = self else { return } - if let videoTrack = event.track as? VideoTrack { - // start listening for qualityChange events on this track - welf.videoQualityChangeListeners[videoTrack.uid] = videoTrack.addEventListener(type: MediaTrackEventTypes.ACTIVE_QUALITY_CHANGED, listener: welf.onActiveQualityChange(event:)) - } - } - self.videoRemoveTrackListener = player.videoTracks.addEventListener(type: VideoTrackListEventTypes.REMOVE_TRACK) { [weak self] event in - guard let welf = self else { return } - if let videoTrack = event.track as? VideoTrack { - if let videoQualityChangeListener = welf.videoQualityChangeListeners.removeValue(forKey: videoTrack.uid) { - videoTrack.removeEventListener(type: MediaTrackEventTypes.ACTIVE_QUALITY_CHANGED, listener: videoQualityChangeListener) - } - } - } - - // Ad events - self.adBreakBeginListener = player.ads.addEventListener(type: AdsEventTypes.AD_BREAK_BEGIN, listener: self.onAdBreakBegin(event:)) - self.adBreakEndListener = player.ads.addEventListener(type: AdsEventTypes.AD_BREAK_END, listener: self.onAdBreakEnd(event:)) - self.adBeginListener = player.ads.addEventListener(type: AdsEventTypes.AD_BEGIN, listener: self.onAdBegin(event:)) - self.adEndListener = player.ads.addEventListener(type: AdsEventTypes.AD_END, listener: self.onAdEnd(event:)) - - self.log("Listeners attached.") - } - - func removeEventListeners() -> Void { - guard let player = self.player else {return} - - // Player events - if let playingListener = self.playingListener { - player.removeEventListener(type: PlayerEventTypes.PLAYING, listener: playingListener) - } - if let pauseListener = self.pauseListener { - player.removeEventListener(type: PlayerEventTypes.PAUSE, listener: pauseListener) - } - if let endedListener = self.endedListener { - player.removeEventListener(type: PlayerEventTypes.ENDED, listener: endedListener) - } - if let waitingListener = self.waitingListener { - player.removeEventListener(type: PlayerEventTypes.WAITING, listener: waitingListener) - } - if let sourceChangeListener = self.sourceChangeListener { - player.removeEventListener(type: PlayerEventTypes.SOURCE_CHANGE, listener: sourceChangeListener) - } - if let loadedMetadataListener = self.loadedMetadataListener { - player.removeEventListener(type: PlayerEventTypes.LOADED_META_DATA, listener: loadedMetadataListener) - } - if let errorListener = self.errorListener { - player.removeEventListener(type: PlayerEventTypes.ERROR, listener: errorListener) - } - - // Bitrate - let videoTrackCount = player.videoTracks.count - if videoTrackCount > 0 { - for i in 0.. Void { - self.log("onLoadedMetadata triggered.") - self.maybeStartSession() - } - - func onPlaying(event: PlayingEvent) -> Void { - guard let player = self.player else {return} - self.log("onPlayingEvent triggered.") - //self.maybeStartSession(mediaLengthSec: player.duration) - self.mediaApi.play(playhead: player.currentTime) - } - - func onPause(event: PauseEvent) -> Void { - guard let player = self.player else {return} - self.log("onPause triggered.") - self.mediaApi.pause(playhead: player.currentTime) - } - - func onWaiting(event: WaitingEvent) -> Void { - guard let player = self.player else {return} - self.log("onWaiting triggered.") - self.mediaApi.bufferStart(playhead: player.currentTime) - } - - func onEnded(event: EndedEvent) -> Void { - guard let player = self.player else {return} - self.log("onEnded triggered.") - self.mediaApi.sessionComplete(playhead: player.currentTime) - self.reset() - } - - func onSourceChange(event: SourceChangeEvent) -> Void { - self.log("onSourceChange triggered.") - self.maybeEndSession() - } - - func onActiveQualityChange(event: ActiveQualityChangedEvent) -> Void { - guard let player = self.player else {return} - self.log("onActiveQualityChange triggered.") - var bitrate = 0 - if let activeTrack = self.activeTrack(tracks: player.videoTracks) { - bitrate = activeTrack.activeQuality?.bandwidth ?? 0 - } - self.mediaApi.bitrateChange( - playhead: player.currentTime, qoeDataDetails: AdobeQoeDataDetails(bitrate: bitrate) - ) - } - - private func activeTrack(tracks: THEOplayerSDK.MediaTrackList) -> MediaTrack? { - guard tracks.count > 0 else { - return nil; - } - var track: MediaTrack? - for i in 0...tracks.count-1 { - track = tracks.get(i) - if (track != nil && track!.enabled) { - return track - } - } - return nil; - } - - func onError(event: ErrorEvent) -> Void { - guard let player = self.player else {return} - self.log("onError triggered.") - var errorCodeString = "-1" - if let errorCodeValue = event.errorObject?.code.rawValue as? Int32 { - errorCodeString = String(errorCodeValue) - } - self.mediaApi.error( - playhead: player.currentTime, - errorDetails: AdobeErrorDetails( - name: errorCodeString, - source: .player - ) - ) - } - - func onAdBreakBegin(event: AdBreakBeginEvent) -> Void { - guard let player = self.player else {return} - self.log("onAdBreakBegin triggered.") - self.isPlayingAd = true - self.startPinger(AD_PING_INTERVAL) - let podDetails = AdobeUtils.calculateAdvertisingPodDetails(adBreak: event.ad, lastPodIndex: self.adBreakPodIndex) - self.mediaApi.adBreakStart(playhead: player.currentTime, advertisingPodDetails: podDetails) - if (podDetails.index > adBreakPodIndex) { - adBreakPodIndex += 1 - } - } - - func onAdBreakEnd(event: AdBreakEndEvent) -> Void { - guard let player = self.player else {return} - self.log("onAdBreakEnd triggered.") - self.isPlayingAd = false - self.adPodPosition = 1 - self.startPinger(CONTENT_PING_INTERVAL) - self.mediaApi.adBreakComplete(playhead: player.currentTime) - } - - func onAdBegin(event: AdBeginEvent) -> Void { - guard let player = self.player else {return} - self.log("onAdBegin triggered.") - self.mediaApi.adStart( - playhead: player.currentTime, - advertisingDetails: AdobeUtils.calculateAdvertisingDetails(ad: event.ad, podPosition: self.adPodPosition), - customMetadata: self.customMetadata - ) - self.adPodPosition += 1 - } - - func onAdEnd(event: AdEndEvent) -> Void { - guard let player = self.player else {return} - self.log("onAdEnd triggered.") - self.mediaApi.adComplete(playhead: player.currentTime) - } - - /** - * Start a new session, but only if: - * - no existing session has is in progress; - * - the player has a valid source; - * - no ad is playing, otherwise the ad's media duration will be picked up; - * - the player's content media duration is known. - * - * @param mediaLength - * @private - */ - func maybeStartSession(mediaLengthSec: Double? = nil) -> Void { - guard let player = self.player else { - return - } - - let mediaLength = self.getContentLength() - let hasValidSource = player.source != nil - let hasValidDuration = player.duration != nil && !(player.duration!.isNaN) - self.log("maybeStartSession - mediaLength: \(mediaLength)") - self.log("maybeStartSession - hasValidSource: \(hasValidSource)") - self.log("maybeStartSession - hasValidDuration: \(hasValidDuration)") - self.log("maybeStartSession - sessionInProgress: \(self.sessionInProgress)") - self.log("maybeStartSession - isPlayingAd: \(self.isPlayingAd)") - - guard !sessionInProgress else { - self.log("maybeStartSession - NOT started: already in progress") - return - } - - guard !isPlayingAd else { - self.log("maybeStartSession - NOT started: playing ad") - return - } - - guard hasValidSource && hasValidDuration else { - let reason = hasValidSource ? "duration" : "source" - self.log("maybeStartSession - NOT started: invalid \(reason)") - return - } - - Task { @MainActor in - - let sessionDetails = AdobeSessionDetails( - ID: "N/A", - channel: "N/A", - contentType: getContentType(), - length: mediaLength, - name: player.source?.metadata?.title ?? "N/A", - playerName: "THEOplayer" - ) - - self.log("maybeStartSession - call startSession") - await self.mediaApi.startSession(sessionDetails: sessionDetails, customMetadata: self.customMetadata) - - guard self.mediaApi.hasSessionStarted() else { - self.log("maybeStartSession - session was not started") - return - } - - sessionInProgress = true - self.log("maybeStartSession - STARTED sessionId: \(self.mediaApi.sessionId ?? "")") - - if !isPlayingAd { - startPinger(CONTENT_PING_INTERVAL) - } else { - startPinger(AD_PING_INTERVAL) - } - } - } - - private func maybeEndSession() -> Void { - guard let player = self.player else {return} - self.log("maybeEndSession") - if (self.mediaApi.hasSessionStarted()) { - self.mediaApi.sessionEnd(playhead: player.currentTime) - } - reset() - } - - private func reset() -> Void { - self.log("reset"); - self.mediaApi.reset() - self.adBreakPodIndex = 0; - self.adPodPosition = 1; - self.isPlayingAd = false; - self.sessionInProgress = false; - self.pingTimer?.invalidate(); - self.pingTimer = nil - self.currentChapter = nil; - } - - private func startPinger(_ interval: Double) { - DispatchQueue.main.async { - self.pingTimer?.invalidate() - self.pingTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true, block: { t in - guard let player = self.player else {return} - self.mediaApi.ping(playhead: player.currentTime) - }) - self.log("Pinger started with interval \(interval).") - } - } - - /** - * Get the current media length in seconds. - * - * - In case of a live stream, set it to 24h. - * - * @param mediaLengthInMSec optional mediaLengthInMSec provided by a player event. - * @private - */ - private func getContentLength(mediaLengthInSec: Double? = nil) -> Int { - if let mediaLength = mediaLengthInSec { - return mediaLength == Double.infinity ? 86400 : Int(mediaLength) - } - - if let player = self.player, - let duration = player.duration { - return duration == Double.infinity ? 86400 : Int(duration) - } - - return 86400 - } - - private func getContentType() -> ContentType { - if let player = self.player, - let duration = player.duration { - if duration != Double.infinity { - return ContentType.vod - } - } - return ContentType.live - } - - private func log(_ text: String) { - if self.debug { - print("[adobe-edge-connector]", text) - } + self.handler.destroy() } } diff --git a/adobe-edge/ios/Connector/AdobeUtils.swift b/adobe-edge/ios/Connector/AdobeUtils.swift index 56302032..e261bbef 100644 --- a/adobe-edge/ios/Connector/AdobeUtils.swift +++ b/adobe-edge/ios/Connector/AdobeUtils.swift @@ -4,52 +4,50 @@ import Foundation import THEOplayerSDK class AdobeUtils { - class func calculateAdvertisingPodDetails(adBreak: AdBreak?, lastPodIndex: Int) -> AdobeAdvertisingPodDetails { - let currentAdBreakTimeOffset = adBreak?.timeOffset ?? 0 - - let index: Int - if currentAdBreakTimeOffset == 0 { - index = 0 - } else if currentAdBreakTimeOffset < 0 { - index = -1 - } else { - index = lastPodIndex + 1 + class func toAdobeCustomMetadataDetails(_ array: [[String: Any]]) -> [String: String] { + var result = [String: String]() + for item in array { + let stringsItem = AdobeUtils.toStringMap(item) + if let name = stringsItem["name"], let value = stringsItem["value"] { + result[name] = value + } } - - return AdobeAdvertisingPodDetails( - index: index, - offset: currentAdBreakTimeOffset - ) + return result } - class func calculateAdvertisingDetails(ad: Ad?, podPosition: Int) -> AdobeAdvertisingDetails { - let length = (ad as? LinearAd)?.duration ?? 0 + class func toStringMap(_ map: [String: Any]) -> [String: String] { + var result = [String: String]() + for (key, value) in map { + if let stringValue = value as? String { + result[key] = stringValue + } else if let optionalValue = value as? CustomStringConvertible { + // Convert other types (Int, Bool, Double, etc.) to String + result[key] = String(describing: optionalValue) + } else { + // If value is nil or not convertible, use empty string + result[key] = "" + } + } - return AdobeAdvertisingDetails( - length: length, - name: "NA", - playerName: "THEOplayer", - podPosition: podPosition - ) + return result } - class func buildUserAgent() -> String { - let device = UIDevice.current - let model = device.model - let osVersion = device.systemVersion.replacingOccurrences(of: ".", with: "_") - let locale = (UserDefaults.standard.array(forKey: "AppleLanguages")?.first as? String) ?? Locale.current.identifier - let userAgent = "Mozilla/5.0 (\(model); CPU OS \(osVersion) like Mac OS X; \(locale))" - return userAgent + class func sanitisePlayhead(_ playhead: Double?) -> Int { + guard let playhead = playhead else { + return 0 + } + + if playhead == Double.infinity { + // If content is live, the playhead must be the current second of the day. + let now = Date().timeIntervalSince1970 + return Int(now.truncatingRemainder(dividingBy: 86400)) + } + + return Int(playhead) } - class func toDictionary(_ value: T) -> [String: Any] { - let encoder = JSONEncoder() - guard let data = try? encoder.encode(value), - let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), - let dictionary = jsonObject as? [String: Any] else { - return [:] - } - return dictionary + class func sanitiseContentLength(_ mediaLength: Double?) -> Int { + mediaLength == .infinity ? 86400 : Int(mediaLength ?? 0) } } From 2bf7526040b8cc152f783f871333fce83700bfde Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 12 Dec 2025 18:17:58 +0100 Subject: [PATCH 19/70] Add Adobe pod dependencies --- adobe-edge/react-native-theoplayer-adobe-edge.podspec | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/adobe-edge/react-native-theoplayer-adobe-edge.podspec b/adobe-edge/react-native-theoplayer-adobe-edge.podspec index 55439cde..d5f087ff 100644 --- a/adobe-edge/react-native-theoplayer-adobe-edge.podspec +++ b/adobe-edge/react-native-theoplayer-adobe-edge.podspec @@ -17,7 +17,11 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,swift}" s.dependency "react-native-theoplayer" - + s.dependency 'AEPCore', '~> 5.0' + s.dependency 'AEPEdge', '~> 5.0' + s.dependency 'AEPEdgeIdentity', '~> 5.0' + s.dependency 'AEPEdgeMedia', '~> 5.0' + # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. if respond_to?(:install_modules_dependencies, true) From b2423df85fa47400c1aebe5736ace23876c28dc6 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 12 Dec 2025 18:18:23 +0100 Subject: [PATCH 20/70] Rework connector API to match with new connector. --- .../TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift | 71 ++++++------------- adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m | 10 +-- 2 files changed, 22 insertions(+), 59 deletions(-) diff --git a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift index 5d797cbb..12ed67b8 100644 --- a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift +++ b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift @@ -2,6 +2,8 @@ import Foundation import UIKit import react_native_theoplayer import THEOplayerSDK +import AEPCore +import AEPServices @objc(THEOplayerAdobeEdgeRCTAdobeEdgeAPI) class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { @@ -19,20 +21,20 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { return false } - @objc(initialize:baseUrl:configId:userAgent:debug:debugSessionId:) - func initialize(_ node: NSNumber, baseUrl: String, configId: String, userAgent: String?, debug: Bool = false, debugSessionId: String?) -> Void { - self.debug = debug + @objc(initialize:config:) + func initialize(_ node: NSNumber, config: NSDictionary) -> Void { log("initialize triggered.") + let environmentId = config["environmentId"] as? String ?? "MissingEnvironmentID" + let debugEnabled = config["debugEnabled"] as? Bool ?? false + + MobileCore.setLogLevel(.debug) + MobileCore.initialize(appId: environmentId) + self.debug = debugEnabled + DispatchQueue.main.async { if let view = self.view(for: node), let player = view.player { - let connector = AdobeEdgeConnector( - player: player, - baseUrl: baseUrl, - configId: configId, - userAgent: userAgent, - debug: debug, - debugSessionId: debugSessionId - ) + let trackerConfig: [String: String] = AdobeUtils.toStringMap(config as? [String: Any] ?? [:]) + let connector = AdobeEdgeConnector(player: player, trackerConfig: trackerConfig) self.connectors[node] = connector self.log("added connector to view \(node)") } else { @@ -47,17 +49,7 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { self.debug = debug DispatchQueue.main.async { if let connector = self.connectors[node] { - connector.setDebug(debug) - } - } - } - - @objc(setDebugSessionId:debugSessionId:) - func setDebugSessionId(_ node: NSNumber, debugSessionId: String?) -> Void { - log("setDebugSessionId triggered.") - DispatchQueue.main.async { - if let connector = self.connectors[node] { - connector.setDebugSessionId(debugSessionId) + connector.setLoggingMode(debug ? .debug : .error) } } } @@ -66,17 +58,9 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { func updateMetadata(_ node: NSNumber, metadata: [NSDictionary]) -> Void { log("updateMetadata triggered.") DispatchQueue.main.async { - if let connector = self.connectors[node] { - connector.updateMetadata( - metadata.flatMap { dict in - dict.map { (key, value) in - AdobeCustomMetadataDetails( - name: key as? String, - value: "\(value)" - ) - } - } - ) + if let connector = self.connectors[node], + let newMetadata = metadata as? [[String: Any]] { + connector.updateMetadata(AdobeUtils.toAdobeCustomMetadataDetails(newMetadata)) } } } @@ -86,12 +70,7 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { log("setError triggered.") DispatchQueue.main.async { if let connector = self.connectors[node] { - connector.setError( - AdobeErrorDetails( - name: errorDetails["name"] as? String ?? "", - source: (errorDetails["source"] as? String ?? "") == "player" ? .player : .external - ) - ) + connector.setError(errorDetails["name"] as? String ?? "NA") } } } @@ -100,17 +79,9 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { func stopAndStartNewSession(_ node: NSNumber, customMetadataDetails: [NSDictionary]) -> Void { log("stopAndStartNewSession triggered") DispatchQueue.main.async { - if let connector = self.connectors[node] { - connector.stopAndStartNewSession( - customMetadataDetails.flatMap { dict in - dict.map { (key, value) in - AdobeCustomMetadataDetails( - name: key as? String, - value: "\(value)" - ) - } - } - ) + if let connector = self.connectors[node], + let newMetadata = customMetadataDetails as? [[String: Any]] { + connector.stopAndStartNewSession(AdobeUtils.toAdobeCustomMetadataDetails(newMetadata)) } } } diff --git a/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m b/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m index 7a2e6354..87026e24 100644 --- a/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m +++ b/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m @@ -8,19 +8,11 @@ @interface RCT_EXTERN_REMAP_MODULE(AdobeEdgeModule, THEOplayerAdobeEdgeRCTAdobeEdgeAPI, NSObject) RCT_EXTERN_METHOD(initialize:(nonnull NSNumber *)node - baseUrl:(nonnull NSString *)baseUrl - configId:(nonnull NSString *)configId - userAgent:(nullable NSString*)userAgent - debug:(BOOL)debug - debugSessionId:(nullable NSString*)debugSessionId - ) + config:(NSDictionary)config) RCT_EXTERN_METHOD(setDebug:(nonnull NSNumber *)node debug:(BOOL)debug) -RCT_EXTERN_METHOD(setDebugSessionId:(nonnull NSNumber *)node - debugSessionId:(nullable NSString*)debugSessionId) - RCT_EXTERN_METHOD(updateMetadata:(nonnull NSNumber *)node metadata:(NSArray *)metadata) From d2caf4c638fb4ed30e36f6f5483df3476355d8fc Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 12 Dec 2025 14:52:05 +0100 Subject: [PATCH 21/70] Fix empty chapter name --- .../com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt index b7159256..4c2beef1 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -272,7 +272,7 @@ class AdobeEdgeHandler( Media.Event.ChapterStart, Media.createChapterObject( chapterCue.id.ifEmpty { "NA" }, - chapterCue.id.toIntOrNull() ?: 0, + chapterCue.id.toIntOrNull() ?: 1, chapterCue.endTime.toInt(), (chapterCue.endTime - chapterCue.startTime).toInt() ), From 93ebd11ea73a6a000c6f39bb0867f74a51c50d68 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 12 Dec 2025 18:20:58 +0100 Subject: [PATCH 22/70] Drop unused types --- .../api/AdobeAdvertisingDetails.swift | 48 --- .../api/AdobeAdvertisingPodDetails.swift | 18 - .../Connector/api/AdobeChapterDetails.swift | 30 -- .../api/AdobeCustomMetadataDetails.swift | 12 - .../ios/Connector/api/AdobeErrorDetails.swift | 17 - .../api/AdobeImplementationDetails.swift | 23 - .../ios/Connector/api/AdobeMediaDetails.swift | 43 -- .../Connector/api/AdobePlayerStateData.swift | 18 - .../Connector/api/AdobeQoeDataDetails.swift | 69 --- .../Connector/api/AdobeSessionDetails.swift | 193 --------- adobe-edge/ios/Connector/api/EventType.swift | 23 - .../ios/Connector/api/MediaEdgeAPI.swift | 397 ------------------ 12 files changed, 891 deletions(-) delete mode 100644 adobe-edge/ios/Connector/api/AdobeAdvertisingDetails.swift delete mode 100644 adobe-edge/ios/Connector/api/AdobeAdvertisingPodDetails.swift delete mode 100644 adobe-edge/ios/Connector/api/AdobeChapterDetails.swift delete mode 100644 adobe-edge/ios/Connector/api/AdobeCustomMetadataDetails.swift delete mode 100644 adobe-edge/ios/Connector/api/AdobeErrorDetails.swift delete mode 100644 adobe-edge/ios/Connector/api/AdobeImplementationDetails.swift delete mode 100644 adobe-edge/ios/Connector/api/AdobeMediaDetails.swift delete mode 100644 adobe-edge/ios/Connector/api/AdobePlayerStateData.swift delete mode 100644 adobe-edge/ios/Connector/api/AdobeQoeDataDetails.swift delete mode 100644 adobe-edge/ios/Connector/api/AdobeSessionDetails.swift delete mode 100644 adobe-edge/ios/Connector/api/EventType.swift delete mode 100644 adobe-edge/ios/Connector/api/MediaEdgeAPI.swift diff --git a/adobe-edge/ios/Connector/api/AdobeAdvertisingDetails.swift b/adobe-edge/ios/Connector/api/AdobeAdvertisingDetails.swift deleted file mode 100644 index 6e16bee5..00000000 --- a/adobe-edge/ios/Connector/api/AdobeAdvertisingDetails.swift +++ /dev/null @@ -1,48 +0,0 @@ -// AdobeAdvertisingDetails.swift - -/// Advertising details information. -/// -/// - SeeAlso: [Adobe XDM AdvertisingDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/advertisingdetails.schema.md) -struct AdobeAdvertisingDetails: Codable { - /// ID of the ad. Any integer and/or varter combination. - var id: String? - - /// Company/Brand whose product is featured in the ad. - var advertiser: String? - - /// ID of the ad campaign. - var campaignID: String? - - /// ID of the ad creative. - var creativeID: String? - - /// URL of the ad creative. - var creativeURL: String? - - /// Ad is compvared. - var isCompvared: Bool? - - /// Ad is started. - var isStarted: Bool? - - /// Length of video ad in seconds. - var length: Int - - /// Friendly name of the ad. In reporting, “Ad Name” is the classification and “Ad Name (variable)” is the eVar. - var name: String - - /// Placement ID of the ad. - var placementID: String? - - /// The name of the player responsible for rendering the ad. - var playerName: String - - /// The index of the ad inside the parent ad start, for example, the first ad has index 0 and the second ad has index 1. - var podPosition: Int - - /// ID of the ad site. - var siteID: String? - - /// The total amount of time, in seconds, spent watching the ad (i.e., the number of seconds played). - var timePlayed: Int? -} diff --git a/adobe-edge/ios/Connector/api/AdobeAdvertisingPodDetails.swift b/adobe-edge/ios/Connector/api/AdobeAdvertisingPodDetails.swift deleted file mode 100644 index 568452e9..00000000 --- a/adobe-edge/ios/Connector/api/AdobeAdvertisingPodDetails.swift +++ /dev/null @@ -1,18 +0,0 @@ -// AdobeAdvertisingPodDetails.swift - -/// Advertising Pod details information. -/// -/// - SeeAlso: [Adobe XDM AdvertisingPodDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/advertisingpoddetails.schema.md) -struct AdobeAdvertisingPodDetails: Codable { - /// The ID of the ad break. - var id: String? - - /// The friendly name of the Ad Break. - var friendlyName: String? - - /// The index of the ad inside the parent ad break start, for example, the first ad has index 0 and the second ad has index 1. - var index: Int - - /// The offset of the ad break inside the content, in seconds. - var offset: Int -} diff --git a/adobe-edge/ios/Connector/api/AdobeChapterDetails.swift b/adobe-edge/ios/Connector/api/AdobeChapterDetails.swift deleted file mode 100644 index cfe87e5e..00000000 --- a/adobe-edge/ios/Connector/api/AdobeChapterDetails.swift +++ /dev/null @@ -1,30 +0,0 @@ -// AdobeChapterDetails.swift - -/// Chapter details information. -/// -/// - SeeAlso: [Adobe XDM ChapterDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/chapterdetails.schema.md) -struct AdobeChapterDetails: Codable { - /// The ID of the chapter. - var id: String? - - /// The friendly name of the chapter. - var friendlyName: String? - - /// The position (index, integer) of the chapter inside the content. - var index: Int - - /// Chapter is compvared. - var isCompvared: Bool? - - /// Chapter is started. - var isStarted: Bool? - - /// The length of the chapter, in seconds. - var length: Int - - /// The offset of the chapter inside the content (in seconds) from the start. - var offset: Int - - /// The time spent on the chapter, in seconds. - var timePlayed: Int? -} diff --git a/adobe-edge/ios/Connector/api/AdobeCustomMetadataDetails.swift b/adobe-edge/ios/Connector/api/AdobeCustomMetadataDetails.swift deleted file mode 100644 index 9997b81c..00000000 --- a/adobe-edge/ios/Connector/api/AdobeCustomMetadataDetails.swift +++ /dev/null @@ -1,12 +0,0 @@ -// AdobeCustomMetadataDetails.swift - -/// Custom metadata details information. -/// -/// - SeeAlso: [Adobe XDM CustomMetadataDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/custommetadatadetails.schema.md) -struct AdobeCustomMetadataDetails: Codable { - /// The name of the custom field. - let name: String? - - /// The value of the custom field. - let value: String? -} diff --git a/adobe-edge/ios/Connector/api/AdobeErrorDetails.swift b/adobe-edge/ios/Connector/api/AdobeErrorDetails.swift deleted file mode 100644 index dd7ca0b1..00000000 --- a/adobe-edge/ios/Connector/api/AdobeErrorDetails.swift +++ /dev/null @@ -1,17 +0,0 @@ -// AdobeErrorDetails.swift - -/// Error details information. -/// -/// - SeeAlso: [Adobe XDM ErrorDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/errordetails.schema.md) -struct AdobeErrorDetails: Codable { - /// The error ID. - let name: String - - /// The error source. - let source: ErrorSource -} - -enum ErrorSource: String, Codable { - case player = "player" - case external = "external" -} diff --git a/adobe-edge/ios/Connector/api/AdobeImplementationDetails.swift b/adobe-edge/ios/Connector/api/AdobeImplementationDetails.swift deleted file mode 100644 index 68ed1f9d..00000000 --- a/adobe-edge/ios/Connector/api/AdobeImplementationDetails.swift +++ /dev/null @@ -1,23 +0,0 @@ -// AdobeImplementationDetails.swift - -/// Details about the SDK, library, or service used in an application or web page implementation of a service. -/// -/// - SeeAlso: [Adobe XDM ImplementationDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/implementationdetails.schema.md) -struct AdobeImplementationDetails: Codable { - /// The environment of the implementation - let environment: AdobeEnvironment? - - /// SDK or endpoint identifier. All SDKs or endpoints are identified through a URI, including extensions. - let name: String? - - /// The version identifier of the API, e.g h.18. - let version: String? -} - -/// The environment of the implementation. -enum AdobeEnvironment: String, Codable { - case browser = "BROWSER" - case app = "APP" - case server = "SERVER" - case iot = "IOT" -} diff --git a/adobe-edge/ios/Connector/api/AdobeMediaDetails.swift b/adobe-edge/ios/Connector/api/AdobeMediaDetails.swift deleted file mode 100644 index 9a670e35..00000000 --- a/adobe-edge/ios/Connector/api/AdobeMediaDetails.swift +++ /dev/null @@ -1,43 +0,0 @@ -// AdobeMediaDetails.swift - -/// Media details information. -/// -/// - SeeAlso: [Adobe XDM MediaDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/mediadetails.schema.md) -struct AdobeMediaDetails: Codable { - /// If the content is live, the playhead must be the current second of the day, 0 <= playhead < 86400. - /// If the content is recorded, the playhead must be the current second of content, 0 <= playhead < content length. - let playhead: Int? - - /// Identifies an instance of a content stream unique to an individual playback. - let sessionID: String? - - /// Session details information related to the experience event. - let sessionDetails: AdobeSessionDetails? - - /// Advertising details information related to the experience event. - let advertisingDetails: AdobeAdvertisingDetails? - - /// Advertising Pod details information - let advertisingPodDetails: AdobeAdvertisingPodDetails? - - /// Chapter details information related to the experience event. - let chapterDetails: AdobeChapterDetails? - - /// Error details information related to the experience event. - let errorDetails: AdobeErrorDetails? - - /// Qoe data details information related to the experience event. - let qoeDataDetails: AdobeQoeDataDetails? - - /// The list of states start. - let statesStart: [AdobePlayerStateData]? - - /// The list of states end. - let statesEnd: [AdobePlayerStateData]? - - /// The list of states. - let states: [AdobePlayerStateData]? - - /// The list of custom metadata. - let customMetadata: [AdobeCustomMetadataDetails]? -} diff --git a/adobe-edge/ios/Connector/api/AdobePlayerStateData.swift b/adobe-edge/ios/Connector/api/AdobePlayerStateData.swift deleted file mode 100644 index 8c1ff97c..00000000 --- a/adobe-edge/ios/Connector/api/AdobePlayerStateData.swift +++ /dev/null @@ -1,18 +0,0 @@ -// AdobePlayerStateData.swift - -/// Player state data information. -/// -/// - SeeAlso: [Adobe XDM PlayerStateData Schema](https://github.com/adobe/xdm/blob/master/components/datatypes/playerstatedata.schema.json) -struct AdobePlayerStateData: Codable { - /// The name of the player state. - let name: String - - /// Whether or not the player state is set on that state. - let isSet: Bool? - - /// The number of times that player state was set on the stream. - let count: Int? - - /// The total duration of that player state. - let time: Int? -} diff --git a/adobe-edge/ios/Connector/api/AdobeQoeDataDetails.swift b/adobe-edge/ios/Connector/api/AdobeQoeDataDetails.swift deleted file mode 100644 index 0086a3ea..00000000 --- a/adobe-edge/ios/Connector/api/AdobeQoeDataDetails.swift +++ /dev/null @@ -1,69 +0,0 @@ -// AdobeQoeDataDetails.swift - -/// Qoe data details information related to the experience event. -/// -/// - SeeAlso: [Adobe XDM QoeDataDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/qoedatadetails.schema.md) -struct AdobeQoeDataDetails: Codable { - /// The average bitrate (in kbps). The value is predefined buckets at 100kbps intervals. - var bitrateAverage: String? - - /// The bitrate value (in kbps). - var bitrate: Int? - - /// The average bitrate (in kbps, integer). - var bitrateAverageBucket: Int? - - /// The number of streams in which bitrate changes occurred. - var hasBitrateChangeImpactedStreams: Bool? - - /// The number of bitrate changes. - var bitrateChangeCount: Int? - - /// The number of streams in which frames were dropped. - var hasDroppedFrameImpactedStreams: Bool? - - /// The number of frames dropped during playback of the main content. - var droppedFrames: Int? - - /// The number of times a user quit the video before its start. - var isDroppedBeforeStart: Bool? - - /// The current value of the stream frame-rate (in frames per second). - var framesPerSecond: Int? - - /// Describes the duration (in seconds) passed between video load and start. - var timeToStart: Int? - - /// The number of streams impacted by buffering. - var hasBufferImpactedStreams: Bool? - - /// The number of buffer events. - var bufferCount: Int? - - /// The total amount of time, in seconds, spent buffering. - var bufferTime: Int? - - /// The number of streams in which an error event occurred. - var hasErrorImpactedStreams: Bool? - - /// The number of errors that occurred. - var errorCount: Int? - - /// The number of streams in which a stalled event occurred. - var hasStallImpactedStreams: Bool? - - /// The number of times the playback was stalled during a playback session. - var stallCount: Int? - - /// The total time (seconds) the playback was stalled during a playback session. - var stallTime: Int? - - /// The unique error IDs generated by the player SDK. - var playerSdkErrors: [String]? - - /// The unique error IDs from any external source, e.g., CDN errors. - var externalErrors: [String]? - - /// The unique error IDs generated by Media SDK during playback. - var mediaSdkErrors: [String]? -} diff --git a/adobe-edge/ios/Connector/api/AdobeSessionDetails.swift b/adobe-edge/ios/Connector/api/AdobeSessionDetails.swift deleted file mode 100644 index 19d48186..00000000 --- a/adobe-edge/ios/Connector/api/AdobeSessionDetails.swift +++ /dev/null @@ -1,193 +0,0 @@ -// AdobeSessionDetails.swift - -/// Session details information related to the experience event. -/// -/// - SeeAlso: [Adobe XDM SessionDetails Schema](https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/sessiondetails.schema.md) -struct AdobeSessionDetails: Codable { - /// This identifies an instance of a content stream unique to an individual playback. - var ID: String? - - /// The number of ads started during the playback. - var adCount: Int? - - /// The type of ad loaded as defined by each customer's internal representation. - var adLoad: String? - - /// The name of the album that the music recording or video belongs to. - var album: String? - - /// The SDK version used by the player. - var appVersion: String? - - /// The name of the album artist or group performing the music recording or video. - var artist: String? - - /// This is the unique identifier for the content of the media asset. - var assetID: String? - - /// Name of the media author. - var author: String? - - /// Describes the average content time spent for a specific media item. - var averageMinuteAudience: Int? - - /// Distribution channel from where the content was played. - var channel: String - - /// The number of chapters started during the playback. - var chapterCount: Int? - - /// The type of the stream delivery. - var contentType: ContentType - - /// A property that defines the time of the day when the content was broadcast or played. - var dayPart: String? - - /// The number of the episode. - var episode: String? - - /// The estimated number of video or audio streams per each individual content. - var estimatedStreams: Int? - - /// The type of feed, which can either represent actual feed-related data such as EAST HD or SD, or the source of the feed like a URL. - var feed: String? - - /// The date when the content first aired on television. - var firstAirDate: String? - - /// The date when the content first aired on any digital channel or platform. - var firstDigitalDate: String? - - /// This is the "friendly" (human-readable) name of the content. - var friendlyName: String? - - /// Type or grouping of content as defined by content producer. - var genre: String? - - /// Indicates if one or more pauses occurred during the playback of a single media item. - var hasPauseImpactedStreams: Bool? - - /// Indicates that the playhead passed the 10% marker of media based on stream length. - var hasProgress10: Bool? - - /// Indicates that the playhead passed the 25% marker of media based on stream length. - var hasProgress25: Bool? - - /// Indicates that the playhead passed the 50% marker of media based on stream length. - var hasProgress50: Bool? - - /// Indicates that the playhead passed the 75% marker of media based on stream length. - var hasProgress75: Bool? - - /// Indicates that the playhead passed the 95% marker of media based on stream length. - var hasProgress95: Bool? - - /// Marks each playback that was resumed after more than 30 minutes of buffer, pause, or stall period. - var hasResume: Bool? - - /// Indicates when at least one frame, not necessarily the first has been viewed. - var hasSegmentView: Bool? - - /// The user has been authorized via Adobe authentication. - var isAuthorized: Bool? - - /// Indicates if a timed media asset was watched to compvarion. - var isCompvared: Bool? - - /// The stream was played locally on the device after being downloaded. - var isDownloaded: Bool? - - /// Set to true when the hit is federated. - var isFederated: Bool? - - /// First frame of media is consumed. - var isPlayed: Bool? - - /// Load event for the media. - var isViewed: Bool? - - /// Name of the record label. - var label: String? - - /// Clip Length/Runtime - This is the maximum length (or duration) of the content being consumed (in seconds). - var length: Int - - /// MVPD provided via Adobe authentication. - var mvpd: String? - - /// Content ID of the content, which can be used to tie back to other industry / CMS IDs. - var name: String - - /// The network/channel name. - var network: String? - - /// Creator of the content. - var originator: String? - - /// The number of pause periods that occurred during playback. - var pauseCount: Int? - - /// Describes the duration in seconds in which playback was paused by the user. - var pauseTime: Int? - - /// Name of the content player. - var playerName: String - - /// Name of the audio content publisher. - var publisher: String? - - /// Rating as defined by TV Parental Guidelines. - var rating: String? - - /// The season number the show belongs to. - var season: String? - - /// Indicates the amount of time, in seconds, that passed between the user's last known interaction and the moment the session was closed. - var secondsSinceLastCall: Int? - - /// The interval that describes the part of the content that has been viewed in minutes. - var segment: String? - - /// Program/Series Name. - var show: String? - - /// The type of content for example, trailer or full episode. - var showType: String? - - /// The radio station name on which the audio is played. - var station: String? - - /// Format of the stream (HD, SD). - var streamFormat: String? - - /// The type of the media stream. - var streamType: StreamType? - - /// Sums the event duration (in seconds) for all events of type PLAY on the main content. - var timePlayed: Int? - - /// Describes the total amount of time spent by a user on a specific timed media asset, which includes time spent watching ads. - var totalTimePlayed: Int? - - /// Describes the sum of the unique intervals seen by a user on a timed media asset. - var uniqueTimePlayed: Int? -} - -/// The type of the stream delivery. -enum ContentType: String, Codable { - case vod = "VOD" - case live = "LIVE" - case linear = "LINEAR" - case ugc = "UGC" - case dvod = "DVOD" - case radio = "RADIO" - case podcast = "PODCAST" - case audiobook = "AUDIOBOOK" - case song = "SONG" -} - -/// The type of the media stream. -enum StreamType: String, Codable { - case video = "VIDEO" - case audio = "AUDIO" -} diff --git a/adobe-edge/ios/Connector/api/EventType.swift b/adobe-edge/ios/Connector/api/EventType.swift deleted file mode 100644 index 6484bc84..00000000 --- a/adobe-edge/ios/Connector/api/EventType.swift +++ /dev/null @@ -1,23 +0,0 @@ -// EventType.swift - -/// Enum representing the types of media events. -enum EventType: String, Codable { - case sessionStart = "media.sessionStart" - case play = "media.play" - case ping = "media.ping" - case bitrateChange = "media.bitrateChange" - case bufferStart = "media.bufferStart" - case pauseStart = "media.pauseStart" - case adBreakStart = "media.adBreakStart" - case adStart = "media.adStart" - case adComplete = "media.adComplete" - case adSkip = "media.adSkip" - case adBreakComplete = "media.adBreakComplete" - case chapterStart = "media.chapterStart" - case chapterSkip = "media.chapterSkip" - case chapterComplete = "media.chapterComplete" - case error = "media.error" - case sessionEnd = "media.sessionEnd" - case sessionComplete = "media.sessionComplete" - case statesUpdate = "media.statesUpdate" -} diff --git a/adobe-edge/ios/Connector/api/MediaEdgeAPI.swift b/adobe-edge/ios/Connector/api/MediaEdgeAPI.swift deleted file mode 100644 index ebd1b23e..00000000 --- a/adobe-edge/ios/Connector/api/MediaEdgeAPI.swift +++ /dev/null @@ -1,397 +0,0 @@ -// MediaEdgeAPI.swift - -import Foundation - -struct QueuedEvent { - let path: String - let mediaDetails: [String: Any?] -} - -class MediaEdgeAPI { - private let baseUrl: String - private let configId: String - private let userAgent: String - private var debugSessionId: String? - private let urlSession: URLSession - private let jsonEncoder = JSONEncoder() - private let jsonDecoder = JSONDecoder() - private(set) var sessionId: String? - private var hasSessionFailed = false - private var eventQueue = [QueuedEvent]() - private let dispatchQueue = DispatchQueue.main - - private let pathToEventTypeMap: [String: EventType] = [ - "/play": .play, - "/pauseStart": .pauseStart, - "/error": .error, - "/ping": .ping, - "/bufferStart": .bufferStart, - "/sessionComplete": .sessionComplete, - "/sessionEnd": .sessionEnd, - "/statesUpdate": .statesUpdate, - "/bitrateChange": .bitrateChange, - "/chapterSkip": .chapterSkip, - "/chapterStart": .chapterStart, - "/chapterComplete": .chapterComplete, - "/adBreakStart": .adBreakStart, - "/adBreakComplete": .adBreakComplete, - "/adStart": .adStart, - "/adSkip": .adSkip, - "/adComplete": .adComplete - ] - - init(baseUrl: String, configId: String, userAgent: String, debugSessionId: String? = nil) { - self.baseUrl = baseUrl - self.configId = configId - self.userAgent = userAgent - self.debugSessionId = debugSessionId - self.urlSession = URLSession.shared - self.jsonEncoder.dateEncodingStrategy = .iso8601 - self.jsonDecoder.dateDecodingStrategy = .iso8601 - } - - func setDebugSessionId(debugId: String?) { - debugSessionId = debugId - } - - func hasSessionStarted() -> Bool { - return sessionId != nil - } - - func reset() { - sessionId = nil - hasSessionFailed = false - eventQueue.removeAll() - } - - func play(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/play", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func pause(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/pauseStart", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func error(playhead: Double?, errorDetails: AdobeErrorDetails, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/error", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails, - "errorDetails": errorDetails - ]) - } - - func ping(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - guard let sessionId = sessionId else { return } - Task { @MainActor in - await postEvent(sessionId: sessionId, path: "/ping", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - } - - func bufferStart(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/bufferStart", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func sessionComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/sessionComplete", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func sessionEnd(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/sessionEnd", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - sessionId = nil - } - - func statesUpdate( - playhead: Double?, - statesStart: [AdobePlayerStateData]? = nil, - statesEnd: [AdobePlayerStateData]? = nil, - qoeDataDetails: AdobeQoeDataDetails? = nil - ) { - maybeQueueEvent(path: "/statesUpdate", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "statesStart": statesStart, - "statesEnd": statesEnd, - "qoeDataDetails": qoeDataDetails - ]) - } - - func bitrateChange(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails) { - maybeQueueEvent(path: "/bitrateChange", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func chapterSkip(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/chapterSkip", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func chapterStart( - playhead: Double?, - chapterDetails: AdobeChapterDetails, - customMetadata: [AdobeCustomMetadataDetails]? = nil, - qoeDataDetails: AdobeQoeDataDetails? = nil - ) { - maybeQueueEvent(path: "/chapterStart", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "chapterDetails": chapterDetails, - "customMetadata": customMetadata, - "qoeDataDetails": qoeDataDetails - ]) - } - - func chapterComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/chapterComplete", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func adBreakStart( - playhead: Double, - advertisingPodDetails: AdobeAdvertisingPodDetails, - qoeDataDetails: AdobeQoeDataDetails? = nil - ) { - maybeQueueEvent(path: "/adBreakStart", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "advertisingPodDetails": advertisingPodDetails, - "qoeDataDetails": qoeDataDetails - ]) - } - - func adBreakComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/adBreakComplete", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func adStart( - playhead: Double, - advertisingDetails: AdobeAdvertisingDetails, - customMetadata: [AdobeCustomMetadataDetails]? = nil, - qoeDataDetails: AdobeQoeDataDetails? = nil - ) { - maybeQueueEvent(path: "/adStart", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "advertisingDetails": advertisingDetails, - "customMetadata": customMetadata, - "qoeDataDetails": qoeDataDetails - ]) - } - - func adSkip(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/adSkip", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - func adComplete(playhead: Double?, qoeDataDetails: AdobeQoeDataDetails? = nil) { - maybeQueueEvent(path: "/adComplete", mediaDetails: [ - "playhead": sanitisePlayhead(playhead), - "qoeDataDetails": qoeDataDetails - ]) - } - - private func createUrlWithClientParams(baseUrl: String) -> URL? { - var components = URLComponents(string: baseUrl) - components?.queryItems = [ - URLQueryItem(name: "configId", value: configId) - ] - if let debugSessionId = debugSessionId { - components?.queryItems?.append(URLQueryItem(name: "debugSessionId", value: debugSessionId)) - } - return components?.url - } - - private func sendRequest(url: String, body: String) async throws -> Data? { - guard let url = createUrlWithClientParams(baseUrl: url) else { - throw URLError(.badURL) - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue(userAgent, forHTTPHeaderField: "User-Agent") - request.httpBody = body.data(using: .utf8) - - do { - let (data, response) = try await urlSession.data(for: request) - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - throw URLError(.badServerResponse) - } - print("Adobe sendRequest responseCode: \(httpResponse)") - return data - } catch { - throw error - } - } - - func startSession( - sessionDetails: AdobeSessionDetails, - customMetadata: [AdobeCustomMetadataDetails]? = nil, - qoeDataDetails: AdobeQoeDataDetails? = nil - ) async { - do { - var mediaCollection: [String:Any] = [:] - mediaCollection["playhead"] = 0 - mediaCollection["sessionDetails"] = sessionDetails.toJSONEncodableDictionary() - if let qoeData = qoeDataDetails { - mediaCollection["qoeDataDetails"] = qoeData.toJSONEncodableDictionary() - } - if let metadata = customMetadata { - mediaCollection["customMetadata"] = metadata.map({ $0.toJSONEncodableDictionary() }) - } - let xdm: [String:Any] = [ - "eventType": EventType.sessionStart.rawValue, - "timestamp": ISO8601DateFormatter().string(from: Date()), - "mediaCollection": mediaCollection - ] - let event: [String:Any] = ["xdm" : xdm] - let body: [String:Any] = ["events": [event]] - - let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) - guard let jsonString = String(data: jsonData, encoding: .utf8) else { - throw EncodingError.invalidValue(body, EncodingError.Context(codingPath: [], debugDescription: "Failed to convert data to string")) - } - - let responseData = try await sendRequest(url: "\(baseUrl)/sessionStart", body: jsonString) - - if let responseData = responseData, - let jsonResponse = try? JSONSerialization.jsonObject(with: responseData) as? [String: Any] { - - if let error = jsonResponse["error"] as? [String: Any] { - throw NSError(domain: "MediaEdgeAPI", code: 1, userInfo: error) - } else if let errors = (jsonResponse["data"] as? [String: Any])?["errors"] as? [[String: Any]] { - throw NSError(domain: "MediaEdgeAPI", code: 1, userInfo: ["errors": errors]) - } - - if let handle = jsonResponse["handle"] as? [[String: Any]] { - let sessionIdHandle = handle.first { handleItem in - handleItem["type"] as? String == "media-analytics:new-session" - } - - if let handle = sessionIdHandle, - let payload = handle["payload"] as? [[String:Any]], - let sessionId = payload.first?["sessionId"] as? String { - self.sessionId = sessionId - print("Adobe sessionId received: \(sessionId)") - } - } - } - - if !eventQueue.isEmpty, let sessionId = sessionId { - for event in eventQueue { - await postEvent(sessionId: sessionId, path: event.path, mediaDetails: event.mediaDetails) - } - eventQueue.removeAll() - } - } catch { - print("Failed to start session. \(error.localizedDescription)") - hasSessionFailed = true - } - } - - private func maybeQueueEvent(path: String, mediaDetails: [String: Any?]) { - guard !hasSessionFailed else { return } - - if let sessionId = sessionId { - Task { @MainActor in - await postEvent(sessionId: sessionId, path: path, mediaDetails: mediaDetails) - } - } else { - eventQueue.append(QueuedEvent(path: path, mediaDetails: mediaDetails)) - } - } - - private func postEvent(sessionId: String, path: String, mediaDetails: [String: Any?]) async { - do { - var mediaCollection: [String:Any] = [:] - mediaCollection["playhead"] = 0 - if let qoeData = mediaDetails["qoeDataDetails"] as? AdobeQoeDataDetails { - mediaCollection["qoeDataDetails"] = qoeData.toJSONEncodableDictionary() - } - if let metadata = mediaDetails["customMetadata"] as? [AdobeCustomMetadataDetails] { - mediaCollection["customMetadata"] = metadata.map({ $0.toJSONEncodableDictionary() }) - } - if let advertisingPodDetails = mediaDetails["advertisingPodDetails"] as? AdobeAdvertisingPodDetails { - mediaCollection["advertisingPodDetails"] = advertisingPodDetails.toJSONEncodableDictionary() - } - if let advertisingDetails = mediaDetails["advertisingDetails"] as? AdobeAdvertisingDetails { - mediaCollection["advertisingDetails"] = advertisingDetails.toJSONEncodableDictionary() - } - if let chapterDetails = mediaDetails["chapterDetails"] as? AdobeChapterDetails { - mediaCollection["chapterDetails"] = chapterDetails.toJSONEncodableDictionary() - } - if let errorDetails = mediaDetails["errorDetails"] as? AdobeErrorDetails { - mediaCollection["errorDetails"] = errorDetails.toJSONEncodableDictionary() - } - mediaCollection["sessionID"] = sessionId - - let xdm: [String:Any] = [ - "eventType": pathToEventTypeMap[path]?.rawValue ?? path, - "timestamp": ISO8601DateFormatter().string(from: Date()), - "mediaCollection": mediaCollection - ] - let event: [String:Any] = ["xdm" : xdm] - let body: [String:Any] = ["events": [event]] - - let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) - guard let jsonString = String(data: jsonData, encoding: .utf8) else { - throw EncodingError.invalidValue(body, EncodingError.Context(codingPath: [], debugDescription: "Failed to convert data to string")) - } - - print("postEvent - \(path) \(jsonString)") - - let responseData = try await sendRequest(url: "\(baseUrl)\(path)", body: jsonString) - - if let responseData = responseData, - let jsonResponse = try? JSONSerialization.jsonObject(with: responseData) as? [String: Any] { - - if let error = jsonResponse["error"] as? [String: Any] { - print("Failed to send event. \(error)") - } else if let errors = (jsonResponse["data"] as? [String: Any])?["errors"] as? [[String: Any]] { - print("Failed to send event. \(errors)") - } - } - } catch { - print("Failed to send event. \(error.localizedDescription)") - } - } - - private func sanitisePlayhead(_ playhead: Double?) -> Int { - guard let playhead = playhead else { - return 0 - } - - if playhead.isInfinite { - // If content is live, the playhead must be the current second of the day - let now = Date().timeIntervalSince1970 - return Int(now) % 86400 - } - - return Int(playhead.rounded()) - } -} From bbbcb0a3629dd5a992a87d45c4e422b922442794 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Mon, 15 Dec 2025 09:40:11 +0100 Subject: [PATCH 23/70] Add event queue # Conflicts: # adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt --- .../adobe/edge/AdobeEdgeHandler.kt | 152 ++++++++++++++---- 1 file changed, 122 insertions(+), 30 deletions(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt index 4c2beef1..db5f9eac 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -4,6 +4,7 @@ import android.util.Log import com.adobe.marketing.mobile.LoggingMode import com.adobe.marketing.mobile.MobileCore import com.adobe.marketing.mobile.edge.media.Media +import com.adobe.marketing.mobile.edge.media.Media.Event import com.adobe.marketing.mobile.edge.media.MediaConstants import com.theoplayer.android.api.ads.LinearAd import com.theoplayer.android.api.event.EventListener @@ -41,6 +42,41 @@ typealias RemoveVideoTrackEvent = com.theoplayer.android.api.event.track.mediatr private const val TAG = "AdobeEdgeConnector" +enum class EventType { + PLAY, + PAUSE, + AD_BREAK_START, + AD_BREAK_COMPLETE, + AD_START, + AD_COMPLETE, + AD_SKIP, + CHAPTER_START, + CHAPTER_COMPLETE, + CHAPTER_SKIP, + SEEK_START, + SEEK_COMPLETE, + BUFFER_START, + BUFFER_COMPLETE, + BITRATE_CHANGE, + STATE_START, + STATE_END, + PLAYHEAD_UPDATE, + ERROR, + COMPLETE, + QOE_UPDATE, + SESSION_END, +} + +data class QueuedEvent( + val type: EventType, + val info: Map?, + val metadata: Map? +) + +const val PROP_CURRENT_TIME = "currentTime" +const val PROP_ERROR_ID = "errorId" +const val PROP_NA = "NA" + class AdobeEdgeHandler( private val player: Player, trackerConfig: Map = emptyMap() @@ -85,6 +121,7 @@ class AdobeEdgeHandler( private val onAdSkip = EventListener { handleAdSkip() } private val tracker = Media.createTracker(trackerConfig) + private val eventQueue = mutableListOf() private fun logDebug(message: String) { if (loggingMode >= LoggingMode.DEBUG) { @@ -107,7 +144,7 @@ class AdobeEdgeHandler( } fun setError(errorId: String) { - tracker.trackError(errorId) + queueOrSendEvent(EventType.ERROR, mapOf(PROP_ERROR_ID to errorId), null) } fun stopAndStartNewSession(metadata: Map?) { @@ -171,38 +208,87 @@ class AdobeEdgeHandler( } } + private fun sendEvent( + event: EventType, + info: Map? = null, + metadata: Map? = null + ) { + when (event) { + EventType.AD_BREAK_START -> tracker.trackEvent(Event.AdBreakStart, info, metadata) + EventType.AD_BREAK_COMPLETE -> tracker.trackEvent(Event.AdBreakComplete, info, metadata) + EventType.AD_START -> tracker.trackEvent(Event.AdStart, info, metadata) + EventType.AD_COMPLETE -> tracker.trackEvent(Event.AdComplete, info, metadata) + EventType.AD_SKIP -> tracker.trackEvent(Event.AdSkip, info, metadata) + EventType.CHAPTER_START -> tracker.trackEvent(Event.ChapterStart, info, metadata) + EventType.CHAPTER_COMPLETE -> tracker.trackEvent(Event.ChapterComplete, info, metadata) + EventType.CHAPTER_SKIP -> tracker.trackEvent(Event.ChapterSkip, info, metadata) + EventType.SEEK_START -> tracker.trackEvent(Event.SeekStart, info, metadata) + EventType.SEEK_COMPLETE -> tracker.trackEvent(Event.SeekComplete, info, metadata) + EventType.BUFFER_START -> tracker.trackEvent(Event.BufferStart, info, metadata) + EventType.BUFFER_COMPLETE -> tracker.trackEvent(Event.BufferComplete, info, metadata) + EventType.BITRATE_CHANGE -> tracker.trackEvent(Event.BitrateChange, info, metadata) + EventType.STATE_START -> tracker.trackEvent(Event.StateStart, info, metadata) + EventType.STATE_END -> tracker.trackEvent(Event.StateEnd, info, metadata) + EventType.PLAYHEAD_UPDATE -> tracker.updateCurrentPlayhead( + (info?.get(PROP_CURRENT_TIME) as Int?) ?: 0 + ) + + EventType.ERROR -> tracker.trackError(info?.get(PROP_ERROR_ID) as String? ?: PROP_NA) + EventType.COMPLETE -> tracker.trackComplete() + EventType.QOE_UPDATE -> tracker.updateQoEObject(info ?: emptyMap()) + EventType.SESSION_END -> tracker.trackSessionEnd() + EventType.PLAY -> tracker.trackPlay() + EventType.PAUSE -> tracker.trackPause() + } + } + + private fun queueOrSendEvent( + type: EventType, + info: Map? = null, + metadata: Map? = null + ) { + if (sessionInProgress) { + sendEvent(type, info, metadata) + } else { + eventQueue.add(QueuedEvent(type, info, metadata)) + } + } + private fun handlePlaying() { // NOTE: In case of a pre-roll ad, the `playing` event will be sent twice: once starting the re-roll, and once // starting content. During the pre-roll, all events will be queued. The session will be started after the pre-roll, // making sure we can start the session with the correct content duration (not the ad duration). logDebug("onPlaying") maybeStartSession(player.duration) - tracker.trackPlay() + queueOrSendEvent(EventType.PLAY) } private fun handlePause() { logDebug("onPause") - tracker.trackPause() + queueOrSendEvent(EventType.PAUSE) } private fun handleTimeUpdate(event: TimeUpdateEvent) { logDebug("onTimeUpdate") - tracker.updateCurrentPlayhead(sanitisePlayhead(event.currentTime)) + queueOrSendEvent( + EventType.PLAYHEAD_UPDATE, + mapOf(PROP_CURRENT_TIME to sanitisePlayhead(event.currentTime)) + ) } private fun handleWaiting() { logDebug("onWaiting") - tracker.trackEvent(Media.Event.BufferStart, null, null) + queueOrSendEvent(EventType.BUFFER_START) } private fun handleSeeking() { logDebug("handleSeeking") - tracker.trackEvent(Media.Event.SeekStart, null, null) + queueOrSendEvent(EventType.SEEK_START) } private fun handleSeeked() { logDebug("handleSeeked") - tracker.trackEvent(Media.Event.SeekComplete, null, null) + queueOrSendEvent(EventType.SEEK_COMPLETE) } private fun handleEnded() { @@ -212,7 +298,7 @@ class AdobeEdgeHandler( * been completely viewed. If the viewing session is ended before the media is completely viewed, * use trackSessionEnd instead. */ - tracker.trackComplete() + queueOrSendEvent(EventType.COMPLETE) reset() } @@ -222,8 +308,8 @@ class AdobeEdgeHandler( } private fun handleQualityChanged(event: ActiveQualityChangedEvent) { - tracker.updateQoEObject( - Media.createQoEObject( + queueOrSendEvent( + EventType.QOE_UPDATE, Media.createQoEObject( event.quality?.bandwidth?.toInt() ?: 0, 0, 0, @@ -266,12 +352,12 @@ class AdobeEdgeHandler( logDebug("onEnterCue") val chapterCue = event.cue if (currentChapter != null && currentChapter?.endTime != chapterCue.startTime) { - tracker.trackEvent(Media.Event.ChapterSkip, null, null) + queueOrSendEvent(EventType.CHAPTER_SKIP) } - tracker.trackEvent( - Media.Event.ChapterStart, + queueOrSendEvent( + EventType.CHAPTER_START, Media.createChapterObject( - chapterCue.id.ifEmpty { "NA" }, + chapterCue.id.ifEmpty { PROP_NA }, chapterCue.id.toIntOrNull() ?: 1, chapterCue.endTime.toInt(), (chapterCue.endTime - chapterCue.startTime).toInt() @@ -283,12 +369,12 @@ class AdobeEdgeHandler( private fun handleExitCue() { logDebug("onExitCue") - tracker.trackEvent(Media.Event.ChapterComplete, null, null) + queueOrSendEvent(EventType.CHAPTER_COMPLETE) } private fun handleError(event: ErrorEvent) { logDebug("onError") - tracker.trackError(event.errorObject.code.toString()) + queueOrSendEvent(EventType.ERROR, mapOf("errorId" to event.errorObject.code.toString())) } private fun handleAdBreakBegin(event: AdBreakBeginEvent) { @@ -300,15 +386,14 @@ class AdobeEdgeHandler( currentAdBreakTimeOffset < 0 -> -1 else -> adBreakPodIndex + 1 } - tracker.trackEvent( - Media.Event.AdBreakStart, - Media.createAdBreakObject( - "NA", + queueOrSendEvent( + EventType.AD_BREAK_START, Media.createAdBreakObject( + PROP_NA, index, currentAdBreakTimeOffset - ), - null + ) ) + if (index > adBreakPodIndex) { adBreakPodIndex++ } @@ -318,16 +403,16 @@ class AdobeEdgeHandler( logDebug("onAdBreakEnd") isPlayingAd = false adPodPosition = 1 - tracker.trackEvent(Media.Event.AdBreakComplete, null, null) + queueOrSendEvent(EventType.AD_BREAK_COMPLETE) } private fun handleAdBegin(event: AdBeginEvent) { logDebug("onAdBegin") - tracker.trackEvent( - Media.Event.AdStart, + queueOrSendEvent( + EventType.AD_START, Media.createAdObject( - "NA", - "NA", + PROP_NA, + PROP_NA, adPodPosition, (event.ad as? LinearAd)?.duration ?: 0 ), @@ -338,12 +423,12 @@ class AdobeEdgeHandler( private fun handleAdEnd() { logDebug("onAdEnd") - tracker.trackEvent(Media.Event.AdComplete, null, null) + queueOrSendEvent(EventType.AD_COMPLETE) } private fun handleAdSkip() { logDebug("onAdSkip") - tracker.trackEvent(Media.Event.AdSkip, null, null) + queueOrSendEvent(EventType.AD_SKIP) } private fun maybeEndSession() { @@ -354,7 +439,7 @@ class AdobeEdgeHandler( * even if the user has not viewed the media to completion. If the media is viewed to completion, * use trackComplete instead. */ - tracker.trackSessionEnd() + queueOrSendEvent(EventType.SESSION_END) } reset() } @@ -406,6 +491,12 @@ class AdobeEdgeHandler( sessionInProgress = true + // Post any queued events now that the session has started. + if (eventQueue.isNotEmpty()) { + eventQueue.forEach { event -> sendEvent(event.type, event.info, event.metadata) } + eventQueue.clear() + } + logDebug("maybeStartSession - STARTED") } @@ -422,6 +513,7 @@ class AdobeEdgeHandler( fun reset() { logDebug("reset") + eventQueue.clear() adBreakPodIndex = 0 adPodPosition = 1 isPlayingAd = false From f86e2db8b2fa33d07b0fe7162281378c9d24707e Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Mon, 15 Dec 2025 14:37:33 +0100 Subject: [PATCH 24/70] iOS code improvements --- .../ios/Connector/AdobeEdgeHandler.swift | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index 44b2ab21..aab3262e 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -21,7 +21,7 @@ class AdobeEdgeHandler { private var isPlayingAd = false private var customMetadata: [String:String] = [:] private var currentChapter: TextTrackCue? = nil - private var loggingMode: LogLevel = .error + private var loggingMode: LogLevel = .debug private var tracker: MediaTracker = Media.createTracker() // MARK: Player Listeners @@ -78,7 +78,7 @@ class AdobeEdgeHandler { } func stopAndStartNewSession(_ metadata: [String:String]) -> Void { - guard let player = self.player else {return} + guard let player = self.player else { return } self.maybeEndSession() self.updateMetadata(metadata) self.maybeStartSession() @@ -86,7 +86,7 @@ class AdobeEdgeHandler { } func addEventListeners() -> Void { - guard let player = self.player else {return} + guard let player = self.player else { return } // Player events self.playingListener = player.addEventListener(type: PlayerEventTypes.PLAYING, listener: self.handlePlaying(event:)) @@ -203,6 +203,7 @@ class AdobeEdgeHandler { guard let player = self.player else { return } self.logDebug("onPlaying") self.maybeStartSession(mediaLengthSec: player.duration) + self.tracker.trackPlay() } @@ -211,8 +212,9 @@ class AdobeEdgeHandler { } private func onPause() { - guard let player = self.player else { return } + guard self.player != nil else { return } self.logDebug("onPause") + self.tracker.trackPause() } @@ -222,25 +224,28 @@ class AdobeEdgeHandler { } func handleWaiting(event: WaitingEvent) -> Void { - guard let player = self.player else { return } + guard self.player != nil else { return } self.logDebug("onWaiting") + self.tracker.trackEvent(event: MediaEvent.BufferStart, info: nil, metadata: nil) } func handleSeeking(event: SeekingEvent) -> Void { - guard let player = self.player else { return } + guard self.player != nil else { return } self.logDebug("onSeeking") + self.tracker.trackEvent(event: MediaEvent.SeekStart, info: nil, metadata: nil) } func handleSeeked(event: SeekedEvent) -> Void { - guard let player = self.player else { return } + guard self.player != nil else { return } self.logDebug("onSeeked") + self.tracker.trackEvent(event: MediaEvent.SeekComplete, info: nil, metadata: nil) } func handleEnded(event: EndedEvent) -> Void { - guard let player = self.player else { return } + guard self.player != nil else { return } self.logDebug("onEnded") self.tracker.trackComplete() self.reset() @@ -254,6 +259,7 @@ class AdobeEdgeHandler { func handleActiveQualityChange(event: ActiveQualityChangedEvent) -> Void { guard let player = self.player else { return } self.logDebug("onActiveQualityChange") + var bitrate = 0 if let activeTrack = self.activeTrack(tracks: player.videoTracks) { bitrate = activeTrack.activeQuality?.bandwidth ?? 0 @@ -278,7 +284,7 @@ class AdobeEdgeHandler { } func handleError(event: ErrorEvent) -> Void { - guard let player = self.player else { return } + guard self.player != nil else { return } self.logDebug("onError") var errorCodeString = "-1" if let errorCodeValue = event.errorObject?.code.rawValue as? Int32 { @@ -288,7 +294,7 @@ class AdobeEdgeHandler { } func handleAdBreakBegin(event: AdBreakBeginEvent) -> Void { - guard let player = self.player else { return } + guard self.player != nil else { return } self.logDebug("onAdBreakBegin") self.isPlayingAd = true let currentAdBreakTimeOffset = event.ad?.timeOffset ?? 0 @@ -301,7 +307,7 @@ class AdobeEdgeHandler { } func handleAdBreakEnd(event: AdBreakEndEvent) -> Void { - guard let player = self.player else { return } + guard self.player != nil else { return } self.logDebug("onAdBreakEnd") self.isPlayingAd = false self.adPodPosition = 1 @@ -309,31 +315,30 @@ class AdobeEdgeHandler { } func handleAdBegin(event: AdBeginEvent) -> Void { - guard let player = self.player else { return } + guard self.player != nil else { return } self.logDebug("onAdBegin") let duration = event.ad?.duration ?? 0 let adObject = Media.createAdObjectWith(name: "NA", id: "NA", position: self.adPodPosition, length: duration) self.tracker.trackEvent(event: MediaEvent.AdBreakStart, info: adObject, metadata: nil) - self.adPodPosition += 1 } func handleAdEnd(event: AdEndEvent) -> Void { - guard let player = self.player else { return } + guard self.player != nil else { return } self.logDebug("onAdEnd") self.tracker.trackEvent(event: MediaEvent.AdComplete, info: nil, metadata: nil) } func handleAdSkip(event: AdSkipEvent) -> Void { - guard let player = self.player else { return } + guard self.player != nil else { return } self.logDebug("onAdSkip") self.tracker.trackEvent(event: MediaEvent.AdSkip, info: nil, metadata: nil) } private func maybeEndSession() -> Void { - guard let player = self.player else { return } + guard self.player != nil else { return } self.logDebug("maybeEndSession") - if (self.sessionInProgress) { + if self.sessionInProgress { self.tracker.trackSessionEnd() } self.reset() @@ -355,18 +360,20 @@ class AdobeEdgeHandler { let mediaLength = AdobeUtils.sanitiseContentLength(mediaLengthSec) let hasValidSource = player.source != nil let hasValidDuration = player.duration != nil && !(player.duration!.isNaN) + let streamType = self.getStreamType() self.logDebug("maybeStartSession - mediaLength: \(mediaLength)") self.logDebug("maybeStartSession - hasValidSource: \(hasValidSource)") self.logDebug("maybeStartSession - hasValidDuration: \(hasValidDuration)") self.logDebug("maybeStartSession - sessionInProgress: \(self.sessionInProgress)") self.logDebug("maybeStartSession - isPlayingAd: \(self.isPlayingAd)") + self.logDebug("maybeStartSession - streamType: \(streamType)") - guard !sessionInProgress else { + guard !self.sessionInProgress else { self.logDebug("maybeStartSession - NOT started: already in progress") return } - guard !isPlayingAd else { + guard !self.isPlayingAd else { self.logDebug("maybeStartSession - NOT started: playing ad") return } @@ -377,17 +384,16 @@ class AdobeEdgeHandler { return } - + let metadata: [String: Any] = player.source?.metadata?.metadataKeys ?? [:] if let mediaObject = Media.createMediaObjectWith( - name: player.source?.metadata?.title ?? "N/A", - id: "N/A", + name: metadata["title"] as? String ?? "N/A", + id: metadata["id"] as? String ?? "N/A", length: mediaLength, - streamType: self.getStreamType(), + streamType: streamType, mediaType: MediaType.Video ) { self.tracker.trackSessionStart(info: mediaObject, metadata: self.customMetadata) - - sessionInProgress = true + self.sessionInProgress = true self.logDebug("maybeStartSession - STARTED") } } From 56249b45ea847b95c7297d9aee13cad8be59381e Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Mon, 15 Dec 2025 14:38:03 +0100 Subject: [PATCH 25/70] unset sessionInProgress on session end --- adobe-edge/ios/Connector/AdobeEdgeHandler.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index aab3262e..6d366688 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -340,6 +340,7 @@ class AdobeEdgeHandler { self.logDebug("maybeEndSession") if self.sessionInProgress { self.tracker.trackSessionEnd() + self.sessionInProgress = false } self.reset() } From 7708d918fd89ef8840a9e4efb66cf34e8adf3aa7 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Mon, 15 Dec 2025 14:38:51 +0100 Subject: [PATCH 26/70] Don't log timeUpdate events --- adobe-edge/ios/Connector/AdobeEdgeHandler.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index 6d366688..915d7b4a 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -219,8 +219,10 @@ class AdobeEdgeHandler { } private func handleTimeUpdate(event: TimeUpdateEvent) { - self.logDebug("onTimeUpdate") - self.tracker.updateCurrentPlayhead(time: AdobeUtils.sanitisePlayhead(event.currentTime)) + guard self.player != nil else { return } + //self.logDebug("onTimeUpdate") + + self.tracker.updateCurrentPlayhead(time: self.sanitisePlayhead(event.currentTime)) } func handleWaiting(event: WaitingEvent) -> Void { From 9e7cc7d61531f1a67d6b21f3622db577a80e06c1 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Mon, 15 Dec 2025 14:39:32 +0100 Subject: [PATCH 27/70] Move Utils closer to react native bridge code --- .../ios/Connector/AdobeEdgeHandler.swift | 20 +++++++++++++++- .../TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift | 6 ++--- ...heoplayerAdobeEdgeRCTAdobeEdgeUtils.swift} | 24 +++---------------- 3 files changed, 25 insertions(+), 25 deletions(-) rename adobe-edge/ios/{Connector/AdobeUtils.swift => TheoplayerAdobeEdgeRCTAdobeEdgeUtils.swift} (59%) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index 915d7b4a..4f06c29d 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -360,7 +360,7 @@ class AdobeEdgeHandler { func maybeStartSession(mediaLengthSec: Double? = nil) -> Void { guard let player = self.player else { return } - let mediaLength = AdobeUtils.sanitiseContentLength(mediaLengthSec) + let mediaLength = self.sanitiseContentLength(mediaLengthSec) let hasValidSource = player.source != nil let hasValidDuration = player.duration != nil && !(player.duration!.isNaN) let streamType = self.getStreamType() @@ -425,4 +425,22 @@ class AdobeEdgeHandler { } return MediaConstants.StreamType.LIVE } + + private func sanitisePlayhead(_ playhead: Double?) -> Int { + guard let playhead = playhead else { + return 0 + } + + if playhead == Double.infinity { + // If content is live, the playhead must be the current second of the day. + let now = Date().timeIntervalSince1970 + return Int(now.truncatingRemainder(dividingBy: 86400)) + } + + return Int(playhead) + } + + private func sanitiseContentLength(_ mediaLength: Double?) -> Int { + mediaLength == .infinity ? 86400 : Int(mediaLength ?? 0) + } } diff --git a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift index 12ed67b8..f517ecdf 100644 --- a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift +++ b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift @@ -33,7 +33,7 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { DispatchQueue.main.async { if let view = self.view(for: node), let player = view.player { - let trackerConfig: [String: String] = AdobeUtils.toStringMap(config as? [String: Any] ?? [:]) + let trackerConfig: [String: String] = TheoplayerAdobeEdgeRCTAdobeEdgeUtils.toStringMap(config as? [String: Any] ?? [:]) let connector = AdobeEdgeConnector(player: player, trackerConfig: trackerConfig) self.connectors[node] = connector self.log("added connector to view \(node)") @@ -60,7 +60,7 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { DispatchQueue.main.async { if let connector = self.connectors[node], let newMetadata = metadata as? [[String: Any]] { - connector.updateMetadata(AdobeUtils.toAdobeCustomMetadataDetails(newMetadata)) + connector.updateMetadata(TheoplayerAdobeEdgeRCTAdobeEdgeUtils.toAdobeCustomMetadataDetails(newMetadata)) } } } @@ -81,7 +81,7 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { DispatchQueue.main.async { if let connector = self.connectors[node], let newMetadata = customMetadataDetails as? [[String: Any]] { - connector.stopAndStartNewSession(AdobeUtils.toAdobeCustomMetadataDetails(newMetadata)) + connector.stopAndStartNewSession(TheoplayerAdobeEdgeRCTAdobeEdgeUtils.toAdobeCustomMetadataDetails(newMetadata)) } } } diff --git a/adobe-edge/ios/Connector/AdobeUtils.swift b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeUtils.swift similarity index 59% rename from adobe-edge/ios/Connector/AdobeUtils.swift rename to adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeUtils.swift index e261bbef..79dbd5eb 100644 --- a/adobe-edge/ios/Connector/AdobeUtils.swift +++ b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeUtils.swift @@ -1,13 +1,13 @@ -// AdobeUtils.swift +// TheoplayerAdobeEdgeRCTAdobeEdgeUtils.swift import Foundation import THEOplayerSDK -class AdobeUtils { +class TheoplayerAdobeEdgeRCTAdobeEdgeUtils { class func toAdobeCustomMetadataDetails(_ array: [[String: Any]]) -> [String: String] { var result = [String: String]() for item in array { - let stringsItem = AdobeUtils.toStringMap(item) + let stringsItem = TheoplayerAdobeEdgeRCTAdobeEdgeUtils.toStringMap(item) if let name = stringsItem["name"], let value = stringsItem["value"] { result[name] = value } @@ -31,23 +31,5 @@ class AdobeUtils { return result } - - class func sanitisePlayhead(_ playhead: Double?) -> Int { - guard let playhead = playhead else { - return 0 - } - - if playhead == Double.infinity { - // If content is live, the playhead must be the current second of the day. - let now = Date().timeIntervalSince1970 - return Int(now.truncatingRemainder(dividingBy: 86400)) - } - - return Int(playhead) - } - - class func sanitiseContentLength(_ mediaLength: Double?) -> Int { - mediaLength == .infinity ? 86400 : Int(mediaLength ?? 0) - } } From 2be74295c086c4b3a9e6ce2790de190309059750 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Mon, 15 Dec 2025 12:00:01 +0100 Subject: [PATCH 28/70] Align web connector --- adobe-edge/src/api/details/barrel.ts | 3 +- .../src/internal/AdobeConnectorAdapterWeb.ts | 318 +- .../src/internal/web/AdobeEdgeConnector.ts | 45 + .../src/internal/web/AdobeEdgeHandler.ts | 526 +++ .../src/internal/{ => web}/EventType.ts | 0 adobe-edge/src/internal/web/MediaEdge.d.ts | 1614 -------- adobe-edge/src/internal/web/MediaEdgeAPI.ts | 295 -- .../src/internal/web/PathToEventTypeMap.ts | 23 - adobe-edge/src/internal/web/Utils.ts | 50 +- .../src/internal/web/media-edge-0.1.json | 3688 ----------------- 10 files changed, 622 insertions(+), 5940 deletions(-) create mode 100644 adobe-edge/src/internal/web/AdobeEdgeConnector.ts create mode 100644 adobe-edge/src/internal/web/AdobeEdgeHandler.ts rename adobe-edge/src/internal/{ => web}/EventType.ts (100%) delete mode 100644 adobe-edge/src/internal/web/MediaEdge.d.ts delete mode 100644 adobe-edge/src/internal/web/MediaEdgeAPI.ts delete mode 100644 adobe-edge/src/internal/web/PathToEventTypeMap.ts delete mode 100644 adobe-edge/src/internal/web/media-edge-0.1.json diff --git a/adobe-edge/src/api/details/barrel.ts b/adobe-edge/src/api/details/barrel.ts index fbf5a629..814de63e 100644 --- a/adobe-edge/src/api/details/barrel.ts +++ b/adobe-edge/src/api/details/barrel.ts @@ -8,4 +8,5 @@ export type { AdobeImplementationDetails, AdobeEnvironment } from './AdobeImplem export type { AdobeMediaDetails } from './AdobeMediaDetails'; export type { AdobePlayerStateData } from './AdobePlayerStateData'; export type { AdobeQoeDataDetails } from './AdobeQoeDataDetails'; -export type { AdobeSessionDetails, ContentType, StreamType } from './AdobeSessionDetails'; +export { ContentType, StreamType } from './AdobeSessionDetails'; +export type { AdobeSessionDetails } from './AdobeSessionDetails'; diff --git a/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts b/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts index 1b392931..35eb19d6 100644 --- a/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts @@ -1,332 +1,34 @@ -import type { - Ad, - AdBreak, - AdEvent, - ErrorEvent, - LoadedMetadataEvent, - MediaTrackEvent, - TextTrackCue, - TextTrackEvent, - THEOplayer, -} from 'react-native-theoplayer'; -import { AdEventType, MediaTrackEventType, PlayerEventType, TextTrackEventType } from 'react-native-theoplayer'; -import { calculateAdvertisingPodDetails, calculateAdvertisingDetails, calculateChapterDetails, sanitiseContentLength } from './web/Utils'; -import { MediaEdgeAPI } from './web/MediaEdgeAPI'; +import type { THEOplayer } from 'react-native-theoplayer'; import type { AdobeCustomMetadataDetails, AdobeErrorDetails } from '@theoplayer/react-native-analytics-adobe-edge'; -import { ContentType } from '../api/details/AdobeSessionDetails'; -import { ErrorSource } from '../api/details/AdobeErrorDetails'; import { AdobeConnectorAdapter } from './AdobeConnectorAdapter'; import { AdobeEdgeWebConfig } from '../api/AdobeEdgeWebConfig'; - -const TAG = 'AdobeConnector'; -const CONTENT_PING_INTERVAL = 10000; -const AD_PING_INTERVAL = 1000; +import { AdobeEdgeConnector } from './web/AdobeEdgeConnector'; +import { ChromelessPlayer } from 'theoplayer'; export class AdobeConnectorAdapterWeb implements AdobeConnectorAdapter { - private player: THEOplayer; - - /** Timer handling the ping event request */ - private pingInterval: ReturnType | undefined; - - /** Whether we are in a current session or not */ - private sessionInProgress = false; - - private adBreakPodIndex = 0; - - private adPodPosition = 1; - - private isPlayingAd = false; - - private customMetadata: AdobeCustomMetadataDetails[] = []; - - private currentChapter: TextTrackCue | undefined; - - private debug = false; - - private mediaApi: MediaEdgeAPI; + private connector: AdobeEdgeConnector; constructor(player: THEOplayer, config: AdobeEdgeWebConfig) { - this.player = player; - this.mediaApi = new MediaEdgeAPI(config); - this.debug = config.debugEnabled || false; - this.addEventListeners(); - this.logDebug('Initialized connector'); + this.connector = new AdobeEdgeConnector(player.nativeHandle as ChromelessPlayer, config); } setDebug(debug: boolean) { - this.debug = debug; - this.mediaApi.setDebug(debug); + this.connector.setDebug(debug); } updateMetadata(metadata: AdobeCustomMetadataDetails[]): void { - this.customMetadata = [...this.customMetadata, ...metadata]; + this.connector.updateMetadata(metadata); } setError(errorDetails: AdobeErrorDetails): void { - void this.mediaApi.error(this.player.currentTime, errorDetails); + void this.connector.setError(errorDetails); } async stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]): Promise { - await this.maybeEndSession(); - if (metadata !== undefined) { - this.updateMetadata(metadata); - } - await this.maybeStartSession(); - - if (this.player.paused) { - this.onPause(); - } else { - this.onPlaying(); - } - } - - private addEventListeners(): void { - this.player.addEventListener(PlayerEventType.PLAYING, this.onPlaying); - this.player.addEventListener(PlayerEventType.PAUSE, this.onPause); - this.player.addEventListener(PlayerEventType.ENDED, this.onEnded); - this.player.addEventListener(PlayerEventType.WAITING, this.onWaiting); - this.player.addEventListener(PlayerEventType.SOURCE_CHANGE, this.onSourceChange); - this.player.addEventListener(PlayerEventType.TEXT_TRACK, this.onTextTrackEvent); - this.player.addEventListener(PlayerEventType.MEDIA_TRACK, this.onMediaTrackEvent); - this.player.addEventListener(PlayerEventType.LOADED_METADATA, this.onLoadedMetadata); - this.player.addEventListener(PlayerEventType.ERROR, this.onError); - this.player.addEventListener(PlayerEventType.AD_EVENT, this.onAdEvent); - window.addEventListener('beforeunload', this.onBeforeUnload); - } - - private removeEventListeners(): void { - this.player.removeEventListener(PlayerEventType.PLAYING, this.onPlaying); - this.player.removeEventListener(PlayerEventType.PAUSE, this.onPause); - this.player.removeEventListener(PlayerEventType.ENDED, this.onEnded); - this.player.removeEventListener(PlayerEventType.WAITING, this.onWaiting); - this.player.removeEventListener(PlayerEventType.SOURCE_CHANGE, this.onSourceChange); - this.player.removeEventListener(PlayerEventType.TEXT_TRACK, this.onTextTrackEvent); - this.player.removeEventListener(PlayerEventType.MEDIA_TRACK, this.onMediaTrackEvent); - this.player.removeEventListener(PlayerEventType.LOADED_METADATA, this.onLoadedMetadata); - this.player.removeEventListener(PlayerEventType.ERROR, this.onError); - this.player.removeEventListener(PlayerEventType.AD_EVENT, this.onAdEvent); - window.removeEventListener('beforeunload', this.onBeforeUnload); - } - - private onLoadedMetadata = (e: LoadedMetadataEvent) => { - this.logDebug('onLoadedMetadata'); - - // NOTE: In case of a pre-roll ad: - // - on Android & iOS, the onLoadedMetadata is sent *after* a pre-roll has finished; - // - on Web, onLoadedMetadata is sent twice, once before the pre-roll, where player.duration is still NaN, - // and again after the pre-roll with a correct duration. - void this.maybeStartSession(e.duration); - }; - - private onPlaying = () => { - this.logDebug('onPlaying'); - void this.mediaApi.play(this.player.currentTime); - }; - - private onPause = () => { - this.logDebug('onPause'); - void this.mediaApi.pause(this.player.currentTime); - }; - - private onWaiting = () => { - this.logDebug('onWaiting'); - void this.mediaApi.bufferStart(this.player.currentTime); - }; - - private onEnded = async () => { - this.logDebug('onEnded'); - await this.mediaApi.sessionComplete(this.player.currentTime); - this.reset(); - }; - - private onSourceChange = () => { - this.logDebug('onSourceChange'); - void this.maybeEndSession(); - }; - - private onMediaTrackEvent = (event: MediaTrackEvent) => { - if (event.subType === MediaTrackEventType.ACTIVE_QUALITY_CHANGED) { - const quality = Array.isArray(event.qualities) ? event.qualities[0] : event.qualities; - void this.mediaApi.bitrateChange(this.player.currentTime, { - bitrate: quality?.bandwidth ?? 0, - }); - } - }; - - private onTextTrackEvent = (event: TextTrackEvent) => { - const track = this.player.textTracks.find((track) => track.uid === event.trackUid); - if (track !== undefined && track.kind === 'chapters') { - switch (event.subType) { - case TextTrackEventType.ENTER_CUE: { - const chapterCue = event.cue; - if (this.currentChapter && this.currentChapter.endTime !== chapterCue.startTime) { - this.mediaApi.chapterSkip(this.player.currentTime); - } - const chapterDetails = calculateChapterDetails(chapterCue); - this.mediaApi.chapterStart(this.player.currentTime, chapterDetails, this.customMetadata); - this.currentChapter = chapterCue; - break; - } - case TextTrackEventType.EXIT_CUE: { - this.mediaApi.chapterComplete(this.player.currentTime); - break; - } - } - } - }; - - private onError = (error: ErrorEvent) => { - void this.mediaApi.error(this.player.currentTime, { - name: error.error.errorCode, - source: ErrorSource.PLAYER, - }); - }; - - private onAdEvent = (event: AdEvent) => { - switch (event.subType) { - case AdEventType.AD_BREAK_BEGIN: { - this.isPlayingAd = true; - this.startPinger(AD_PING_INTERVAL); - const adBreak = event.ad as AdBreak; - const podDetails = calculateAdvertisingPodDetails(adBreak, this.adBreakPodIndex); - void this.mediaApi.adBreakStart(this.player.currentTime, podDetails); - if (podDetails.index > this.adBreakPodIndex) { - this.adBreakPodIndex++; - } - break; - } - case AdEventType.AD_BREAK_END: { - this.isPlayingAd = false; - this.adPodPosition = 1; - this.startPinger(CONTENT_PING_INTERVAL); - void this.mediaApi.adBreakComplete(this.player.currentTime); - break; - } - case AdEventType.AD_BEGIN: { - const ad = event.ad as Ad; - const adDetails = calculateAdvertisingDetails(ad, this.adPodPosition); - void this.mediaApi.adStart(this.player.currentTime, adDetails, this.customMetadata); - this.adPodPosition++; - break; - } - case AdEventType.AD_END: { - void this.mediaApi.adComplete(this.player.currentTime); - break; - } - case AdEventType.AD_SKIP: { - void this.mediaApi.adSkip(this.player.currentTime); - break; - } - } - }; - - private onBeforeUnload = () => { - void this.maybeEndSession(); - }; - - private async maybeEndSession(): Promise { - this.logDebug(`maybeEndSession`); - if (this.mediaApi.hasSessionStarted()) { - await this.mediaApi.sessionEnd(this.player.currentTime); - } - this.reset(); - return Promise.resolve(); - } - - /** - * Start a new session, but only if: - * - no existing session has is in progress; - * - the player has a valid source; - * - no ad is playing, otherwise the ad's media duration will be picked up; - * - the player's content media duration is known. - * - * @param mediaLengthMsec - * @private - */ - private async maybeStartSession(mediaLengthMsec?: number): Promise { - const mediaLength = this.getContentLength(mediaLengthMsec); - const hasValidSource = this.player.source !== undefined; - const hasValidDuration = isValidDuration(mediaLength); - const isPlayingAd = await this.player.ads.playing(); - - this.logDebug( - `maybeStartSession -`, - `mediaLength: ${mediaLength},`, - `hasValidSource: ${hasValidSource},`, - `hasValidDuration: ${hasValidDuration},`, - `isPlayingAd: ${isPlayingAd}`, - ); - - if (this.sessionInProgress || !hasValidSource || !hasValidDuration || isPlayingAd) { - this.logDebug('maybeStartSession - NOT started'); - return; - } - - const sessionDetails = { - ID: 'N/A', - name: this.player?.source?.metadata?.title ?? 'N/A', - channel: 'N/A', - contentType: this.getContentType(), - playerName: 'THEOplayer', - length: mediaLength, - }; - - await this.mediaApi.startSession(sessionDetails, this.customMetadata); - if (!this.mediaApi.hasSessionStarted()) { - return; - } - - this.sessionInProgress = true; - this.logDebug('maybeStartSession - STARTED', `sessionId: ${this.mediaApi.sessionId}`); - - if (!this.isPlayingAd) { - this.startPinger(CONTENT_PING_INTERVAL); - } else { - this.startPinger(AD_PING_INTERVAL); - } - } - - private startPinger(interval: number): void { - if (this.pingInterval !== undefined) { - clearInterval(this.pingInterval); - } - this.pingInterval = setInterval(() => { - void this.mediaApi.ping(this.player.currentTime); - }, interval); - } - - private getContentLength(mediaLengthMsec?: number): number { - return sanitiseContentLength(mediaLengthMsec !== undefined ? mediaLengthMsec : this.player.duration); - } - - private getContentType(): ContentType { - return this.player.duration === Infinity ? ContentType.LIVE : ContentType.VOD; - } - - reset(): void { - this.logDebug('reset'); - this.mediaApi.reset(); - this.adBreakPodIndex = 0; - this.adPodPosition = 1; - this.isPlayingAd = false; - this.sessionInProgress = false; - clearInterval(this.pingInterval); - this.pingInterval = undefined; - this.currentChapter = undefined; + await this.connector.stopAndStartNewSession(metadata); } async destroy(): Promise { - await this.maybeEndSession(); - this.removeEventListeners(); - } - - private logDebug(message?: any, ...optionalParams: any[]) { - if (this.debug) { - console.debug(TAG, message, ...optionalParams); - } + return this.connector.destroy(); } } - -function isValidDuration(v: number | undefined): boolean { - return v !== undefined && !Number.isNaN(v); -} diff --git a/adobe-edge/src/internal/web/AdobeEdgeConnector.ts b/adobe-edge/src/internal/web/AdobeEdgeConnector.ts new file mode 100644 index 00000000..35bea84b --- /dev/null +++ b/adobe-edge/src/internal/web/AdobeEdgeConnector.ts @@ -0,0 +1,45 @@ +import { AdobeEdgeHandler } from './AdobeEdgeHandler'; +import { + AdobeCustomMetadataDetails, + AdobeEdgeWebConfig, + AdobeErrorDetails, + AdobeQoeDataDetails, +} from '@theoplayer/react-native-analytics-adobe-edge'; +import { ChromelessPlayer } from 'theoplayer'; + +export class AdobeEdgeConnector { + private _handler: AdobeEdgeHandler; + + constructor(player: ChromelessPlayer, config: AdobeEdgeWebConfig) { + this._handler = new AdobeEdgeHandler(player, config); + } + + /** + * 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?: AdobeCustomMetadataDetails[]) { + return this._handler.stopAndStartNewSession(metadata); + } + + updateMetadata(metadata: AdobeCustomMetadataDetails[]) { + this._handler.updateMetadata(metadata); + } + + setDebug(debug: boolean) { + this._handler.setDebug(debug); + } + + setError(errorDetails: AdobeErrorDetails, qoeDataDetails?: AdobeQoeDataDetails) { + return this._handler.setError(errorDetails, qoeDataDetails); + } + + destroy() { + return this._handler.destroy(); + } +} diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts new file mode 100644 index 00000000..c3057216 --- /dev/null +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -0,0 +1,526 @@ +import { + AdobeCustomMetadataDetails, + AdobeErrorDetails, + AdobeMediaDetails, + AdobeQoeDataDetails, + AdobeSessionDetails, + ContentType, + ErrorSource, +} from '@theoplayer/react-native-analytics-adobe-edge'; +import type { AdobePlayerStateData } from '../../api/details/AdobePlayerStateData'; +import { + calculateAdvertisingDetails, + calculateAdvertisingPodDetails, + calculateChapterDetails, + isValidDuration, + sanitiseConfig, + sanitiseContentLength, + sanitisePlayhead, +} from './Utils'; +import { createInstance } from '@adobe/alloy'; +import { AdobeEdgeWebConfig } from '../../api/AdobeEdgeWebConfig'; +import { + type TextTrackCue, + TextTrack, + type ChromelessPlayer, + AdEvent, + ErrorEvent, + AddTrackEvent, + TextTrackEnterCueEvent, + RemoveTrackEvent, + MediaTrack, + QualityEvent, + AdBreakEvent, +} from 'theoplayer'; +import { EventType } from './EventType'; + +const TAG = 'AdobeConnector'; +const CONTENT_PING_INTERVAL = 10000; +const AD_PING_INTERVAL = 1000; + +// eslint-disable-next-line @typescript-eslint/ban-types +type AlloyClient = Function; + +/** + * Alloy globally stores clients by name. We are allowed create clients with the same config only once. + */ +interface ClientDescription { + datastreamId: string; + orgId: string; + client: AlloyClient; +} +const createdClients: ClientDescription[] = []; + +/** + * The MediaEdgeAPI class is responsible for communicating media events to Adobe Experience Platform. + * + * Event handling for manually-tracked sessions is used. In this mode you need to pass the sessionID to the media event, + * along with the playhead value (integer value). You could also pass the Quality of Experience data details, if needed. + * + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/js-overview} + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/streamingmedia} + */ +export class AdobeEdgeHandler { + private _player: ChromelessPlayer; + private _sessionId: string | undefined; + private _hasSessionFailed: boolean; + + /** Timer handling the ping event request */ + private _pingInterval: ReturnType | undefined; + + /** Whether we are in a current session or not */ + private _sessionInProgress = false; + private _adBreakPodIndex = 0; + private _adPodPosition = 1; + private _isPlayingAd = false; + private _customMetadata: AdobeCustomMetadataDetails[] = []; + private _currentChapter: TextTrackCue | undefined; + private _eventQueue: (() => Promise)[] = []; + private _debug = false; + private readonly _alloyClient: AlloyClient; + + constructor(player: ChromelessPlayer, config: AdobeEdgeWebConfig) { + this._player = player; + this._hasSessionFailed = false; + const sanitisedConfig = sanitiseConfig(config); + const { datastreamId, orgId, debugEnabled } = sanitisedConfig; + + this.addEventListeners(); + + this._alloyClient = findAlloyClient(datastreamId, orgId); + if (!this._alloyClient) { + this._alloyClient = createInstance({ + name: 'alloy', + monitors: [ + { + // Optionally configure callbacks. + // onInstanceConfigured: function (data: any) {}, + }, + ], + }); + this._alloyClient('configure', sanitisedConfig); + + // Store created client to prevent creating duplicates. + createdClients.push({ datastreamId, orgId, client: this._alloyClient }); + } + this.setDebug(debugEnabled); + } + + async stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]): Promise { + await this.maybeEndSession(); + if (metadata !== undefined) { + this.updateMetadata(metadata); + } + await this.maybeStartSession(); + + if (this._player.paused) { + this.onPause(); + } else { + this.onPlaying(); + } + } + + updateMetadata(metadata: AdobeCustomMetadataDetails[]): void { + this._customMetadata = [...this._customMetadata, ...metadata]; + } + + private addEventListeners(): void { + this._player.addEventListener('playing', this.onPlaying); + this._player.addEventListener('pause', this.onPause); + this._player.addEventListener('ended', this.onEnded); + this._player.addEventListener('waiting', this.onWaiting); + this._player.addEventListener('sourcechange', this.onSourceChange); + this._player.textTracks.addEventListener('addtrack', this.onAddTextTrack); + this._player.textTracks.addEventListener('removetrack', this.onRemoveTextTrack); + this._player.videoTracks.addEventListener('addtrack', this.onAddVideoTrack); + this._player.videoTracks.addEventListener('removetrack', this.onRemoveVideoTrack); + this._player.addEventListener('loadedmetadata', this.onLoadedMetadata); + this._player.addEventListener('error', this.onError); + this._player.ads?.addEventListener('adbreakbegin', this.onAdBreakBegin); + 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('adbreakend', this.onAdBreakEnd); + window.addEventListener('beforeunload', this.onBeforeUnload); + } + + private removeEventListeners(): void { + this._player.removeEventListener('playing', this.onPlaying); + this._player.removeEventListener('pause', this.onPause); + this._player.removeEventListener('ended', this.onEnded); + this._player.removeEventListener('waiting', this.onWaiting); + this._player.removeEventListener('sourcechange', this.onSourceChange); + this._player.textTracks.removeEventListener('addtrack', this.onAddTextTrack); + this._player.textTracks.removeEventListener('removetrack', this.onRemoveTextTrack); + this._player.videoTracks.removeEventListener('addtrack', this.onAddVideoTrack); + this._player.videoTracks.removeEventListener('removetrack', this.onRemoveVideoTrack); + this._player.removeEventListener('loadedmetadata', this.onLoadedMetadata); + this._player.removeEventListener('error', this.onError); + this._player.ads?.removeEventListener('adbreakbegin', this.onAdBreakBegin); + 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('adbreakend', this.onAdBreakEnd); + window.removeEventListener('beforeunload', this.onBeforeUnload); + } + + private onLoadedMetadata = () => { + this.logDebug('onLoadedMetadata'); + + // NOTE: In case of a pre-roll ad: + // - on Android & iOS, the onLoadedMetadata is sent *after* a pre-roll has finished; + // - on Web, onLoadedMetadata is sent twice, once before the pre-roll, where player.duration is still NaN, + // and again after the pre-roll with a correct duration. + void this.maybeStartSession(this._player.duration); + }; + + private onPlaying = () => { + this.logDebug('onPlaying'); + void this.queueOrSendEvent(EventType.play, { playhead: this._player.currentTime }); + }; + + private onPause = () => { + this.logDebug('onPause'); + void this.queueOrSendEvent(EventType.pauseStart, { playhead: this._player.currentTime }); + }; + + private onWaiting = () => { + this.logDebug('onWaiting'); + void this.queueOrSendEvent(EventType.bufferStart, { playhead: this._player.currentTime }); + }; + + private onEnded = async () => { + this.logDebug('onEnded'); + void this.queueOrSendEvent(EventType.sessionComplete, { playhead: this._player.currentTime }); + this.reset(); + }; + + private onSourceChange = () => { + this.logDebug('onSourceChange'); + void this.maybeEndSession(); + }; + + private onAddVideoTrack = (event: AddTrackEvent) => { + (event.track as MediaTrack).addEventListener('activequalitychanged', this.onActiveQualityChanged); + }; + + private onRemoveVideoTrack = (event: RemoveTrackEvent) => { + (event.track as MediaTrack).removeEventListener('activequalitychanged', this.onActiveQualityChanged); + }; + + private onActiveQualityChanged = (event: QualityEvent<'activequalitychanged'>) => { + void this.queueOrSendEvent(EventType.bitrateChange, { + playhead: this._player.currentTime, + qoeDataDetails: { + bitrate: event.quality?.bandwidth ?? 0, + }, + }); + }; + + private onAddTextTrack = (event: AddTrackEvent) => { + const track = event.track as TextTrack; + if (track.kind === 'chapters') { + track.addEventListener('entercue', this.onEnterCue); + track.addEventListener('exitcue', this.onExitCue); + } + }; + + private onRemoveTextTrack = (event: RemoveTrackEvent) => { + const track = event.track as TextTrack; + if (track.kind === 'chapters') { + track.removeEventListener('entercue', this.onEnterCue); + track.removeEventListener('exitcue', this.onExitCue); + } + }; + + private onEnterCue = (event: TextTrackEnterCueEvent) => { + const chapterCue = event.cue; + if (this._currentChapter && this._currentChapter.endTime !== chapterCue.startTime) { + void this.queueOrSendEvent(EventType.chapterSkip, { playhead: this._player.currentTime }); + } + void this.queueOrSendEvent(EventType.chapterStart, { + playhead: this._player.currentTime, + chapterDetails: calculateChapterDetails(chapterCue), + customMetadata: this._customMetadata, + }); + this._currentChapter = chapterCue; + }; + + private onExitCue = () => { + void this.queueOrSendEvent(EventType.chapterComplete, { playhead: this._player.currentTime }); + }; + + private onError = (error: ErrorEvent) => { + void this.setError({ + name: error.errorObject.name, + source: ErrorSource.PLAYER, + }); + }; + + private onAdBreakBegin = (event: AdBreakEvent<'adbreakbegin'>) => { + this._isPlayingAd = true; + this.startPinger(AD_PING_INTERVAL); + const podDetails = calculateAdvertisingPodDetails(event.adBreak, this._adBreakPodIndex); + void this.queueOrSendEvent(EventType.adBreakStart, { + playhead: this._player.currentTime, + advertisingPodDetails: podDetails, + }); + if (podDetails.index > this._adBreakPodIndex) { + this._adBreakPodIndex++; + } + }; + + private onAdBegin = (event: AdEvent<'adbegin'>) => { + void this.queueOrSendEvent(EventType.adStart, { + playhead: this._player.currentTime, + advertisingDetails: calculateAdvertisingDetails(event.ad, this._adPodPosition), + customMetadata: this._customMetadata, + }); + this._adPodPosition++; + }; + + private onAdEnd = () => { + void this.queueOrSendEvent(EventType.adComplete, { playhead: this._player.currentTime }); + }; + + private onAdSkip = () => { + void this.queueOrSendEvent(EventType.adSkip, { playhead: this._player.currentTime }); + }; + + private onAdBreakEnd = () => { + this._isPlayingAd = false; + this._adPodPosition = 1; + this.startPinger(CONTENT_PING_INTERVAL); + void this.queueOrSendEvent(EventType.adBreakComplete, { playhead: this._player.currentTime }); + }; + + private onBeforeUnload = () => { + void this.maybeEndSession(); + }; + + private async maybeEndSession(): Promise { + this.logDebug(`maybeEndSession`); + if (this.hasSessionStarted()) { + await this.queueOrSendEvent(EventType.sessionEnd, { playhead: this._player.currentTime }); + this._sessionId = undefined; + } + this.reset(); + return Promise.resolve(); + } + + /** + * Start a new session, but only if: + * - no existing session has is in progress; + * - the player has a valid source; + * - no ad is playing, otherwise the ad's media duration will be picked up; + * - the player's content media duration is known. + * + * @param mediaLengthSec + * @private + */ + private async maybeStartSession(mediaLengthSec?: number): Promise { + const mediaLength = this.getContentLength(mediaLengthSec); + const hasValidSource = this._player.source !== undefined; + const hasValidDuration = isValidDuration(mediaLength); + const isPlayingAd = this._player.ads?.playing ?? false; + + this.logDebug( + `maybeStartSession -`, + `mediaLength: ${mediaLength},`, + `hasValidSource: ${hasValidSource},`, + `hasValidDuration: ${hasValidDuration},`, + `isPlayingAd: ${isPlayingAd}`, + ); + + if (this._sessionInProgress || !hasValidSource || !hasValidDuration || isPlayingAd) { + this.logDebug('maybeStartSession - NOT started'); + return; + } + + const sessionDetails = { + ID: 'N/A', + name: this._player?.source?.metadata?.title ?? 'N/A', + channel: 'N/A', + contentType: this.getContentType(), + playerName: 'THEOplayer', + length: mediaLength, + }; + + await this.startSession(sessionDetails, this._customMetadata); + if (!this.hasSessionStarted()) { + return; + } + + this._sessionInProgress = true; + this.logDebug('maybeStartSession - STARTED', `sessionId: ${this.sessionId}`); + + if (!this._isPlayingAd) { + this.startPinger(CONTENT_PING_INTERVAL); + } else { + this.startPinger(AD_PING_INTERVAL); + } + } + + private startPinger(interval: number): void { + if (this._pingInterval !== undefined) { + clearInterval(this._pingInterval); + } + this._pingInterval = setInterval(() => { + // Only send pings if the session has started, never queue them. + if (this.hasSessionStarted()) { + void this.sendMediaEvent(EventType.ping, { playhead: this._player.currentTime }); + } + }, interval); + } + + private getContentLength(mediaLengthSec?: number): number { + return sanitiseContentLength(mediaLengthSec !== undefined ? mediaLengthSec : this._player.duration); + } + + private getContentType(): ContentType { + return this._player.duration === Infinity ? ContentType.LIVE : ContentType.VOD; + } + + reset(): void { + this.logDebug('reset'); + this._adBreakPodIndex = 0; + this._adPodPosition = 1; + this._isPlayingAd = false; + this._sessionInProgress = false; + clearInterval(this._pingInterval); + this._pingInterval = undefined; + this._currentChapter = undefined; + this._sessionId = undefined; + this._hasSessionFailed = false; + this._eventQueue = []; + } + + async destroy(): Promise { + await this.maybeEndSession(); + this.removeEventListeners(); + } + + setDebug(debug: boolean) { + this._debug = debug; + this._alloyClient('setDebug', { enabled: debug }); + } + + get sessionId(): string | undefined { + return this._sessionId; + } + + hasSessionStarted(): boolean { + return this._sessionId !== undefined; + } + + hasSessionFailed(): boolean { + return this._hasSessionFailed; + } + + async setError(errorDetails: AdobeErrorDetails, qoeDataDetails?: AdobeQoeDataDetails) { + return this.queueOrSendEvent(EventType.error, { playhead: this._player.currentTime, qoeDataDetails, errorDetails }); + } + + async statesUpdate( + playhead: number | undefined, + statesStart?: AdobePlayerStateData[], + statesEnd?: AdobePlayerStateData[], + qoeDataDetails?: AdobeQoeDataDetails, + ) { + return this.queueOrSendEvent(EventType.statesUpdate, { + playhead, + qoeDataDetails, + statesStart, + statesEnd, + }); + } + + /** + * Start a manually-tracked media sessions. + * + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/createmediasession} + */ + async startSession(sessionDetails: AdobeSessionDetails, customMetadata?: AdobeCustomMetadataDetails[], qoeDataDetails?: AdobeQoeDataDetails) { + try { + const result = await this._alloyClient('createMediaSession', { + xdm: { + eventType: EventType.sessionStart, + timestamp: new Date().toISOString(), + mediaCollection: { + playhead: 0, + sessionDetails, + qoeDataDetails, + customMetadata, + }, + }, + }); + + this._sessionId = result.sessionId; + + // empty queue + if (this._sessionId && this._eventQueue.length !== 0) { + this._eventQueue.forEach((doPostEvent) => doPostEvent()); + this._eventQueue = []; + } + } catch (e) { + console.error(TAG, `Failed to start session. ${JSON.stringify(e)}`); + this._hasSessionFailed = true; + } + } + + async queueOrSendEvent(eventType: EventType, mediaDetails: AdobeMediaDetails) { + // Do not bother queueing the event in case starting the session has failed + if (this.hasSessionFailed()) { + return; + } + const doPostEvent = () => { + return this.sendMediaEvent(eventType, mediaDetails); + }; + + // If the session has already started, do not queue but send it directly. + if (!this.hasSessionStarted()) { + this._eventQueue.push(doPostEvent); + } else { + return doPostEvent(); + } + } + + /** + * Use the sendMediaEvent command to track media playbacks, pauses, completions, player state updates, and other + * related events. + * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/sendmediaevent} + */ + private async sendMediaEvent(eventType: EventType, mediaDetails: AdobeMediaDetails) { + // Make sure we are positing data with a valid sessionID. + if (!this._sessionId) { + console.error(TAG, 'Invalid sessionID'); + return; + } + + try { + this._alloyClient('sendMediaEvent', { + xdm: { + eventType, + mediaCollection: { + ...mediaDetails, + playhead: sanitisePlayhead(mediaDetails.playhead), + sessionID: this._sessionId, + }, + }, + }); + } catch (e) { + console.error(TAG, `Failed to send event: ${JSON.stringify(e)}`); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private logDebug(message?: any, ...optionalParams: any[]) { + if (this._debug) { + console.debug(TAG, message, ...optionalParams); + } + } +} + +function findAlloyClient(datastreamId: string, orgId: string): AlloyClient | undefined { + return createdClients.find((client) => client.datastreamId === datastreamId && client.orgId === orgId)?.client; +} diff --git a/adobe-edge/src/internal/EventType.ts b/adobe-edge/src/internal/web/EventType.ts similarity index 100% rename from adobe-edge/src/internal/EventType.ts rename to adobe-edge/src/internal/web/EventType.ts diff --git a/adobe-edge/src/internal/web/MediaEdge.d.ts b/adobe-edge/src/internal/web/MediaEdge.d.ts deleted file mode 100644 index 78596e9e..00000000 --- a/adobe-edge/src/internal/web/MediaEdge.d.ts +++ /dev/null @@ -1,1614 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - '/adBreakComplete': { - /** @description Signals the completion of an ad break */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.adBreakComplete */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/adBreakStart': { - /** @description Signals the start of an ad break */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - advertisingPodDetails: { - /** @description The friendly name of the Ad Break. */ - friendlyName?: string; - /** @description The index of the ad break inside the content starting at 1. */ - index: number; - /** @description The offset of the ad break inside the content, in seconds. */ - offset: number; - }; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - customMetadata?: { - name?: string; - value?: string; - }[]; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.adBreakStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/adComplete': { - /** @description Signals the completion of an ad */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.adComplete */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/adSkip': { - /** @description Signals an ad skip */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.adSkip */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/adStart': { - /** @description Signals the start of an ad */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - advertisingDetails: { - /** @description ID of the ad. (Any integer and/or letter combination) */ - name: string; - /** @description Company/Brand whose product is featured in the ad */ - advertiser?: string; - /** @description ID of the ad campaign */ - campaignID?: string; - /** @description ID of the ad creative */ - creativeID?: string; - /** @description URL of the ad creative */ - creativeURL?: string; - /** @description Length of video ad in seconds */ - length: number; - /** @description Placement ID of the ad */ - placementID?: string; - /** @description Friendly name of the ad */ - friendlyName?: string; - /** @description The name of the player responsible for rendering the ad */ - playerName: string; - /** @description ID of the ad site */ - siteID?: string; - /** @description The position (index) of the ad inside the parent ad break. The first ad has index 0, the second ad has index 1 etc */ - podPosition: number; - }; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - customMetadata?: { - name?: string; - value?: string; - }[]; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.adStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/bitrateChange': { - /** @description Sent when the bitrage changes */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.bitrateChange */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/bufferStart': { - /** @description Sent when buffering starts. Note: Because there is no bufferResume event type, it is inferred when you send a play event after bufferStart. */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.bufferStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/chapterComplete': { - /** @description Signals the completion of a chapter */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.chapterComplete */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/chapterSkip': { - /** @description Signals a chapter skip */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.chapterSkip */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/chapterStart': { - /** @description Signals the start of a chapter segment */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - chapterDetails: { - /** @description The offset of the chapter inside the content (in seconds) from the start */ - offset: number; - /** @description The length of the chapter, in seconds */ - length: number; - /** @description The position (index, integer) of the chapter inside the content */ - index: number; - /** @description The name of the chapter and/or segment */ - friendlyName?: string; - }; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - customMetadata?: { - name?: string; - value?: string; - }[]; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.chapterStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/error': { - /** @description Signals that an error has occurred */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - errorDetails: { - name: string; - /** @enum {string} */ - source: 'player' | 'external'; - }; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.error */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/pauseStart': { - /** @description Sent when the user presses Pause. Because there is no resume event type, it is inferred when you send a play event after a pauseStart. */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.pauseStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/ping': { - /** @description Use the Ping request during main content playback in cases when content must be sent every 10 seconds, regardless of other API events that have been sent. The first ping event should fire 10 seconds after main content playback has begun. For ad content, it must be sent every 1 second during ad tracking. */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.ping */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/play': { - /** @description Sent when the player changes state to "playing" from another state, such as when the on ('Playing') callback is triggered by the player. Other states from which the player moves to "playing" include "buffering", when the user resumes from "paused", when the player recovers from an error, and during autoplay. */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.play */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/sessionComplete': { - /** @description Sent when the end of the main content is reached */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.sessionComplete */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/sessionEnd': { - /** @description Notifies the Media Analytics backend to immediately close the session when the user has abandoned their viewing of the content and they are unlikely to return. If you don't send a sessionEnd, an abandoned session will time-out normally (after no events are received for 10 minutes, or when no playhead movement occurs for 30 minutes), and the session is deleted by the backend. */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.sessionEnd */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; - '/sessionStart': { - /** @description Signals the start of a new session. When the response returns, the "sessionId" must be extracted and sent for all subsequent event calls to the Edge API server. */ - post: { - parameters: { - query: { - /** @description The datastream id */ - configId: string; - }; - }; - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - sessionDetails: { - adLoad?: string; - /** @description The SDK version used by the player. This could have any custom value that makes sense for your player */ - appVersion?: string; - artist?: string; - /** @description Rating as defined by TV Parental Guidelines */ - rating?: string; - /** @description Program/Series Name. Program Name is required only if the show is part of a series. */ - show?: string; - /** @description Distribution Station/Channels or where the content is played. Any string value is accepted here */ - channel: string; - /** @description The number of the episode */ - episode?: string; - /** @description Creator of the content */ - originator?: string; - /** @description The date when the content first aired on television. Any date format is acceptable, but Adobe recommends: YYYY-MM-DD */ - firstAirDate?: string; - /** - * @description Identifies the stream type - * @enum {string} - */ - streamType?: 'audio' | 'video'; - /** @description The user has been authorized via Adobe authentication */ - authorized?: string; - hasResume?: boolean; - /** @description Format of the stream (HD, SD) */ - streamFormat?: string; - /** @description Name / ID of the radio station */ - station?: string; - /** @description Type or grouping of content as defined by content producer. Values should be comma delimited in variable implementation. In reporting, the list eVar will split each value into a line item, with each line item receiving equal metrics weight */ - genre?: string; - /** @description The season number the show belongs to. Season Series is required only if the show is part of a series */ - season?: string; - showType?: string; - /** @description Available values per Stream Type: Audio - "song", "podcast", "audiobook", "radio"; Video: "VoD", "Live", "Linear", "UGC", "DVoD" Customers can provide custom values for this parameter */ - contentType: string; - /** @description This is the "friendly" (human-readable) name of the content */ - friendlyName?: string; - /** @description Name of the player */ - playerName: string; - /** @description Name of the author (of an audiobook) */ - author?: string; - album?: string; - /** @description Clip Length/Runtime - This is the maximum length (or duration) of the content being consumed (in seconds) */ - length: number; - /** @description A property that defines the time of the day when the content was broadcast or played. This could have any value set as necessary by customers */ - dayPart?: string; - /** @description Name of the record label */ - label?: string; - /** @description MVPD provided via Adobe authentication. */ - mvpd?: string; - /** @description Type of feed */ - feed?: string; - /** @description This is the unique identifier for the content of the media asset, such as the TV series episode identifier, movie asset identifier, or live event identifier. Typically these IDs are derived from metadata authorities such as EIDR, TMS/Gracenote, or Rovi. These identifiers can also be from other proprietary or in-house systems. */ - assetID?: string; - /** @description Content ID of the content, which can be used to tie back to other industry / CMS IDs */ - name: string; - /** @description Name of the audio content publisher */ - publisher?: string; - /** @description The date when the content first aired on any digital channel or platform. Any date format is acceptable but Adobe recommends: YYYY-MM-DD */ - firstDigitalDate?: string; - /** @description The network/channel name */ - network?: string; - /** @description Set to true when the hit is generated due to playing a downloaded content media session. Not present when downloaded content is not played. */ - isDownloaded?: boolean; - }; - customMetadata?: { - name?: string; - value?: string; - }[]; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - implementationDetails?: { - version?: string; - }; - identityMap?: { - FPID?: { - id?: string; - /** - * @default ambiguous - * @enum {string} - */ - authenticatedState?: 'ambiguous' | 'authenticated' | 'loggedOut'; - primary?: boolean; - }[]; - }; - /** @default media.sessionStart */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - 'application/json': { - /** @description The request ID. */ - requestId?: string; - handle?: { - payload?: { - /** @description The session ID generated for the media session that must be added for all subsequent calls of the same session */ - sessionId?: string; - }[]; - type?: string; - /** Format: int32 */ - eventIndex?: number; - }[]; - }; - }; - }; - /** @description Multi-Status */ - 207: { - content: { - 'application/json': { - /** @description The request ID. */ - requestId?: string; - handle?: { - payload?: Record[]; - type?: string; - /** Format: int32 */ - eventIndex?: number; - }[]; - /** @description Errors generated by the upstreams configured for the datastream */ - errors?: { - type?: string; - status?: number; - title?: string; - report?: { - /** Format: int32 */ - eventIndex?: number; - report?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }[]; - }; - }; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - }; - }; - }; - }; - }; - }; - }; - '/statesUpdate': { - /** @description Signals that one or multiple states are started and/or ended */ - post: { - requestBody?: { - content: { - 'application/json': { - events?: { - xdm: { - mediaCollection: { - playhead: number; - statesStart?: { - name: string; - }[]; - statesEnd?: { - name: string; - }[]; - /** @description The sessionID generated on sessionStart */ - sessionID: string; - qoeDataDetails?: { - /** Format: int32 */ - bitrate?: number; - /** Format: int32 */ - droppedFrames?: number; - /** Format: int32 */ - framesPerSecond?: number; - /** Format: int32 */ - timeToStart?: number; - }; - }; - /** @default media.statesUpdate */ - eventType: string; - /** Format: date-time */ - timestamp: string; - }; - }[]; - }; - }; - }; - responses: { - /** @description No content */ - 204: { - content: never; - }; - /** @description Bad request */ - 400: { - content: { - 'application/json': { - type?: string; - /** @default 400 */ - status?: number; - title?: string; - /** @description Error that caused the 400 status */ - detail?: string; - report?: { - requestId?: string; - details?: { - /** @description Field that contains the error */ - name?: string; - /** @description Error for that specific field */ - reason?: string; - }[]; - }; - }; - }; - }; - /** @description Not found */ - 404: { - content: { - 'application/json': { - type?: string; - /** @default 404 */ - status?: number; - title?: string; - detail?: string; - report?: { - requestId?: string; - /** @description Error that caused the 404 status */ - details?: string; - }; - }; - }; - }; - }; - }; - }; -} - -export type webhooks = Record; - -export type components = Record; - -export type $defs = Record; - -export type external = Record; - -export type operations = Record; diff --git a/adobe-edge/src/internal/web/MediaEdgeAPI.ts b/adobe-edge/src/internal/web/MediaEdgeAPI.ts deleted file mode 100644 index 0dcb6035..00000000 --- a/adobe-edge/src/internal/web/MediaEdgeAPI.ts +++ /dev/null @@ -1,295 +0,0 @@ -import type { paths } from './MediaEdge'; -import type { - AdobeAdvertisingDetails, - AdobeAdvertisingPodDetails, - AdobeChapterDetails, - AdobeCustomMetadataDetails, - AdobeErrorDetails, - AdobeMediaDetails, - AdobeQoeDataDetails, - AdobeSessionDetails, -} from '@theoplayer/react-native-analytics-adobe-edge'; -import { pathToEventTypeMap } from './PathToEventTypeMap'; -import type { AdobePlayerStateData } from '../../api/details/AdobePlayerStateData'; -import { sanitisePlayhead } from './Utils'; -import { createInstance } from '@adobe/alloy'; -import { AdobeEdgeWebConfig } from '../../api/AdobeEdgeWebConfig'; - -const TAG = 'AdobeEdge'; - -// eslint-disable-next-line @typescript-eslint/ban-types -type AlloyClient = Function; - -/** - * Alloy globally stores clients by name. We are allowed create clients with the same config only once. - */ -interface ClientDescription { - datastreamId: string; - orgId: string; - client: AlloyClient; -} -const createdClients: ClientDescription[] = []; - -/** - * The MediaEdgeAPI class is responsible for communicating media events to Adobe Experience Platform. - * - * Event handling for manually-tracked sessions is used. In this mode you need to pass the sessionID to the media event, - * along with the playhead value (integer value). You could also pass the Quality of Experience data details, if needed. - * - * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/js-overview} - * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/streamingmedia} - */ -export class MediaEdgeAPI { - private _sessionId: string | undefined; - private _hasSessionFailed: boolean; - private _eventQueue: (() => Promise)[] = []; - private readonly _alloyClient: AlloyClient; - - constructor(config: AdobeEdgeWebConfig) { - this._hasSessionFailed = false; - const sanitisedConfig = sanitiseConfig(config); - const { datastreamId, orgId, debugEnabled } = sanitisedConfig; - - this._alloyClient = findAlloyClient(datastreamId, orgId); - if (!this._alloyClient) { - this._alloyClient = createInstance({ - name: 'alloy', - monitors: [ - { - // Optionally configure callbacks. - // onInstanceConfigured: function (data: any) {}, - }, - ], - }); - this._alloyClient('configure', sanitisedConfig); - - // Store created client to prevent creating duplicates. - createdClients.push({ datastreamId, orgId, client: this._alloyClient }); - } - this.setDebug(debugEnabled); - } - - /** - * The appendIdentityToUrl command allows you to add a user identifier to the URL as a query string. - * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/appendidentitytourl} - */ - appendIdentityToUrl(url: string) { - this._alloyClient('appendIdentityToUrl', { url }); - } - - setDebug(debug: boolean) { - this._alloyClient('setDebug', { enabled: debug }); - } - - get sessionId(): string | undefined { - return this._sessionId; - } - - hasSessionStarted(): boolean { - return this._sessionId !== undefined; - } - - hasSessionFailed(): boolean { - return this._hasSessionFailed; - } - - reset() { - this._sessionId = undefined; - this._hasSessionFailed = false; - this._eventQueue = []; - } - - async play(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/play', { playhead, qoeDataDetails }); - } - - async pause(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/pauseStart', { playhead, qoeDataDetails }); - } - - async error(playhead: number | undefined, errorDetails: AdobeErrorDetails, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/error', { playhead, qoeDataDetails, errorDetails }); - } - - async ping(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - // Only send pings if the session has started, never queue them. - if (this.hasSessionStarted()) { - void this.sendMediaEvent('/ping', { playhead, qoeDataDetails }); - } - } - - async bufferStart(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/bufferStart', { playhead, qoeDataDetails }); - } - - async sessionComplete(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/sessionComplete', { playhead, qoeDataDetails }); - } - - async sessionEnd(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - await this.maybeQueueEvent('/sessionEnd', { playhead, qoeDataDetails }); - this._sessionId = undefined; - } - - async statesUpdate( - playhead: number | undefined, - statesStart?: AdobePlayerStateData[], - statesEnd?: AdobePlayerStateData[], - qoeDataDetails?: AdobeQoeDataDetails, - ) { - return this.maybeQueueEvent('/statesUpdate', { - playhead, - qoeDataDetails, - statesStart, - statesEnd, - }); - } - - async bitrateChange(playhead: number | undefined, qoeDataDetails: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/bitrateChange', { playhead, qoeDataDetails }); - } - - async chapterSkip(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/chapterSkip', { playhead, qoeDataDetails }); - } - - async chapterStart( - playhead: number | undefined, - chapterDetails: AdobeChapterDetails, - customMetadata?: AdobeCustomMetadataDetails[], - qoeDataDetails?: AdobeQoeDataDetails, - ) { - return this.maybeQueueEvent('/chapterStart', { playhead, chapterDetails, customMetadata, qoeDataDetails }); - } - - async chapterComplete(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/chapterComplete', { playhead, qoeDataDetails }); - } - - async adBreakStart(playhead: number, advertisingPodDetails: AdobeAdvertisingPodDetails, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/adBreakStart', { - playhead, - qoeDataDetails, - advertisingPodDetails, - }); - } - - async adBreakComplete(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/adBreakComplete', { playhead, qoeDataDetails }); - } - - async adStart( - playhead: number, - advertisingDetails: AdobeAdvertisingDetails, - customMetadata?: AdobeCustomMetadataDetails[], - qoeDataDetails?: AdobeQoeDataDetails, - ) { - return this.maybeQueueEvent('/adStart', { - playhead, - qoeDataDetails, - advertisingDetails, - customMetadata, - }); - } - - async adSkip(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/adSkip', { playhead, qoeDataDetails }); - } - - async adComplete(playhead: number | undefined, qoeDataDetails?: AdobeQoeDataDetails) { - return this.maybeQueueEvent('/adComplete', { playhead, qoeDataDetails }); - } - - /** - * Start a manually-tracked media sessions. - * - * {@link }https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/createmediasession} - */ - async startSession(sessionDetails: AdobeSessionDetails, customMetadata?: AdobeCustomMetadataDetails[], qoeDataDetails?: AdobeQoeDataDetails) { - try { - const result = await this._alloyClient('createMediaSession', { - xdm: { - eventType: pathToEventTypeMap['/sessionStart'], - timestamp: new Date().toISOString(), - mediaCollection: { - playhead: 0, - sessionDetails, - qoeDataDetails, - customMetadata, - }, - }, - }); - - this._sessionId = result.sessionId; - - // empty queue - if (this._sessionId && this._eventQueue.length !== 0) { - this._eventQueue.forEach((doPostEvent) => doPostEvent()); - this._eventQueue = []; - } - } catch (e) { - console.error(TAG, `Failed to start session. ${JSON.stringify(e)}`); - this._hasSessionFailed = true; - } - } - - async maybeQueueEvent(path: keyof paths, mediaDetails: AdobeMediaDetails) { - // Do not bother queueing the event in case starting the session has failed - if (this.hasSessionFailed()) { - return; - } - const doPostEvent = () => { - return this.sendMediaEvent(path, mediaDetails); - }; - - // If the session has already started, do not queue but send it directly. - if (!this.hasSessionStarted()) { - this._eventQueue.push(doPostEvent); - } else { - return doPostEvent(); - } - } - - /** - * Use the sendMediaEvent command to track media playbacks, pauses, completions, player state updates, and other - * related events. - * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/sendmediaevent} - */ - private async sendMediaEvent(path: keyof paths, mediaDetails: AdobeMediaDetails) { - // Make sure we are positing data with a valid sessionID. - if (!this._sessionId) { - console.error(TAG, 'Invalid sessionID'); - return; - } - - try { - this._alloyClient('sendMediaEvent', { - xdm: { - eventType: pathToEventTypeMap[path], - mediaCollection: { - ...mediaDetails, - playhead: sanitisePlayhead(mediaDetails.playhead), - sessionID: this._sessionId, - }, - }, - }); - } catch (e) { - console.error(TAG, `Failed to send event: ${JSON.stringify(e)}`); - } - } -} - -function findAlloyClient(datastreamId: string, orgId: string): AlloyClient | undefined { - return createdClients.find((client) => client.datastreamId === datastreamId && client.orgId === orgId)?.client; -} - -function sanitiseConfig(config: AdobeEdgeWebConfig): AdobeEdgeWebConfig { - return { - ...config, - streamingMedia: { - ...config.streamingMedia, - channel: config.streamingMedia?.channel || 'defaultChannel', - playerName: config.streamingMedia?.playerName || 'THEOplayer', - }, - }; -} diff --git a/adobe-edge/src/internal/web/PathToEventTypeMap.ts b/adobe-edge/src/internal/web/PathToEventTypeMap.ts deleted file mode 100644 index 138694db..00000000 --- a/adobe-edge/src/internal/web/PathToEventTypeMap.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { paths } from './MediaEdge'; -import { EventType } from '../EventType'; - -export const pathToEventTypeMap: Record = { - '/adBreakComplete': EventType.adBreakComplete, - '/adBreakStart': EventType.adBreakStart, - '/adComplete': EventType.adComplete, - '/adSkip': EventType.adSkip, - '/adStart': EventType.adStart, - '/bitrateChange': EventType.bitrateChange, - '/bufferStart': EventType.bufferStart, - '/chapterComplete': EventType.chapterComplete, - '/chapterSkip': EventType.chapterSkip, - '/chapterStart': EventType.chapterStart, - '/error': EventType.error, - '/pauseStart': EventType.pauseStart, - '/ping': EventType.ping, - '/play': EventType.play, - '/sessionComplete': EventType.sessionComplete, - '/sessionEnd': EventType.sessionEnd, - '/sessionStart': EventType.sessionStart, - '/statesUpdate': EventType.statesUpdate, -}; diff --git a/adobe-edge/src/internal/web/Utils.ts b/adobe-edge/src/internal/web/Utils.ts index 6998a5a7..80aa2582 100644 --- a/adobe-edge/src/internal/web/Utils.ts +++ b/adobe-edge/src/internal/web/Utils.ts @@ -1,25 +1,53 @@ import type { Ad, AdBreak, TextTrackCue } from 'react-native-theoplayer'; -import type { AdobeAdvertisingDetails, AdobeAdvertisingPodDetails, AdobeChapterDetails } from '@theoplayer/react-native-analytics-adobe-edge'; +import type { + AdobeAdvertisingDetails, + AdobeAdvertisingPodDetails, + AdobeChapterDetails, + AdobeEdgeWebConfig, +} from '@theoplayer/react-native-analytics-adobe-edge'; -export function sanitisePlayhead(playheadInMsec?: number): number { - if (!playheadInMsec) { +/** + * Sanitise the current playhead in seconds. Adobe expects an integer value. + * + * - If undefined or NaN, set it to 0. + * - If infinite (live stream), set it to the current second of the day. + * + * @param playheadInSec + */ +export function sanitisePlayhead(playheadInSec?: number): number { + if (!playheadInSec || isNaN(playheadInSec)) { return 0; } - if (playheadInMsec === Infinity) { + if (playheadInSec === Infinity) { // If content is live, the playhead must be the current second of the day. const date = new Date(); return date.getSeconds() + 60 * (date.getMinutes() + 60 * date.getHours()); } - return Math.trunc(playheadInMsec / 1000); + return Math.trunc(playheadInSec); } /** - * Sanitise the current media length in seconds. + * Sanitise the current media length. * * - In case of a live stream, set it to 24h. */ -export function sanitiseContentLength(mediaLengthMsec: number): number { - return mediaLengthMsec === Infinity ? 86400 : Math.trunc(1e-3 * mediaLengthMsec); +export function sanitiseContentLength(mediaLengthSec: number): number { + return mediaLengthSec === Infinity ? 86400 : Math.trunc(mediaLengthSec); +} + +export function sanitiseConfig(config: AdobeEdgeWebConfig): AdobeEdgeWebConfig { + return { + ...config, + streamingMedia: { + ...config.streamingMedia, + channel: config.streamingMedia?.channel || 'defaultChannel', + playerName: config.streamingMedia?.playerName || 'THEOplayer', + }, + }; +} + +export function isValidDuration(v: number | undefined): boolean { + return v !== undefined && !Number.isNaN(v); } export function calculateAdvertisingPodDetails(adBreak: AdBreak, lastPodIndex: number): AdobeAdvertisingPodDetails { @@ -42,7 +70,7 @@ export function calculateAdvertisingDetails(ad: Ad, podPosition: number): AdobeA return { podPosition, length: ad.duration ? Math.trunc(ad.duration) : 0, - name: 'NA', // TODO + name: 'NA', playerName: 'THEOplayer', }; } @@ -51,8 +79,8 @@ export function calculateChapterDetails(cue: TextTrackCue): AdobeChapterDetails const id = Number(cue.id); const index = isNaN(id) ? 0 : id; return { - length: Math.trunc((cue.endTime - cue.startTime) / 1000), - offset: Math.trunc(cue.startTime / 1000), + length: Math.trunc(cue.endTime - cue.startTime), + offset: Math.trunc(cue.startTime), index, }; } diff --git a/adobe-edge/src/internal/web/media-edge-0.1.json b/adobe-edge/src/internal/web/media-edge-0.1.json deleted file mode 100644 index c44330cc..00000000 --- a/adobe-edge/src/internal/web/media-edge-0.1.json +++ /dev/null @@ -1,3688 +0,0 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "Media Analytics Edge API", - "description": "The OpenAPI specification for Media Analytics Edge API", - "version": "0.1" - }, - "servers": [ - { - "url": "https://edge.adobedc.net/ee-pre-prd/va/v1" - }, - { - "url": "https://edge.adobedc.net/ee/va/v1" - } - ], - "paths": { - "/adBreakComplete": { - "post": { - "description": "Signals the completion of an ad break", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.adBreakComplete" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.adBreakComplete\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/adBreakStart": { - "post": { - "description": "Signals the start of an ad break", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "advertisingPodDetails": { - "type": "object", - "properties": { - "friendlyName": { - "type": "string", - "description": "The friendly name of the Ad Break." - }, - "index": { - "type": "integer", - "description": "The index of the ad break inside the content starting at 1." - }, - "offset": { - "type": "integer", - "description": "The offset of the ad break inside the content, in seconds." - } - }, - "required": ["index", "offset"] - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "customMetadata": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - } - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["advertisingPodDetails", "playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.adBreakStart" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.adBreakStart\",\n \"mediaCollection\": {\n \"advertisingPodDetails\": {\n \"friendlyName\": \"Mid-roll\",\n \"offset\": 0,\n \"index\": 1\n },\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 15\n },\n \"timestamp\": \"2022-03-04T13:38:15+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.advertisingPodDetails.offset\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/adComplete": { - "post": { - "description": "Signals the completion of an ad", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.adComplete" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.adComplete\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/adSkip": { - "post": { - "description": "Signals an ad skip", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.adSkip" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.adSkip\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/adStart": { - "post": { - "description": "Signals the start of an ad", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "advertisingDetails": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "ID of the ad. (Any integer and/or letter combination)" - }, - "advertiser": { - "type": "string", - "description": "Company/Brand whose product is featured in the ad" - }, - "campaignID": { - "type": "string", - "description": "ID of the ad campaign" - }, - "creativeID": { - "type": "string", - "description": "ID of the ad creative" - }, - "creativeURL": { - "type": "string", - "description": "URL of the ad creative" - }, - "length": { - "type": "integer", - "description": "Length of video ad in seconds" - }, - "placementID": { - "type": "string", - "description": "Placement ID of the ad" - }, - "friendlyName": { - "type": "string", - "description": "Friendly name of the ad" - }, - "playerName": { - "type": "string", - "description": "The name of the player responsible for rendering the ad" - }, - "siteID": { - "type": "string", - "description": "ID of the ad site" - }, - "podPosition": { - "type": "integer", - "description": "The position (index) of the ad inside the parent ad break. The first ad has index 0, the second ad has index 1 etc" - } - }, - "required": ["name", "length", "playerName", "podPosition"] - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "customMetadata": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - } - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["advertisingDetails", "playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.adStart" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.adStart\",\n \"mediaCollection\": {\n \"advertisingDetails\": {\n \"friendlyName\": \"Ad 1\",\n \"name\": \"/uri-reference/001\",\n \"length\": 10,\n \"advertiser\": \"Adobe Marketing\",\n \"campaignID\": \"Adobe Analytics\",\n \"creativeID\": \"creativeID\",\n \"creativeURL\": \"https://creativeurl.com\",\n \"placementID\": \"placementID\",\n \"siteID\": \"siteID\",\n \"podPosition\": 11,\n \"playerName\": \"HTML5 player\"\n },\n \"customMetadata\": [\n {\n \"name\": \"myCustomValue3\",\n \"value\": \"c3\"\n },\n {\n \"name\": \"myCustomValue2\",\n \"value\": \"c2\"\n },\n {\n \"name\": \"myCustomValue1\",\n \"value\": \"c1\"\n }\n ],\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 15\n },\n \"timestamp\": \"2022-03-04T13:38:26+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.advertisingDetails.name\",\n \"reason\": \"Missing required field\"\n },\n {\n \"name\": \"$.events[0].xdm.mediaCollection.advertisingDetails.length\",\n \"reason\": \"Missing required field\"\n },\n {\n \"name\": \"$.events[0].xdm.mediaCollection.advertisingDetails.podPosition\",\n \"reason\": \"Missing required field\"\n },\n {\n \"name\": \"$.events[0].xdm.mediaCollection.sessionID\",\n \"reason\": \"Unexpected error. Hint: InvalidApiSidLength=63\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/bitrateChange": { - "post": { - "description": "Sent when the bitrage changes", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["qoeDataDetails", "playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.bitrateChange" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.bitrateChange\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 30,\n \"qoeDataDetails\": {\n \"framesPerSecond\": 1,\n \"bitrate\": 35000,\n \"droppedFrames\": 30,\n \"timeToStart\": 1364\n }\n },\n \"timestamp\": \"2022-03-04T13:38:40+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.qoeDataDetails\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/bufferStart": { - "post": { - "description": "Sent when buffering starts. Note: Because there is no bufferResume event type, it is inferred when you send a play event after bufferStart.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.bufferStart" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.bufferStart\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/chapterComplete": { - "post": { - "description": "Signals the completion of a chapter", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.chapterComplete" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.chapterComplete\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/chapterSkip": { - "post": { - "description": "Signals a chapter skip", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.chapterSkip" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.chapterSkip\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/chapterStart": { - "post": { - "description": "Signals the start of a chapter segment", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "chapterDetails": { - "type": "object", - "properties": { - "offset": { - "type": "integer", - "description": "The offset of the chapter inside the content (in seconds) from the start" - }, - "length": { - "type": "integer", - "description": "The length of the chapter, in seconds" - }, - "index": { - "type": "integer", - "description": "The position (index, integer) of the chapter inside the content" - }, - "friendlyName": { - "type": "string", - "description": "The name of the chapter and/or segment" - } - }, - "required": ["index", "length", "offset"] - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "customMetadata": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - } - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["chapterDetails", "playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.chapterStart" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.chapterStart\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 10,\n \"chapterDetails\": {\n \"friendlyName\": \"Chapter 1\",\n \"length\": 10,\n \"index\": 1,\n \"offset\": 0\n }\n },\n \"timestamp\": \"2022-03-04T13:37:56+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.chapterDetails.index\",\n \"reason\": \"Missing required field\"\n },\n {\n \"name\": \"$.events[0].xdm.mediaCollection.chapterDetails.length\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/error": { - "post": { - "description": "Signals that an error has occurred", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "errorDetails": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "source": { - "type": "string", - "enum": ["player", "external"] - } - }, - "required": ["name", "source"] - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["errorDetails", "playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.error" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.error\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 35,\n \"qoeDataDetails\": {\n \"bitrate\": 35000,\n \"droppedFrames\": 30\n },\n \"errorDetails\": {\n \"name\": \"test-buffer-start\",\n \"source\": \"player\"\n }\n },\n \"timestamp\": \"2022-03-04T13:39:15+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.errorDetails.name\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/pauseStart": { - "post": { - "description": "Sent when the user presses Pause. Because there is no resume event type, it is inferred when you send a play event after a pauseStart.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.pauseStart" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.pauseStart\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/ping": { - "post": { - "description": "Use the Ping request during main content playback in cases when content must be sent every 10 seconds, regardless of other API events that have been sent. The first ping event should fire 10 seconds after main content playback has begun. For ad content, it must be sent every 1 second during ad tracking.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.ping" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.ping\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/play": { - "post": { - "description": "Sent when the player changes state to \"playing\" from another state, such as when the on ('Playing') callback is triggered by the player. Other states from which the player moves to \"playing\" include \"buffering\", when the user resumes from \"paused\", when the player recovers from an error, and during autoplay.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.play" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.play\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/sessionComplete": { - "post": { - "description": "Sent when the end of the main content is reached", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.sessionComplete" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.sessionComplete\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/sessionEnd": { - "post": { - "description": "Notifies the Media Analytics backend to immediately close the session when the user has abandoned their viewing of the content and they are unlikely to return. If you don't send a sessionEnd, an abandoned session will time-out normally (after no events are received for 10 minutes, or when no playhead movement occurs for 30 minutes), and the session is deleted by the backend.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.sessionEnd" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.sessionEnd\",\n \"mediaCollection\": {\n \"sessionID\": \"5c32e1a6ef6b58be5136ba8db2f79f1d251d3121a898bc8fb60123b8fdb9aa1c\",\n \"playhead\": 25\n },\n \"timestamp\": \"2022-03-04T13:39:00+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection.playhead\",\n \"reason\": \"Missing required field\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - }, - "/sessionStart": { - "post": { - "description": "Signals the start of a new session. When the response returns, the \"sessionId\" must be extracted and sent for all subsequent event calls to the Edge API server.", - "parameters": [ - { - "name": "configId", - "description": "The datastream id", - "in": "query", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "sessionDetails": { - "type": "object", - "properties": { - "adLoad": { - "type": "string" - }, - "appVersion": { - "type": "string", - "description": "The SDK version used by the player. This could have any custom value that makes sense for your player" - }, - "artist": { - "type": "string" - }, - "rating": { - "type": "string", - "description": "Rating as defined by TV Parental Guidelines" - }, - "show": { - "type": "string", - "description": "Program/Series Name. Program Name is required only if the show is part of a series." - }, - "channel": { - "type": "string", - "description": "Distribution Station/Channels or where the content is played. Any string value is accepted here" - }, - "episode": { - "type": "string", - "description": "The number of the episode" - }, - "originator": { - "type": "string", - "description": "Creator of the content" - }, - "firstAirDate": { - "type": "string", - "description": "The date when the content first aired on television. Any date format is acceptable, but Adobe recommends: YYYY-MM-DD" - }, - "streamType": { - "type": "string", - "enum": ["audio", "video"], - "description": "Identifies the stream type" - }, - "authorized": { - "type": "string", - "description": "The user has been authorized via Adobe authentication" - }, - "hasResume": { - "type": "boolean" - }, - "streamFormat": { - "type": "string", - "description": "Format of the stream (HD, SD)" - }, - "station": { - "type": "string", - "description": "Name / ID of the radio station" - }, - "genre": { - "type": "string", - "description": "Type or grouping of content as defined by content producer. Values should be comma delimited in variable implementation. In reporting, the list eVar will split each value into a line item, with each line item receiving equal metrics weight" - }, - "season": { - "type": "string", - "description": "The season number the show belongs to. Season Series is required only if the show is part of a series" - }, - "showType": { - "type": "string" - }, - "contentType": { - "type": "string", - "description": "Available values per Stream Type: Audio - \"song\", \"podcast\", \"audiobook\", \"radio\"; Video: \"VoD\", \"Live\", \"Linear\", \"UGC\", \"DVoD\" Customers can provide custom values for this parameter" - }, - "friendlyName": { - "type": "string", - "description": "This is the \"friendly\" (human-readable) name of the content" - }, - "playerName": { - "type": "string", - "description": "Name of the player" - }, - "author": { - "type": "string", - "description": "Name of the author (of an audiobook)" - }, - "album": { - "type": "string" - }, - "length": { - "type": "integer", - "description": "Clip Length/Runtime - This is the maximum length (or duration) of the content being consumed (in seconds)" - }, - "dayPart": { - "type": "string", - "description": "A property that defines the time of the day when the content was broadcast or played. This could have any value set as necessary by customers" - }, - "label": { - "type": "string", - "description": "Name of the record label" - }, - "mvpd": { - "type": "string", - "description": "MVPD provided via Adobe authentication." - }, - "feed": { - "type": "string", - "description": "Type of feed" - }, - "assetID": { - "type": "string", - "description": "This is the unique identifier for the content of the media asset, such as the TV series episode identifier, movie asset identifier, or live event identifier. Typically these IDs are derived from metadata authorities such as EIDR, TMS/Gracenote, or Rovi. These identifiers can also be from other proprietary or in-house systems." - }, - "name": { - "type": "string", - "description": "Content ID of the content, which can be used to tie back to other industry / CMS IDs" - }, - "publisher": { - "type": "string", - "description": "Name of the audio content publisher" - }, - "firstDigitalDate": { - "type": "string", - "description": "The date when the content first aired on any digital channel or platform. Any date format is acceptable but Adobe recommends: YYYY-MM-DD" - }, - "network": { - "type": "string", - "description": "The network/channel name" - }, - "isDownloaded": { - "type": "boolean", - "description": "Set to true when the hit is generated due to playing a downloaded content media session. Not present when downloaded content is not played." - } - }, - "required": ["name", "playerName", "length", "channel", "contentType"] - }, - "customMetadata": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - } - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionDetails"] - }, - "implementationDetails": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - } - }, - "identityMap": { - "type": "object", - "properties": { - "FPID": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "authenticatedState": { - "type": "string", - "default": "ambiguous", - "enum": ["ambiguous", "authenticated", "loggedOut"] - }, - "primary": { - "type": "boolean" - } - } - } - } - } - }, - "eventType": { - "type": "string", - "default": "media.sessionStart" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"mediaCollection\": {\n \"sessionDetails\": {\n \"dayPart\": \"dayPart\",\n \"mvpd\": \"test-mvpd\",\n \"authorized\": \"true\",\n \"label\": \"test-label\",\n \"station\": \"test-station\",\n \"publisher\": \"test-media-publisher\",\n \"author\": \"test-author\",\n \"name\": \"Friends\",\n \"friendlyName\": \"FriendlyName\",\n \"assetID\": \"/uri-reference\",\n \"originator\": \"David Crane and Marta Kauffman\",\n \"episode\": \"4933\",\n \"genre\": \"Comedy\",\n \"rating\": \"4.8/5\",\n \"season\": \"1521\",\n \"show\": \"Friends Series\",\n \"length\": 100,\n \"firstDigitalDate\": \"releaseDate\",\n \"artist\": \"test-artist\",\n \"hasResume\": false,\n \"album\": \"test-album\",\n \"firstAirDate\": \"firstAirDate\",\n \"showType\": \"sitcom\",\n \"streamFormat\": \"streamFormat\",\n \"streamType\": \"video\",\n \"adLoad\": \"adLoadType\",\n \"channel\": \"broadcastChannel\",\n \"contentType\": \"VOD\",\n \"playerName\": \"HTML5 player\",\n \"appVersion\": \"sdk-1.0\",\n \"feed\": \"sourceFeed\",\n \"network\": \"test-network\"\n },\n \"playhead\": 0,\n \"implementationDetails\": {\n \"version\": \"libraryVersion\"\n },\n \"customMetadata\": [\n {\n \"name\": \"myCustomValue3\",\n \"value\": \"c3\"\n },\n {\n \"name\": \"myCustomValue2\",\n \"value\": \"c2\"\n },\n {\n \"name\": \"myCustomValue1\",\n \"value\": \"c1\"\n }\n ]\n },\n \"timestamp\": \"2023-04-04T11:35:16Z\",\n \"identityMap\": {\n \"FPID\": [\n {\n \"id\": \"CHANGEME\",\n \"authenticatedState\": \"ambiguous\",\n \"primary\": true\n }\n ]\n },\n \"eventType\": \"media.sessionStart\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "requestId": { - "type": "string", - "description": "The request ID." - }, - "handle": { - "type": "array", - "items": { - "type": "object", - "properties": { - "payload": { - "type": "array", - "items": { - "type": "object", - "properties": { - "sessionId": { - "type": "string", - "description": "The session ID generated for the media session that must be added for all subsequent calls of the same session" - } - } - } - }, - "type": { - "type": "string" - }, - "eventIndex": { - "type": "integer", - "format": "int32" - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"requestId\": \"dd850e05-8c3e-4ae4-9ea8-506490978004\",\n \"handle\": [\n {\n \"payload\": [\n {\n \"sessionId\": \"bfba9a5f2986d69a9a9424f6a99702562512eb244f2b65c4f1c1553e7fe9997f\"\n }\n ],\n \"type\": \"media-analytics:new-session\",\n \"eventIndex\": 0\n },\n {\n \"payload\": [\n {\n \"scope\": \"Target\",\n \"hint\": \"34\",\n \"ttlSeconds\": 1800\n },\n {\n \"scope\": \"AAM\",\n \"hint\": \"7\",\n \"ttlSeconds\": 1800\n },\n {\n \"scope\": \"EdgeNetwork\",\n \"hint\": \"va6\",\n \"ttlSeconds\": 1800\n }\n ],\n \"type\": \"locationHint:result\"\n },\n {\n \"payload\": [\n {\n \"key\": \"kndctr_EA0C49475E8AE1870A494023_AdobeOrg_cluster\",\n \"value\": \"va6\",\n \"maxAge\": 1800\n },\n {\n \"key\": \"kndctr_EA0C49475E8AE1870A494023_AdobeOrg_identity\",\n \"value\": \"CiY0Mzg5NTEyNzMzNTUxMDc5MzgzMzU2MjU5NDY5MTY3Mzc3MTc2OFIOCJ-YppX6MBgBKgNWQTbwAZ-YppX6MA==\",\n \"maxAge\": 34128000\n }\n ],\n \"type\": \"state:store\"\n }\n ]\n}" - } - } - } - } - }, - "207": { - "description": "Multi-Status", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "requestId": { - "type": "string", - "description": "The request ID." - }, - "handle": { - "type": "array", - "items": { - "type": "object", - "properties": { - "payload": { - "type": "array", - "items": { - "type": "object" - } - }, - "type": { - "type": "string" - }, - "eventIndex": { - "type": "integer", - "format": "int32" - } - } - } - }, - "errors": { - "type": "array", - "description": "Errors generated by the upstreams configured for the datastream", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer" - }, - "title": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "eventIndex": { - "type": "integer", - "format": "int32" - }, - "report": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\",\n \"handle\": [\n {\n \"payload\": [\n {\n \"scope\": \"Target\",\n \"hint\": \"34\",\n \"ttlSeconds\": 1800\n },\n {\n \"scope\": \"AAM\",\n \"hint\": \"7\",\n \"ttlSeconds\": 1800\n },\n {\n \"scope\": \"EdgeNetwork\",\n \"hint\": \"va6\",\n \"ttlSeconds\": 1800\n }\n ],\n \"type\": \"locationHint:result\"\n },\n {\n \"payload\": [\n {\n \"key\": \"kndctr_EA0C49475E8AE1870A494023_AdobeOrg_cluster\",\n \"value\": \"va6\",\n \"maxAge\": 1800\n },\n {\n \"key\": \"kndctr_EA0C49475E8AE1870A494023_AdobeOrg_identity\",\n \"value\": \"CiY0Mzg5NTEyNzMzNTUxMDc5MzgzMzU2MjU5NDY5MTY3Mzc3MTc2OFIOCI-qtpf6MBgBKgNWQTbwAY-qtpf6MA==\",\n \"maxAge\": 34128000\n }\n ],\n \"type\": \"state:store\"\n }\n ],\n \"errors\": [\n {\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Invalid request\",\n \"report\": {\n \"eventIndex\": 0,\n \"details\": [\n {\n \"name\": \"$.xdm.mediaCollection.sessionDetails.name\",\n \"reason\": \"Missing required field\"\n }\n ]\n }\n }\n ]\n}" - } - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/EXEG-0003-400\",\n \"status\": 400,\n \"title\": \"Invalid datastream ID\",\n \"detail\": \"The datastream ID '66b64400-e418-4184-8fed-b57636d09' referenced in your request does not exist. Update the request with a valid datastream ID and try again.\",\n \"report\": {\n \"requestId\": \"75af7733-9c8a-45a9-b2a1-bb570c58a0da\"\n }\n}" - } - } - } - } - } - } - } - }, - "/statesUpdate": { - "post": { - "description": "Signals that one or multiple states are started and/or ended", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "xdm": { - "type": "object", - "properties": { - "mediaCollection": { - "type": "object", - "properties": { - "playhead": { - "type": "integer" - }, - "statesStart": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "^[a-zA-Z0-9_.]{1,64}$" - } - }, - "required": ["name"] - } - }, - "statesEnd": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "^[a-zA-Z0-9_.]{1,64}$" - } - }, - "required": ["name"] - } - }, - "sessionID": { - "type": "string", - "description": "The sessionID generated on sessionStart" - }, - "qoeDataDetails": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer", - "format": "int32" - }, - "droppedFrames": { - "type": "integer", - "format": "int32" - }, - "framesPerSecond": { - "type": "integer", - "format": "int32" - }, - "timeToStart": { - "type": "integer", - "format": "int32" - } - } - } - }, - "required": ["playhead", "sessionID"] - }, - "eventType": { - "type": "string", - "default": "media.statesUpdate" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": ["mediaCollection", "timestamp", "eventType"] - } - }, - "required": ["xdm"] - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"events\": [\n {\n \"xdm\": {\n \"eventType\": \"media.statesUpdate\",\n \"mediaCollection\": {\n \"sessionID\": \"bfba9a5f2986d69a9a9424f6a99702562512eb244f2b65c4f1c1553e7fe9997f\",\n \"playhead\": 60,\n \"statesStart\": [\n {\n \"name\": \"mute\"\n },\n {\n \"name\": \"pictureInPicture\"\n }\n ],\n \"statesEnd\": [\n {\n \"name\": \"fullScreen\"\n }\n ]\n },\n \"timestamp\": \"2022-03-04T13:40:40+00:00\"\n }\n }\n ]\n}" - } - } - } - } - }, - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 400 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string", - "description": "Error that caused the 400 status" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Field that contains the error" - }, - "reason": { - "type": "string", - "description": "Error for that specific field" - } - } - } - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0400-400\",\n \"status\": 400,\n \"title\": \"Bad Request\",\n \"detail\": \"Invalid request. Please check your input and try again.\",\n \"report\": {\n \"details\": [\n {\n \"name\": \"$.events[0].xdm.mediaCollection\",\n \"reason\": \"Unexpected error. Hint: Empty statesUpdate event. At least one of statesStart or statesEnd list should be non-empty\"\n }\n ],\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "status": { - "type": "integer", - "default": 404 - }, - "title": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "report": { - "type": "object", - "properties": { - "requestId": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Error that caused the 404 status" - } - } - } - } - }, - "examples": { - "0": { - "value": "{\n \"type\": \"https://ns.adobe.com/aep/errors/va-edge-0404-404\",\n \"status\": 404,\n \"title\": \"Not Found\",\n \"detail\": \"The requested resource could not be found but may be available again in the future.\",\n \"report\": {\n \"details\": \"Error processing request. If the session is longer than 24h, please start a new one. Returning Not Found\",\n \"requestId\": \"e3d87437-5054-4bc2-8953-be4be8d0b900\"\n }\n}" - } - } - } - } - } - } - } - } - } -} From a125ff90a3d3a17eb5d09b259f238b1226ade40e Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Mon, 15 Dec 2025 15:01:19 +0100 Subject: [PATCH 29/70] Update types --- .../api/details/AdobeAdvertisingDetails.ts | 2 +- .../api/details/AdobeAdvertisingPodDetails.ts | 2 +- .../src/api/details/AdobeChapterDetails.ts | 2 +- .../api/details/AdobeCustomMetadataDetails.ts | 2 +- .../src/api/details/AdobeErrorDetails.ts | 2 +- .../src/api/details/AdobeIdentityItem.ts | 35 +++++++++++++++++++ .../src/api/details/AdobeIdentityMap.ts | 10 ++++++ .../api/details/AdobeImplementationDetails.ts | 2 +- .../src/api/details/AdobeMediaDetails.ts | 2 +- .../src/api/details/AdobePlayerStateData.ts | 2 +- .../src/api/details/AdobeQoeDataDetails.ts | 2 +- .../src/api/details/AdobeSessionDetails.ts | 2 +- adobe-edge/src/api/details/barrel.ts | 2 ++ 13 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 adobe-edge/src/api/details/AdobeIdentityItem.ts create mode 100644 adobe-edge/src/api/details/AdobeIdentityMap.ts diff --git a/adobe-edge/src/api/details/AdobeAdvertisingDetails.ts b/adobe-edge/src/api/details/AdobeAdvertisingDetails.ts index a7d66c3b..f146769c 100644 --- a/adobe-edge/src/api/details/AdobeAdvertisingDetails.ts +++ b/adobe-edge/src/api/details/AdobeAdvertisingDetails.ts @@ -1,7 +1,7 @@ /** * Advertising details information. * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/advertisingdetails.schema.md + * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/advertisingdetails.schema.md} */ export interface AdobeAdvertisingDetails { // ID of the ad. Any integer and/or letter combination. diff --git a/adobe-edge/src/api/details/AdobeAdvertisingPodDetails.ts b/adobe-edge/src/api/details/AdobeAdvertisingPodDetails.ts index ebaf57d3..a04e3415 100644 --- a/adobe-edge/src/api/details/AdobeAdvertisingPodDetails.ts +++ b/adobe-edge/src/api/details/AdobeAdvertisingPodDetails.ts @@ -1,7 +1,7 @@ /** * Advertising Pod details information. * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/advertisingpoddetails.schema.md + * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/advertisingpoddetails.schema.md} */ export interface AdobeAdvertisingPodDetails { // The ID of the ad break. diff --git a/adobe-edge/src/api/details/AdobeChapterDetails.ts b/adobe-edge/src/api/details/AdobeChapterDetails.ts index 08550d1e..c758e81e 100644 --- a/adobe-edge/src/api/details/AdobeChapterDetails.ts +++ b/adobe-edge/src/api/details/AdobeChapterDetails.ts @@ -1,7 +1,7 @@ /** * Chapter details information. * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/chapterdetails.schema.md + * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/chapterdetails.schema.md} */ export interface AdobeChapterDetails { // The ID of the ad break. diff --git a/adobe-edge/src/api/details/AdobeCustomMetadataDetails.ts b/adobe-edge/src/api/details/AdobeCustomMetadataDetails.ts index 16fec641..1e99e850 100644 --- a/adobe-edge/src/api/details/AdobeCustomMetadataDetails.ts +++ b/adobe-edge/src/api/details/AdobeCustomMetadataDetails.ts @@ -1,7 +1,7 @@ /** * Custom metadata details information. * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/custommetadatadetails.schema.md + * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/custommetadatadetails.schema.md} */ export type AdobeCustomMetadataDetails = { // The name of the custom field. diff --git a/adobe-edge/src/api/details/AdobeErrorDetails.ts b/adobe-edge/src/api/details/AdobeErrorDetails.ts index 25512689..56b265fa 100644 --- a/adobe-edge/src/api/details/AdobeErrorDetails.ts +++ b/adobe-edge/src/api/details/AdobeErrorDetails.ts @@ -1,7 +1,7 @@ /** * Error details information. * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/errordetails.schema.md + * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/errordetails.schema.md} */ export interface AdobeErrorDetails { // The error ID. diff --git a/adobe-edge/src/api/details/AdobeIdentityItem.ts b/adobe-edge/src/api/details/AdobeIdentityItem.ts new file mode 100644 index 00000000..3ab6569f --- /dev/null +++ b/adobe-edge/src/api/details/AdobeIdentityItem.ts @@ -0,0 +1,35 @@ +/** + * The state this identity is authenticated as for this observed ExperienceEvent. + * + * - `ambiguous`: Ambiguous. + * - `authenticated`: User identified by a login or similar action that was valid at the time of the event observation. + * - `loggedOut`: User was identified by a login action at some point of time previously, but is not currently logged in. + */ +export type AuthenticatedState = 'ambiguous' | 'authenticated' | 'loggedOut'; + +/** + * An end user identity item, to be included in an instance of `context/identitymap`. + * + * {@link https://github.com/adobe/xdm/blob/master/components/datatypes/identityitem.schema.json} + */ +export interface AdobeIdentityItem { + /** + * Identity of the consumer in the related namespace. + */ + id: string; + + /** + * The state this identity is authenticated as for this observed ExperienceEvent. + * + * @default `ambiguous`. + */ + authenticatedState: AuthenticatedState; + + /** + * Indicates this identity is the preferred identity. Is used as a hint to help systems better organize how + * identities are queried. + * + * @default `false`. + */ + primary: boolean; +} diff --git a/adobe-edge/src/api/details/AdobeIdentityMap.ts b/adobe-edge/src/api/details/AdobeIdentityMap.ts new file mode 100644 index 00000000..bb68ef13 --- /dev/null +++ b/adobe-edge/src/api/details/AdobeIdentityMap.ts @@ -0,0 +1,10 @@ +import { AdobeIdentityItem } from './AdobeIdentityItem'; + +/** + * Defines a map containing a set of end user identities, keyed on either namespace integration code or the + * namespace ID of the identity. The values of the map are an array, meaning that more than one identity of each + * namespace may be carried. Use identityMap if bringing in data from systems having identities stored in a map structure. + * + * {@link https://github.com/adobe/xdm/blob/master/components/fieldgroups/shared/identitymap.schema.json} + */ +export type AdobeIdentityMap = { [namespace: string]: AdobeIdentityItem[] }; diff --git a/adobe-edge/src/api/details/AdobeImplementationDetails.ts b/adobe-edge/src/api/details/AdobeImplementationDetails.ts index 5242d3b3..07d1b260 100644 --- a/adobe-edge/src/api/details/AdobeImplementationDetails.ts +++ b/adobe-edge/src/api/details/AdobeImplementationDetails.ts @@ -1,7 +1,7 @@ /** * Details about the SDK, library, or service used in an application or web page implementation of a service. * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/implementationdetails.schema.md + * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/implementationdetails.schema.md} */ export interface AdobeImplementationDetails { // The environment of the implementation diff --git a/adobe-edge/src/api/details/AdobeMediaDetails.ts b/adobe-edge/src/api/details/AdobeMediaDetails.ts index 357690e4..b2449048 100644 --- a/adobe-edge/src/api/details/AdobeMediaDetails.ts +++ b/adobe-edge/src/api/details/AdobeMediaDetails.ts @@ -10,7 +10,7 @@ import type { AdobePlayerStateData } from './AdobePlayerStateData'; /** * Media details information. * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/mediadetails.schema.md + * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/mediadetails.schema.md} */ export interface AdobeMediaDetails { // If the content is live, the playhead must be the current second of the day, 0 <= playhead < 86400. diff --git a/adobe-edge/src/api/details/AdobePlayerStateData.ts b/adobe-edge/src/api/details/AdobePlayerStateData.ts index bacb3d3c..c27dd970 100644 --- a/adobe-edge/src/api/details/AdobePlayerStateData.ts +++ b/adobe-edge/src/api/details/AdobePlayerStateData.ts @@ -1,7 +1,7 @@ /** * Player state data information. * - * https://github.com/adobe/xdm/blob/master/components/datatypes/playerstatedata.schema.json + * {@link https://github.com/adobe/xdm/blob/master/components/datatypes/playerstatedata.schema.json} */ export interface AdobePlayerStateData { // The name of the player state. diff --git a/adobe-edge/src/api/details/AdobeQoeDataDetails.ts b/adobe-edge/src/api/details/AdobeQoeDataDetails.ts index 63cc2894..88d5bc2a 100644 --- a/adobe-edge/src/api/details/AdobeQoeDataDetails.ts +++ b/adobe-edge/src/api/details/AdobeQoeDataDetails.ts @@ -1,7 +1,7 @@ /** * Qoe data details information related to the experience event. * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/qoedatadetails.schema.md + * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/qoedatadetails.schema.md} */ export interface AdobeQoeDataDetails { // The average bitrate (in kbps). The value is predefined buckets at 100kbps intervals. The Average Bitrate is diff --git a/adobe-edge/src/api/details/AdobeSessionDetails.ts b/adobe-edge/src/api/details/AdobeSessionDetails.ts index 184b082d..9ff9a2f4 100644 --- a/adobe-edge/src/api/details/AdobeSessionDetails.ts +++ b/adobe-edge/src/api/details/AdobeSessionDetails.ts @@ -1,7 +1,7 @@ /** * Session details information related to the experience event. * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/sessiondetails.schema.md + * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/sessiondetails.schema.md} */ export interface AdobeSessionDetails { // This identifies an instance of a content stream unique to an individual playback. diff --git a/adobe-edge/src/api/details/barrel.ts b/adobe-edge/src/api/details/barrel.ts index 814de63e..6c25e09a 100644 --- a/adobe-edge/src/api/details/barrel.ts +++ b/adobe-edge/src/api/details/barrel.ts @@ -10,3 +10,5 @@ export type { AdobePlayerStateData } from './AdobePlayerStateData'; export type { AdobeQoeDataDetails } from './AdobeQoeDataDetails'; export { ContentType, StreamType } from './AdobeSessionDetails'; export type { AdobeSessionDetails } from './AdobeSessionDetails'; +export type { AdobeIdentityMap } from './AdobeIdentityMap'; +export type { AdobeIdentityItem } from './AdobeIdentityItem'; From 1c72c0787332b1698723cb475f4e277f4aa5ef93 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Mon, 15 Dec 2025 16:37:26 +0100 Subject: [PATCH 30/70] Create Adobe edge events for iOS --- adobe-edge/ios/Connector/AdobeEdgeEvent.swift | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 adobe-edge/ios/Connector/AdobeEdgeEvent.swift diff --git a/adobe-edge/ios/Connector/AdobeEdgeEvent.swift b/adobe-edge/ios/Connector/AdobeEdgeEvent.swift new file mode 100644 index 00000000..8db463f7 --- /dev/null +++ b/adobe-edge/ios/Connector/AdobeEdgeEvent.swift @@ -0,0 +1,31 @@ +// +// AdobeEdgeEvent.swift +// + +enum AdobeEdgeEventType: Int { + case PLAYING = 0 + case PAUSE + case AD_BREAK_START + case AD_BREAK_COMPLETE + case AD_START + case AD_COMPLETE + case AD_SKIP + case SEEK_START + case SEEK_COMPLETE + case BUFFER_START + case BUFFER_COMPLETE + case BITRATE_CHANGE + case STATE_START + case STATE_END + case PLAYHEAD_UPDATE + case ERROR + case COMPLETE + case QOE_UPDATE + case SESSION_END +} + +struct AdobeEdgeEvent { + var type: AdobeEdgeEventType + var info: [String: Any]? = nil + var metadata: [String: String]? = nil +} From 0c9a72f394e1b62b5951503df837e31d250aa96f Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Mon, 15 Dec 2025 16:38:02 +0100 Subject: [PATCH 31/70] Add event queueing for iOS --- .../ios/Connector/AdobeEdgeHandler.swift | 98 ++++++++++++++----- 1 file changed, 76 insertions(+), 22 deletions(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index 4f06c29d..f4ab8e9a 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -12,6 +12,12 @@ import AEPEdgeMedia let CONTENT_PING_INTERVAL = 10.0 let AD_PING_INTERVAL = 1.0 +let PROP_NA: String = "NA" +let PROP_CURRENTTIME: String = "currentTime" +let PROP_ERRORID: String = "errorId" + +let TAG: String = "[AdobeEdgeConnector]" + class AdobeEdgeHandler { private weak var player: THEOplayer? private var trackerConfig: [String:String] @@ -23,6 +29,7 @@ class AdobeEdgeHandler { private var currentChapter: TextTrackCue? = nil private var loggingMode: LogLevel = .debug private var tracker: MediaTracker = Media.createTracker() + private var eventQueue: [AdobeEdgeEvent] = [] // MARK: Player Listeners private var playingListener: THEOplayerSDK.EventListener? @@ -53,7 +60,7 @@ class AdobeEdgeHandler { private func logDebug(_ text: String) { if self.loggingMode >= .debug { - print("[AdobeEdgeConnector]", text) + print(TAG, text) } } @@ -74,7 +81,7 @@ class AdobeEdgeHandler { } func setError(_ errorId: String) -> Void { - self.tracker.trackError(errorId: errorId) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .ERROR, info: [PROP_ERRORID: errorId])) } func stopAndStartNewSession(_ metadata: [String:String]) -> Void { @@ -204,7 +211,7 @@ class AdobeEdgeHandler { self.logDebug("onPlaying") self.maybeStartSession(mediaLengthSec: player.duration) - self.tracker.trackPlay() + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .PLAYING)) } private func handlePause(event: PauseEvent) { @@ -214,42 +221,42 @@ class AdobeEdgeHandler { private func onPause() { guard self.player != nil else { return } self.logDebug("onPause") - - self.tracker.trackPause() + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .PAUSE)) } private func handleTimeUpdate(event: TimeUpdateEvent) { guard self.player != nil else { return } //self.logDebug("onTimeUpdate") - self.tracker.updateCurrentPlayhead(time: self.sanitisePlayhead(event.currentTime)) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .PLAYHEAD_UPDATE, info: [PROP_CURRENTTIME: self.sanitisePlayhead(event.currentTime)])) } func handleWaiting(event: WaitingEvent) -> Void { guard self.player != nil else { return } self.logDebug("onWaiting") - self.tracker.trackEvent(event: MediaEvent.BufferStart, info: nil, metadata: nil) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .BUFFER_START)) } func handleSeeking(event: SeekingEvent) -> Void { guard self.player != nil else { return } self.logDebug("onSeeking") - self.tracker.trackEvent(event: MediaEvent.SeekStart, info: nil, metadata: nil) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .SEEK_START)) } func handleSeeked(event: SeekedEvent) -> Void { guard self.player != nil else { return } self.logDebug("onSeeked") - self.tracker.trackEvent(event: MediaEvent.SeekComplete, info: nil, metadata: nil) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .SEEK_COMPLETE)) } func handleEnded(event: EndedEvent) -> Void { guard self.player != nil else { return } self.logDebug("onEnded") - self.tracker.trackComplete() + + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .COMPLETE)) self.reset() } @@ -267,7 +274,7 @@ class AdobeEdgeHandler { bitrate = activeTrack.activeQuality?.bandwidth ?? 0 } if let qoe = Media.createQoEObjectWith(bitrate: bitrate, startupTime: 0, fps: 0, droppedFrames: 0) { - self.tracker.updateQoEObject(qoe: qoe) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .QOE_UPDATE, info: qoe)) } } @@ -292,7 +299,7 @@ class AdobeEdgeHandler { if let errorCodeValue = event.errorObject?.code.rawValue as? Int32 { errorCodeString = String(errorCodeValue) } - self.tracker.trackError(errorId: errorCodeString) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .ERROR, info: [PROP_ERRORID: errorCodeString])) } func handleAdBreakBegin(event: AdBreakBeginEvent) -> Void { @@ -301,8 +308,8 @@ class AdobeEdgeHandler { self.isPlayingAd = true let currentAdBreakTimeOffset = event.ad?.timeOffset ?? 0 let breakIndex = currentAdBreakTimeOffset < 0 ? -1 : (currentAdBreakTimeOffset == 0 ? 0 : self.adBreakPodIndex + 1) - let adBreakObject = Media.createAdBreakObjectWith(name: "NA", position: breakIndex, startTime: currentAdBreakTimeOffset) - self.tracker.trackEvent(event: MediaEvent.AdBreakStart, info: adBreakObject, metadata: nil) + let adBreakObject = Media.createAdBreakObjectWith(name: PROP_NA, position: breakIndex, startTime: currentAdBreakTimeOffset) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .AD_BREAK_START, info: adBreakObject)) if (breakIndex > self.adBreakPodIndex) { self.adBreakPodIndex += 1 } @@ -313,35 +320,35 @@ class AdobeEdgeHandler { self.logDebug("onAdBreakEnd") self.isPlayingAd = false self.adPodPosition = 1 - self.tracker.trackEvent(event: MediaEvent.AdBreakComplete, info: nil, metadata: nil) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .AD_BREAK_COMPLETE)) } func handleAdBegin(event: AdBeginEvent) -> Void { guard self.player != nil else { return } self.logDebug("onAdBegin") let duration = event.ad?.duration ?? 0 - let adObject = Media.createAdObjectWith(name: "NA", id: "NA", position: self.adPodPosition, length: duration) - self.tracker.trackEvent(event: MediaEvent.AdBreakStart, info: adObject, metadata: nil) + let adObject = Media.createAdObjectWith(name: PROP_NA, id: PROP_NA, position: self.adPodPosition, length: duration) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .AD_START, info: adObject)) self.adPodPosition += 1 } func handleAdEnd(event: AdEndEvent) -> Void { guard self.player != nil else { return } self.logDebug("onAdEnd") - self.tracker.trackEvent(event: MediaEvent.AdComplete, info: nil, metadata: nil) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .AD_COMPLETE)) } func handleAdSkip(event: AdSkipEvent) -> Void { guard self.player != nil else { return } self.logDebug("onAdSkip") - self.tracker.trackEvent(event: MediaEvent.AdSkip, info: nil, metadata: nil) + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .AD_SKIP)) } private func maybeEndSession() -> Void { guard self.player != nil else { return } self.logDebug("maybeEndSession") if self.sessionInProgress { - self.tracker.trackSessionEnd() + self.queueOrSendEvent(event: AdobeEdgeEvent(type: .SESSION_END)) self.sessionInProgress = false } self.reset() @@ -389,8 +396,8 @@ class AdobeEdgeHandler { let metadata: [String: Any] = player.source?.metadata?.metadataKeys ?? [:] if let mediaObject = Media.createMediaObjectWith( - name: metadata["title"] as? String ?? "N/A", - id: metadata["id"] as? String ?? "N/A", + name: metadata["title"] as? String ?? PROP_NA, + id: metadata["id"] as? String ?? PROP_NA, length: mediaLength, streamType: streamType, mediaType: MediaType.Video @@ -398,6 +405,52 @@ class AdobeEdgeHandler { self.tracker.trackSessionStart(info: mediaObject, metadata: self.customMetadata) self.sessionInProgress = true self.logDebug("maybeStartSession - STARTED") + + // Send any queued events + if !self.eventQueue.isEmpty { + self.logDebug("Sending \(self.eventQueue.count) queued events.") + for event in self.eventQueue { + self.sendEvent(event: event) + } + self.eventQueue.removeAll() + } + } + } + + private func sendEvent(event: AdobeEdgeEvent) { + if event.type != AdobeEdgeEventType.PLAYHEAD_UPDATE { // don't clutter output with timeUpdates... + self.logDebug("sendEvent: \(event.type)") + } + + switch event.type { + case .AD_BREAK_START: self.tracker.trackEvent(event: MediaEvent.AdBreakStart, info: event.info, metadata: event.metadata) + case .AD_BREAK_COMPLETE: self.tracker.trackEvent(event: MediaEvent.AdBreakComplete, info: event.info, metadata: event.metadata) + case .AD_START: self.tracker.trackEvent(event: MediaEvent.AdStart, info: event.info, metadata: event.metadata) + case .AD_COMPLETE: self.tracker.trackEvent(event: MediaEvent.AdComplete, info: event.info, metadata: event.metadata) + case .AD_SKIP: self.tracker.trackEvent(event: MediaEvent.AdSkip, info: event.info, metadata: event.metadata) + case .SEEK_START: self.tracker.trackEvent(event: MediaEvent.SeekStart, info: event.info, metadata: event.metadata) + case .SEEK_COMPLETE: self.tracker.trackEvent(event: MediaEvent.SeekComplete, info: event.info, metadata: event.metadata) + case .BUFFER_START: self.tracker.trackEvent(event: MediaEvent.BufferStart, info: event.info, metadata: event.metadata) + case .BUFFER_COMPLETE: self.tracker.trackEvent(event: MediaEvent.BufferComplete, info: event.info, metadata: event.metadata) + case .BITRATE_CHANGE: self.tracker.trackEvent(event: MediaEvent.BitrateChange, info: event.info, metadata: event.metadata) + case .STATE_START: self.tracker.trackEvent(event: MediaEvent.StateStart, info: event.info, metadata: event.metadata) + case .STATE_END: self.tracker.trackEvent(event: MediaEvent.StateEnd, info: event.info, metadata: event.metadata) + case .PLAYHEAD_UPDATE: self.tracker.updateCurrentPlayhead(time: event.info?[PROP_CURRENTTIME] as? Int ?? 0) + case .ERROR: self.tracker.trackError(errorId: event.info?[PROP_ERRORID] as? String ?? PROP_NA) + case .COMPLETE: self.tracker.trackComplete() + case .QOE_UPDATE: self.tracker.updateQoEObject(qoe: event.info ?? [:]) + case .SESSION_END: self.tracker.trackSessionEnd() + case .PLAYING: self.tracker.trackPlay() + case .PAUSE: self.tracker.trackPause() + } + } + + private func queueOrSendEvent(event: AdobeEdgeEvent) { + if self.sessionInProgress { + self.sendEvent(event: event) + } else { + self.logDebug("Queueing event: \(event.type)") + self.eventQueue.append(event) } } @@ -408,6 +461,7 @@ class AdobeEdgeHandler { self.isPlayingAd = false self.sessionInProgress = false self.currentChapter = nil + self.eventQueue.removeAll() } func destroy() -> Void { From b4eaca2125ccb3ba54a80f186031b118314a6b35 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Tue, 16 Dec 2025 14:23:55 +0100 Subject: [PATCH 32/70] Move MobileCore setup to native part. --- adobe-edge/ios/Connector/AdobeEdgeHandler.swift | 9 ++++++++- adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift | 9 ++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index f4ab8e9a..37f2550e 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -27,7 +27,7 @@ class AdobeEdgeHandler { private var isPlayingAd = false private var customMetadata: [String:String] = [:] private var currentChapter: TextTrackCue? = nil - private var loggingMode: LogLevel = .debug + private var loggingMode: LogLevel = .error private var tracker: MediaTracker = Media.createTracker() private var eventQueue: [AdobeEdgeEvent] = [] @@ -68,6 +68,13 @@ class AdobeEdgeHandler { self.player = player self.trackerConfig = trackerConfig self.addEventListeners() + + let environmentId = trackerConfig["environmentId"] ?? "MissingEnvironmentID" + MobileCore.setLogLevel(.error) + MobileCore.initialize(appId: environmentId) { + self.logDebug("MobileCore successfully initialized with App ID: \(environmentId)") + } + self.logDebug("Initialized Connector.") } diff --git a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift index f517ecdf..0ada683a 100644 --- a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift +++ b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift @@ -2,8 +2,6 @@ import Foundation import UIKit import react_native_theoplayer import THEOplayerSDK -import AEPCore -import AEPServices @objc(THEOplayerAdobeEdgeRCTAdobeEdgeAPI) class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { @@ -24,17 +22,14 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { @objc(initialize:config:) func initialize(_ node: NSNumber, config: NSDictionary) -> Void { log("initialize triggered.") - let environmentId = config["environmentId"] as? String ?? "MissingEnvironmentID" - let debugEnabled = config["debugEnabled"] as? Bool ?? false - MobileCore.setLogLevel(.debug) - MobileCore.initialize(appId: environmentId) - self.debug = debugEnabled + self.debug = config["debugEnabled"] as? Bool ?? false DispatchQueue.main.async { if let view = self.view(for: node), let player = view.player { let trackerConfig: [String: String] = TheoplayerAdobeEdgeRCTAdobeEdgeUtils.toStringMap(config as? [String: Any] ?? [:]) let connector = AdobeEdgeConnector(player: player, trackerConfig: trackerConfig) + connector.setLoggingMode(self.debug ? .debug : .error) self.connectors[node] = connector self.log("added connector to view \(node)") } else { From bbc8ba836a38cd1fe6052172a2680607cddf5126 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Tue, 16 Dec 2025 14:30:00 +0100 Subject: [PATCH 33/70] Add test setup for consents --- adobe-edge/ios/Connector/AdobeEdgeHandler.swift | 5 ++++- adobe-edge/react-native-theoplayer-adobe-edge.podspec | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index 37f2550e..12082e55 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -8,6 +8,7 @@ import UIKit import AEPServices import AEPCore import AEPEdgeMedia +import AEPEdgeConsent let CONTENT_PING_INTERVAL = 10.0 let AD_PING_INTERVAL = 1.0 @@ -73,9 +74,11 @@ class AdobeEdgeHandler { MobileCore.setLogLevel(.error) MobileCore.initialize(appId: environmentId) { self.logDebug("MobileCore successfully initialized with App ID: \(environmentId)") + // let collectConsent = ["collect": ["val": "y"]] + // Consent.update(with: ["consents": collectConsent]) } - self.logDebug("Initialized Connector.") + self.logDebug("Connector Initialized.") } func setLoggingMode(_ debug: LogLevel) -> Void { diff --git a/adobe-edge/react-native-theoplayer-adobe-edge.podspec b/adobe-edge/react-native-theoplayer-adobe-edge.podspec index d5f087ff..9a6329ea 100644 --- a/adobe-edge/react-native-theoplayer-adobe-edge.podspec +++ b/adobe-edge/react-native-theoplayer-adobe-edge.podspec @@ -21,6 +21,7 @@ Pod::Spec.new do |s| s.dependency 'AEPEdge', '~> 5.0' s.dependency 'AEPEdgeIdentity', '~> 5.0' s.dependency 'AEPEdgeMedia', '~> 5.0' + s.dependency 'AEPEdgeConsent', '~> 5.0' # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. From 1c8dcdf15d7517e105233332bf6e3218ad0535bd Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Wed, 17 Dec 2025 09:37:27 +0100 Subject: [PATCH 34/70] Allow passing a custom identityMap --- adobe-edge/src/api/AdobeConnector.ts | 41 +++++++++++++++++-- .../src/internal/AdobeConnectorAdapter.ts | 4 +- .../internal/AdobeConnectorAdapterNative.ts | 14 +++++-- .../src/internal/AdobeConnectorAdapterWeb.ts | 10 +++-- .../src/internal/web/AdobeEdgeConnector.ts | 9 +++- .../src/internal/web/AdobeEdgeHandler.ts | 11 ++++- 6 files changed, 76 insertions(+), 13 deletions(-) diff --git a/adobe-edge/src/api/AdobeConnector.ts b/adobe-edge/src/api/AdobeConnector.ts index 8845ca05..2cc510e5 100644 --- a/adobe-edge/src/api/AdobeConnector.ts +++ b/adobe-edge/src/api/AdobeConnector.ts @@ -6,20 +6,24 @@ import { Platform } from 'react-native'; import { AdobeConnectorAdapter } from '../internal/AdobeConnectorAdapter'; import { AdobeConnectorAdapterWeb } from '../internal/AdobeConnectorAdapterWeb'; import { AdobeEdgeConfig } from './AdobeEdgeConfig'; +import { AdobeIdentityMap } from './details/AdobeIdentityMap'; export class AdobeConnector { private connectorAdapter?: AdobeConnectorAdapter; - constructor(player: THEOplayer, config: AdobeEdgeConfig) { + /** + * Creates an instance of AdobeConnector. + */ + constructor(player: THEOplayer, config: AdobeEdgeConfig, customIdentityMap?: AdobeIdentityMap) { if (['ios', 'android'].includes(Platform.OS)) { if (config.mobile) { - this.connectorAdapter = new AdobeConnectorAdapterNative(player, config.mobile); + this.connectorAdapter = new AdobeConnectorAdapterNative(player, config.mobile, customIdentityMap); } else { console.error('AdobeConnector Error: Missing config for mobile platform'); } } else { if (config.web) { - this.connectorAdapter = new AdobeConnectorAdapterWeb(player, config.web); + this.connectorAdapter = new AdobeConnectorAdapterWeb(player, config.web, customIdentityMap); } else { console.error('AdobeConnector Error: Missing config for Web platform'); } @@ -33,6 +37,37 @@ export class AdobeConnector { this.connectorAdapter?.updateMetadata(customMetadataDetails); } + /** + * Sets custom identity map. + * + * @example + * ```typescript + * { + * "EMAIL": [ + * { + * "id": "user@example.com", + * "authenticatedState": "authenticated", + * "primary": "false" + * }, + * { + * "id" : "useralias@example.com", + * "authenticatedState": "ambiguous", + * "primary": false + * } + * ], + * "Email_LC_SHA256": [ + * { + * "id": "2394509340-9b942f32f709db2c57e79cecec4462836ca1efef1c336a939c4b1674bcc74320", + * "authenticatedState": "authenticated", + * "primary": "false" + * } + * ] + * } + */ + setCustomIdentityMap(customIdentityMap: AdobeIdentityMap): void { + this.connectorAdapter?.setCustomIdentityMap(customIdentityMap); + } + /** * Dispatch error event to adobe */ diff --git a/adobe-edge/src/internal/AdobeConnectorAdapter.ts b/adobe-edge/src/internal/AdobeConnectorAdapter.ts index 98161e9b..49612678 100644 --- a/adobe-edge/src/internal/AdobeConnectorAdapter.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapter.ts @@ -1,10 +1,12 @@ -import type { AdobeCustomMetadataDetails, AdobeErrorDetails } from '@theoplayer/react-native-analytics-adobe-edge'; +import { AdobeCustomMetadataDetails, AdobeErrorDetails, AdobeIdentityMap } from '@theoplayer/react-native-analytics-adobe-edge'; export interface AdobeConnectorAdapter { setDebug(debug: boolean): void; updateMetadata(metadata: AdobeCustomMetadataDetails[]): void; + setCustomIdentityMap(customIdentityMap: AdobeIdentityMap): void; + setError(metadata: AdobeErrorDetails): void; stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]): Promise; diff --git a/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts b/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts index ccaf5b3b..c5de3337 100644 --- a/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts @@ -1,6 +1,6 @@ import type { NativeHandleType, THEOplayer } from 'react-native-theoplayer'; import { NativeModules } from 'react-native'; -import type { AdobeCustomMetadataDetails, AdobeErrorDetails } from '@theoplayer/react-native-analytics-adobe-edge'; +import { AdobeCustomMetadataDetails, AdobeErrorDetails, AdobeIdentityMap } from '@theoplayer/react-native-analytics-adobe-edge'; import { AdobeConnectorAdapter } from './AdobeConnectorAdapter'; import { AdobeEdgeMobileConfig } from '../api/AdobeEdgeMobileConfig'; @@ -10,10 +10,10 @@ const ERROR_MSG = 'AdobeConnectorAdapter Error'; export class AdobeConnectorAdapterNative implements AdobeConnectorAdapter { private readonly nativeHandle: NativeHandleType; - constructor(player: THEOplayer, config: AdobeEdgeMobileConfig) { + constructor(player: THEOplayer, config: AdobeEdgeMobileConfig, customIdentityMap?: AdobeIdentityMap) { this.nativeHandle = player.nativeHandle || -1; try { - NativeModules.AdobeEdgeModule.initialize(this.nativeHandle, config); + NativeModules.AdobeEdgeModule.initialize(this.nativeHandle, config, customIdentityMap); } catch (error: unknown) { console.error(TAG, `${ERROR_MSG}: ${error}`); } @@ -35,6 +35,14 @@ export class AdobeConnectorAdapterNative implements AdobeConnectorAdapter { } } + setCustomIdentityMap(customIdentityMap: AdobeIdentityMap): void { + try { + NativeModules.AdobeEdgeModule.setCustomIdentityMap(this.nativeHandle || -1, customIdentityMap); + } catch (error: unknown) { + console.error(TAG, `${ERROR_MSG}: ${error}`); + } + } + setError(errorDetails: AdobeErrorDetails) { try { NativeModules.AdobeEdgeModule.setError(this.nativeHandle || -1, errorDetails); diff --git a/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts b/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts index 35eb19d6..fd6171fc 100644 --- a/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts @@ -1,5 +1,5 @@ import type { THEOplayer } from 'react-native-theoplayer'; -import type { AdobeCustomMetadataDetails, AdobeErrorDetails } from '@theoplayer/react-native-analytics-adobe-edge'; +import { AdobeCustomMetadataDetails, AdobeErrorDetails, AdobeIdentityMap } from '@theoplayer/react-native-analytics-adobe-edge'; import { AdobeConnectorAdapter } from './AdobeConnectorAdapter'; import { AdobeEdgeWebConfig } from '../api/AdobeEdgeWebConfig'; import { AdobeEdgeConnector } from './web/AdobeEdgeConnector'; @@ -8,8 +8,8 @@ import { ChromelessPlayer } from 'theoplayer'; export class AdobeConnectorAdapterWeb implements AdobeConnectorAdapter { private connector: AdobeEdgeConnector; - constructor(player: THEOplayer, config: AdobeEdgeWebConfig) { - this.connector = new AdobeEdgeConnector(player.nativeHandle as ChromelessPlayer, config); + constructor(player: THEOplayer, config: AdobeEdgeWebConfig, customIdentityMap?: AdobeIdentityMap) { + this.connector = new AdobeEdgeConnector(player.nativeHandle as ChromelessPlayer, config, customIdentityMap); } setDebug(debug: boolean) { @@ -20,6 +20,10 @@ export class AdobeConnectorAdapterWeb implements AdobeConnectorAdapter { this.connector.updateMetadata(metadata); } + setCustomIdentityMap(customIdentityMap: AdobeIdentityMap): void { + this.connector?.setCustomIdentityMap(customIdentityMap); + } + setError(errorDetails: AdobeErrorDetails): void { void this.connector.setError(errorDetails); } diff --git a/adobe-edge/src/internal/web/AdobeEdgeConnector.ts b/adobe-edge/src/internal/web/AdobeEdgeConnector.ts index 35bea84b..894a8c51 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeConnector.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeConnector.ts @@ -3,6 +3,7 @@ import { AdobeCustomMetadataDetails, AdobeEdgeWebConfig, AdobeErrorDetails, + AdobeIdentityMap, AdobeQoeDataDetails, } from '@theoplayer/react-native-analytics-adobe-edge'; import { ChromelessPlayer } from 'theoplayer'; @@ -10,8 +11,8 @@ import { ChromelessPlayer } from 'theoplayer'; export class AdobeEdgeConnector { private _handler: AdobeEdgeHandler; - constructor(player: ChromelessPlayer, config: AdobeEdgeWebConfig) { - this._handler = new AdobeEdgeHandler(player, config); + constructor(player: ChromelessPlayer, config: AdobeEdgeWebConfig, customIdentityMap?: AdobeIdentityMap) { + this._handler = new AdobeEdgeHandler(player, config, customIdentityMap); } /** @@ -31,6 +32,10 @@ export class AdobeEdgeConnector { this._handler.updateMetadata(metadata); } + setCustomIdentityMap(customIdentityMap: AdobeIdentityMap): void { + this._handler.setCustomIdentityMap(customIdentityMap); + } + setDebug(debug: boolean) { this._handler.setDebug(debug); } diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts index c3057216..9fda2a70 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -1,6 +1,7 @@ import { AdobeCustomMetadataDetails, AdobeErrorDetails, + AdobeIdentityMap, AdobeMediaDetails, AdobeQoeDataDetails, AdobeSessionDetails, @@ -74,14 +75,16 @@ export class AdobeEdgeHandler { private _adPodPosition = 1; private _isPlayingAd = false; private _customMetadata: AdobeCustomMetadataDetails[] = []; + private _customIdentityMap: AdobeIdentityMap | undefined; private _currentChapter: TextTrackCue | undefined; private _eventQueue: (() => Promise)[] = []; private _debug = false; private readonly _alloyClient: AlloyClient; - constructor(player: ChromelessPlayer, config: AdobeEdgeWebConfig) { + constructor(player: ChromelessPlayer, config: AdobeEdgeWebConfig, customIdentityMap?: AdobeIdentityMap) { this._player = player; this._hasSessionFailed = false; + this._customIdentityMap = customIdentityMap; const sanitisedConfig = sanitiseConfig(config); const { datastreamId, orgId, debugEnabled } = sanitisedConfig; @@ -124,6 +127,10 @@ export class AdobeEdgeHandler { this._customMetadata = [...this._customMetadata, ...metadata]; } + setCustomIdentityMap(customIdentityMap: AdobeIdentityMap): void { + this._customIdentityMap = customIdentityMap; + } + private addEventListeners(): void { this._player.addEventListener('playing', this.onPlaying); this._player.addEventListener('pause', this.onPause); @@ -452,6 +459,7 @@ export class AdobeEdgeHandler { qoeDataDetails, customMetadata, }, + identityMap: this._customIdentityMap, }, }); @@ -506,6 +514,7 @@ export class AdobeEdgeHandler { playhead: sanitisePlayhead(mediaDetails.playhead), sessionID: this._sessionId, }, + identityMap: this._customIdentityMap, }, }); } catch (e) { From 57829c1f477f0e9b084b4b97b2d51f898c631ffa Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Wed, 17 Dec 2025 12:03:00 +0100 Subject: [PATCH 35/70] Set custom identityMap --- .../adobe/edge/AdobeEdgeConnector.kt | 8 ++++++- .../adobe/edge/AdobeEdgeHandler.kt | 17 ++++++++++---- .../edge/ReactTHEOplayerAdobeEdgeModule.kt | 12 +++++++++- .../reactnative/adobe/edge/Utils.kt | 23 +++++++++++++++++++ 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt index 10f80c90..1e439bb2 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeConnector.kt @@ -1,19 +1,25 @@ package com.theoplayer.reactnative.adobe.edge import com.adobe.marketing.mobile.LoggingMode +import com.adobe.marketing.mobile.edge.identity.IdentityMap import com.theoplayer.android.api.player.Player class AdobeEdgeConnector( player: Player, trackerConfig: Map, + customIdentityMap: IdentityMap? = null, ) { - private val handler = AdobeEdgeHandler(player, trackerConfig) + private val handler = AdobeEdgeHandler(player, trackerConfig, customIdentityMap) fun updateMetadata(metadata: HashMap) { handler.updateMetadata(metadata) } + fun setCustomIdentityMap(customIdentityMap: IdentityMap) { + handler.setCustomIdentityMap(customIdentityMap) + } + fun stopAndStartNewSession(metadata: Map?) { handler.stopAndStartNewSession(metadata) } diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt index db5f9eac..aea11b37 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -3,6 +3,8 @@ package com.theoplayer.reactnative.adobe.edge import android.util.Log import com.adobe.marketing.mobile.LoggingMode import com.adobe.marketing.mobile.MobileCore +import com.adobe.marketing.mobile.edge.identity.Identity +import com.adobe.marketing.mobile.edge.identity.IdentityMap import com.adobe.marketing.mobile.edge.media.Media import com.adobe.marketing.mobile.edge.media.Media.Event import com.adobe.marketing.mobile.edge.media.MediaConstants @@ -79,7 +81,8 @@ const val PROP_NA = "NA" class AdobeEdgeHandler( private val player: Player, - trackerConfig: Map = emptyMap() + trackerConfig: Map = emptyMap(), + customIdentityMap: IdentityMap? = null ) { private var sessionInProgress = false @@ -88,13 +91,9 @@ class AdobeEdgeHandler( private var adPodPosition = 1 private var isPlayingAd = false - private var customMetadata = mutableMapOf() - private var currentChapter: TextTrackCue? = null - private var loggingMode: LoggingMode = LoggingMode.ERROR - private val onPlaying = EventListener { handlePlaying() } private val onPause = EventListener { handlePause() } private val onEnded = EventListener { handleEnded() } @@ -131,6 +130,7 @@ class AdobeEdgeHandler( init { addEventListeners() + customIdentityMap?.let { setCustomIdentityMap(it) } logDebug("Initialized connector") } @@ -143,6 +143,13 @@ class AdobeEdgeHandler( customMetadata += metadata } + fun setCustomIdentityMap(customIdentityMap: IdentityMap) { + /** + * https://developer.adobe.com/client-sdks/edge/identity-for-edge-network/api-reference/#updateidentities + */ + Identity.updateIdentities(customIdentityMap) + } + fun setError(errorId: String) { queueOrSendEvent(EventType.ERROR, mapOf(PROP_ERROR_ID to errorId), null) } diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt index 65d87c21..2d466286 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt @@ -28,6 +28,7 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : fun initialize( tag: Int, config: ReadableMap, + customIdentityMap: ReadableMap? ) { /** * If an asset config file is provided, use it to initialize the MobileCore SDK, otherwise use @@ -43,7 +44,8 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : view?.playerContext?.playerView?.let { playerView -> adobeConnectors[tag] = AdobeEdgeConnector( player = playerView.player, - trackerConfig = config.toHashMap().mapValues { it.value?.toString() ?: "" } + trackerConfig = config.toHashMap().mapValues { it.value?.toString() ?: "" }, + customIdentityMap = customIdentityMap?.toAdobeIdentityMap() ) if (config.hasKey(PROP_DEBUG_ENABLED)) { setDebug(tag, config.getBoolean(PROP_DEBUG_ENABLED)) @@ -73,6 +75,14 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : adobeConnectors[tag]?.updateMetadata(metadataList.toAdobeCustomMetadataDetails()) } + /** + * Sets customMetadataDetails which will be passed for the session start request. + */ + @ReactMethod + fun setCustomIdentityMap(tag: Int, customIdentityMap: ReadableMap) { + adobeConnectors[tag]?.setCustomIdentityMap(customIdentityMap.toAdobeIdentityMap()) + } + /** * Dispatch error event to adobe */ diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt index 36b75231..37d05c93 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt @@ -1,7 +1,10 @@ package com.theoplayer.reactnative.adobe.edge +import com.adobe.marketing.mobile.edge.identity.AuthenticatedState +import com.adobe.marketing.mobile.edge.identity.IdentityItem import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap +import com.adobe.marketing.mobile.edge.identity.IdentityMap private const val PROP_NAME = "name" private const val PROP_VALUE = "value" @@ -33,3 +36,23 @@ fun ReadableArray.toAdobeCustomMetadataDetails() : HashMap { .filter { e -> e != null && e.hasKey(PROP_NAME) && e.hasKey(PROP_VALUE) } } } + +fun ReadableMap.toAdobeIdentityMap(): IdentityMap { + return IdentityMap().apply { + toHashMap().forEach { (namespace, items) -> + val itemList = items as? List<*> ?: return@forEach + itemList.forEach { item -> + val itemMap = item as? Map<*, *> ?: return@forEach + addItem(IdentityItem( + itemMap["id"] as? String ?: "", + when (itemMap["authenticatedState"] as? String) { + "authenticated" -> AuthenticatedState.AUTHENTICATED + "loggedOut" -> AuthenticatedState.LOGGED_OUT + else -> AuthenticatedState.AMBIGUOUS + }, + itemMap["primary"] as? Boolean ?: false + ), namespace) + } + } + } +} From 434b2df42751d2b0ccf6d1933254160273554e44 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Wed, 17 Dec 2025 13:17:37 +0100 Subject: [PATCH 36/70] Drop ping command --- .../src/internal/web/AdobeEdgeHandler.ts | 27 ------------------- adobe-edge/src/internal/web/EventType.ts | 1 - 2 files changed, 28 deletions(-) diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts index 9fda2a70..9b5235c5 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -36,8 +36,6 @@ import { import { EventType } from './EventType'; const TAG = 'AdobeConnector'; -const CONTENT_PING_INTERVAL = 10000; -const AD_PING_INTERVAL = 1000; // eslint-disable-next-line @typescript-eslint/ban-types type AlloyClient = Function; @@ -66,9 +64,6 @@ export class AdobeEdgeHandler { private _sessionId: string | undefined; private _hasSessionFailed: boolean; - /** Timer handling the ping event request */ - private _pingInterval: ReturnType | undefined; - /** Whether we are in a current session or not */ private _sessionInProgress = false; private _adBreakPodIndex = 0; @@ -266,7 +261,6 @@ export class AdobeEdgeHandler { private onAdBreakBegin = (event: AdBreakEvent<'adbreakbegin'>) => { this._isPlayingAd = true; - this.startPinger(AD_PING_INTERVAL); const podDetails = calculateAdvertisingPodDetails(event.adBreak, this._adBreakPodIndex); void this.queueOrSendEvent(EventType.adBreakStart, { playhead: this._player.currentTime, @@ -297,7 +291,6 @@ export class AdobeEdgeHandler { private onAdBreakEnd = () => { this._isPlayingAd = false; this._adPodPosition = 1; - this.startPinger(CONTENT_PING_INTERVAL); void this.queueOrSendEvent(EventType.adBreakComplete, { playhead: this._player.currentTime }); }; @@ -360,24 +353,6 @@ export class AdobeEdgeHandler { this._sessionInProgress = true; this.logDebug('maybeStartSession - STARTED', `sessionId: ${this.sessionId}`); - - if (!this._isPlayingAd) { - this.startPinger(CONTENT_PING_INTERVAL); - } else { - this.startPinger(AD_PING_INTERVAL); - } - } - - private startPinger(interval: number): void { - if (this._pingInterval !== undefined) { - clearInterval(this._pingInterval); - } - this._pingInterval = setInterval(() => { - // Only send pings if the session has started, never queue them. - if (this.hasSessionStarted()) { - void this.sendMediaEvent(EventType.ping, { playhead: this._player.currentTime }); - } - }, interval); } private getContentLength(mediaLengthSec?: number): number { @@ -394,8 +369,6 @@ export class AdobeEdgeHandler { this._adPodPosition = 1; this._isPlayingAd = false; this._sessionInProgress = false; - clearInterval(this._pingInterval); - this._pingInterval = undefined; this._currentChapter = undefined; this._sessionId = undefined; this._hasSessionFailed = false; diff --git a/adobe-edge/src/internal/web/EventType.ts b/adobe-edge/src/internal/web/EventType.ts index 02cdd971..b65ca7de 100644 --- a/adobe-edge/src/internal/web/EventType.ts +++ b/adobe-edge/src/internal/web/EventType.ts @@ -1,7 +1,6 @@ export enum EventType { sessionStart = 'media.sessionStart', play = 'media.play', - ping = 'media.ping', bitrateChange = 'media.bitrateChange', bufferStart = 'media.bufferStart', pauseStart = 'media.pauseStart', From dd6d9613e0843a6e656ec0c639362c1aa4b50093 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Wed, 14 Jan 2026 15:23:19 +0100 Subject: [PATCH 37/70] Remove unused code --- adobe-edge/src/internal/web/AdobeEdgeHandler.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts index 9b5235c5..14a9eef9 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -68,7 +68,6 @@ export class AdobeEdgeHandler { private _sessionInProgress = false; private _adBreakPodIndex = 0; private _adPodPosition = 1; - private _isPlayingAd = false; private _customMetadata: AdobeCustomMetadataDetails[] = []; private _customIdentityMap: AdobeIdentityMap | undefined; private _currentChapter: TextTrackCue | undefined; @@ -260,7 +259,6 @@ export class AdobeEdgeHandler { }; private onAdBreakBegin = (event: AdBreakEvent<'adbreakbegin'>) => { - this._isPlayingAd = true; const podDetails = calculateAdvertisingPodDetails(event.adBreak, this._adBreakPodIndex); void this.queueOrSendEvent(EventType.adBreakStart, { playhead: this._player.currentTime, @@ -289,7 +287,6 @@ export class AdobeEdgeHandler { }; private onAdBreakEnd = () => { - this._isPlayingAd = false; this._adPodPosition = 1; void this.queueOrSendEvent(EventType.adBreakComplete, { playhead: this._player.currentTime }); }; @@ -367,7 +364,6 @@ export class AdobeEdgeHandler { this.logDebug('reset'); this._adBreakPodIndex = 0; this._adPodPosition = 1; - this._isPlayingAd = false; this._sessionInProgress = false; this._currentChapter = undefined; this._sessionId = undefined; From 2bfadf9d8e4559ef4841e4a0f2b18ea00645b8a1 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Thu, 15 Jan 2026 13:36:32 +0100 Subject: [PATCH 38/70] Use tracker & align with mobile --- .../src/internal/web/AdobeEdgeConnector.ts | 12 +- .../src/internal/web/AdobeEdgeHandler.ts | 575 +++++++++--------- adobe-edge/src/internal/web/EventType.ts | 37 +- adobe-edge/src/internal/web/Media.ts | 120 ++++ adobe-edge/src/internal/web/Utils.ts | 89 ++- 5 files changed, 463 insertions(+), 370 deletions(-) create mode 100644 adobe-edge/src/internal/web/Media.ts diff --git a/adobe-edge/src/internal/web/AdobeEdgeConnector.ts b/adobe-edge/src/internal/web/AdobeEdgeConnector.ts index 894a8c51..e4f8d849 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeConnector.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeConnector.ts @@ -1,11 +1,5 @@ import { AdobeEdgeHandler } from './AdobeEdgeHandler'; -import { - AdobeCustomMetadataDetails, - AdobeEdgeWebConfig, - AdobeErrorDetails, - AdobeIdentityMap, - AdobeQoeDataDetails, -} from '@theoplayer/react-native-analytics-adobe-edge'; +import { AdobeCustomMetadataDetails, AdobeEdgeWebConfig, AdobeErrorDetails, AdobeIdentityMap } from '@theoplayer/react-native-analytics-adobe-edge'; import { ChromelessPlayer } from 'theoplayer'; export class AdobeEdgeConnector { @@ -40,8 +34,8 @@ export class AdobeEdgeConnector { this._handler.setDebug(debug); } - setError(errorDetails: AdobeErrorDetails, qoeDataDetails?: AdobeQoeDataDetails) { - return this._handler.setError(errorDetails, qoeDataDetails); + setError(errorDetails: AdobeErrorDetails) { + return this._handler.setError(errorDetails.name); } destroy() { diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts index 14a9eef9..01df7b7c 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -1,22 +1,13 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { AdobeCustomMetadataDetails, AdobeIdentityMap, ContentType } from '@theoplayer/react-native-analytics-adobe-edge'; import { - AdobeCustomMetadataDetails, - AdobeErrorDetails, - AdobeIdentityMap, - AdobeMediaDetails, - AdobeQoeDataDetails, - AdobeSessionDetails, - ContentType, - ErrorSource, -} from '@theoplayer/react-native-analytics-adobe-edge'; -import type { AdobePlayerStateData } from '../../api/details/AdobePlayerStateData'; -import { - calculateAdvertisingDetails, - calculateAdvertisingPodDetails, - calculateChapterDetails, + idToInt, isValidDuration, + sanitiseChapterId, sanitiseConfig, sanitiseContentLength, sanitisePlayhead, + toAdobeCustomMetadataDetails, } from './Utils'; import { createInstance } from '@adobe/alloy'; import { AdobeEdgeWebConfig } from '../../api/AdobeEdgeWebConfig'; @@ -32,13 +23,16 @@ import { MediaTrack, QualityEvent, AdBreakEvent, + VideoQuality, } from 'theoplayer'; import { EventType } from './EventType'; +import AlloyClient, { Media, MediaTracker } from './Media'; const TAG = 'AdobeConnector'; -// eslint-disable-next-line @typescript-eslint/ban-types -type AlloyClient = Function; +const PROP_PLAYHEAD = 'playhead'; +const PROP_ERROR_ID = 'errorId'; +const PROP_NA = 'N/A'; /** * Alloy globally stores clients by name. We are allowed create clients with the same config only once. @@ -48,6 +42,16 @@ interface ClientDescription { orgId: string; client: AlloyClient; } + +type EventInfo = { [key: string]: any }; +type EventMetadata = { [key: string]: string }; + +interface QueuedEvent { + type: EventType; + info: EventInfo; + metadata: EventMetadata; +} + const createdClients: ClientDescription[] = []; /** @@ -59,25 +63,24 @@ const createdClients: ClientDescription[] = []; * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/js-overview} * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/configure/streamingmedia} */ -export class AdobeEdgeHandler { +class AdobeEdgeHandler { private _player: ChromelessPlayer; - private _sessionId: string | undefined; - private _hasSessionFailed: boolean; /** Whether we are in a current session or not */ private _sessionInProgress = false; private _adBreakPodIndex = 0; private _adPodPosition = 1; - private _customMetadata: AdobeCustomMetadataDetails[] = []; + private _customMetadata: EventMetadata = {}; private _customIdentityMap: AdobeIdentityMap | undefined; private _currentChapter: TextTrackCue | undefined; - private _eventQueue: (() => Promise)[] = []; + private _eventQueue: QueuedEvent[] = []; private _debug = false; - private readonly _alloyClient: AlloyClient; + private readonly _alloyClient: AlloyClient | undefined; + private _media: Media | undefined; + private _tracker: MediaTracker | undefined; constructor(player: ChromelessPlayer, config: AdobeEdgeWebConfig, customIdentityMap?: AdobeIdentityMap) { this._player = player; - this._hasSessionFailed = false; this._customIdentityMap = customIdentityMap; const sanitisedConfig = sanitiseConfig(config); const { datastreamId, orgId, debugEnabled } = sanitisedConfig; @@ -88,10 +91,22 @@ export class AdobeEdgeHandler { if (!this._alloyClient) { this._alloyClient = createInstance({ name: 'alloy', + /** + * Optional event callbacks for debugging purposes. + */ monitors: [ { - // Optionally configure callbacks. - // onInstanceConfigured: function (data: any) {}, + // onBeforeLog: (arg0: any) => void; + // onInstanceCreated: (arg0: any) => void; + // onInstanceConfigured: (arg0: any) => void; + // onBeforeCommand: (arg0: any) => void; + // onCommandResolved: (arg0: any) => void; + // onCommandRejected: (arg0: any) => void; + // onBeforeNetworkRequest: (arg0: any) => void; + // onNetworkResponse: (arg0: any) => void; + // onNetworkError: (arg0: any) => void; + // onContentHiding: (arg0: any) => void; + // onContentRendering: (arg0: any) => void; }, ], }); @@ -100,211 +115,265 @@ export class AdobeEdgeHandler { // Store created client to prevent creating duplicates. createdClients.push({ datastreamId, orgId, client: this._alloyClient }); } - this.setDebug(debugEnabled); - } - async stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]): Promise { - await this.maybeEndSession(); - if (metadata !== undefined) { - this.updateMetadata(metadata); - } - await this.maybeStartSession(); + /** + * Acquire Media Analytics APIs & tracker. + * https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/getmediaanalyticstracker + */ + this._alloyClient('getMediaAnalyticsTracker', {}).then((result: any) => { + this._media = result; + this._tracker = this._media?.getInstance(); + }); - if (this._player.paused) { - this.onPause(); - } else { - this.onPlaying(); - } + this.setDebug(debugEnabled || false); } - updateMetadata(metadata: AdobeCustomMetadataDetails[]): void { - this._customMetadata = [...this._customMetadata, ...metadata]; + updateMetadata(metadata: AdobeCustomMetadataDetails[]) { + this._customMetadata = { ...this._customMetadata, ...toAdobeCustomMetadataDetails(metadata) }; } - setCustomIdentityMap(customIdentityMap: AdobeIdentityMap): void { + setCustomIdentityMap(customIdentityMap: AdobeIdentityMap) { this._customIdentityMap = customIdentityMap; } - private addEventListeners(): void { - this._player.addEventListener('playing', this.onPlaying); - this._player.addEventListener('pause', this.onPause); - this._player.addEventListener('ended', this.onEnded); - this._player.addEventListener('waiting', this.onWaiting); - this._player.addEventListener('sourcechange', this.onSourceChange); - this._player.textTracks.addEventListener('addtrack', this.onAddTextTrack); - this._player.textTracks.addEventListener('removetrack', this.onRemoveTextTrack); - this._player.videoTracks.addEventListener('addtrack', this.onAddVideoTrack); - this._player.videoTracks.addEventListener('removetrack', this.onRemoveVideoTrack); + setError(errorId: string) { + this.queueOrSendEvent(EventType.error, { PROP_ERROR_ID: errorId }); + } + + stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]) { + this.maybeEndSession(); + if (metadata) { + this.updateMetadata(metadata); + } + this.maybeStartSession(); + if (this._player.paused) { + this.handlePause(); + } else { + this.handlePlaying(); + } + } + + private addEventListeners() { + this._player.addEventListener('playing', this.handlePlaying); + this._player.addEventListener('pause', this.handlePause); + this._player.addEventListener('ended', this.handleEnded); + this._player.addEventListener('waiting', this.handleWaiting); + this._player.addEventListener('seeking', this.handleSeeking); + this._player.addEventListener('seeked', this.handleSeeked); + this._player.addEventListener('timeupdate', this.handleTimeUpdate); + this._player.addEventListener('sourcechange', this.handleSourceChange); + this._player.textTracks.addEventListener('addtrack', this.handleAddTextTrack); + this._player.textTracks.addEventListener('removetrack', this.handleRemoveTextTrack); + this._player.videoTracks.addEventListener('addtrack', this.handleAddVideoTrack); + this._player.videoTracks.addEventListener('removetrack', this.handleRemoveVideoTrack); this._player.addEventListener('loadedmetadata', this.onLoadedMetadata); - this._player.addEventListener('error', this.onError); - this._player.ads?.addEventListener('adbreakbegin', this.onAdBreakBegin); - 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('adbreakend', this.onAdBreakEnd); + this._player.addEventListener('error', this.handleError); + this._player.ads?.addEventListener('adbreakbegin', this.handleAdBreakBegin); + this._player.ads?.addEventListener('adbreakend', this.handleAdBreakEnd); + this._player.ads?.addEventListener('adbegin', this.handleAdBegin); + this._player.ads?.addEventListener('adend', this.handleAdEnd); + this._player.ads?.addEventListener('adskip', this.handleAdSkip); window.addEventListener('beforeunload', this.onBeforeUnload); } - private removeEventListeners(): void { - this._player.removeEventListener('playing', this.onPlaying); - this._player.removeEventListener('pause', this.onPause); - this._player.removeEventListener('ended', this.onEnded); - this._player.removeEventListener('waiting', this.onWaiting); - this._player.removeEventListener('sourcechange', this.onSourceChange); - this._player.textTracks.removeEventListener('addtrack', this.onAddTextTrack); - this._player.textTracks.removeEventListener('removetrack', this.onRemoveTextTrack); - this._player.videoTracks.removeEventListener('addtrack', this.onAddVideoTrack); - this._player.videoTracks.removeEventListener('removetrack', this.onRemoveVideoTrack); + private removeEventListeners() { + this._player.removeEventListener('playing', this.handlePlaying); + this._player.removeEventListener('pause', this.handlePause); + this._player.removeEventListener('ended', this.handleEnded); + this._player.removeEventListener('waiting', this.handleWaiting); + this._player.removeEventListener('seeking', this.handleSeeking); + this._player.removeEventListener('seeked', this.handleSeeked); + this._player.removeEventListener('timeupdate', this.handleTimeUpdate); + this._player.removeEventListener('sourcechange', this.handleSourceChange); + this._player.textTracks.removeEventListener('addtrack', this.handleAddTextTrack); + this._player.textTracks.removeEventListener('removetrack', this.handleRemoveTextTrack); + this._player.videoTracks.removeEventListener('addtrack', this.handleAddVideoTrack); + this._player.videoTracks.removeEventListener('removetrack', this.handleRemoveVideoTrack); this._player.removeEventListener('loadedmetadata', this.onLoadedMetadata); - this._player.removeEventListener('error', this.onError); - this._player.ads?.removeEventListener('adbreakbegin', this.onAdBreakBegin); - 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('adbreakend', this.onAdBreakEnd); + this._player.removeEventListener('error', this.handleError); + this._player.ads?.removeEventListener('adbreakbegin', this.handleAdBreakBegin); + this._player.ads?.removeEventListener('adbreakend', this.handleAdBreakEnd); + this._player.ads?.removeEventListener('adbegin', this.handleAdBegin); + this._player.ads?.removeEventListener('adend', this.handleAdEnd); + this._player.ads?.removeEventListener('adskip', this.handleAdSkip); window.removeEventListener('beforeunload', this.onBeforeUnload); } - private onLoadedMetadata = () => { - this.logDebug('onLoadedMetadata'); + private sendEvent(eventType: EventType, info: EventInfo, metadata: EventMetadata) { + switch (eventType) { + case EventType.updatePlayhead: + this._tracker?.updatePlayhead(info[PROP_PLAYHEAD]); + break; + case EventType.error: + this._tracker?.trackError(info[PROP_ERROR_ID] || PROP_NA); + break; + case EventType.sessionComplete: + this._tracker?.trackComplete(); + break; + case EventType.sessionEnd: + this._tracker?.trackSessionEnd(); + break; + case EventType.play: + this._tracker?.trackPlay(); + break; + case EventType.pauseStart: + this._tracker?.trackPause(); + break; + default: + this._tracker?.trackEvent(eventType, info, metadata); + break; + } + } - // NOTE: In case of a pre-roll ad: - // - on Android & iOS, the onLoadedMetadata is sent *after* a pre-roll has finished; - // - on Web, onLoadedMetadata is sent twice, once before the pre-roll, where player.duration is still NaN, - // and again after the pre-roll with a correct duration. - void this.maybeStartSession(this._player.duration); - }; + private queueOrSendEvent(type: EventType, info: EventInfo = {}, metadata: EventMetadata = {}) { + const extendedInfo = { ...info, playhead: sanitisePlayhead(this._player.currentTime) }; + if (this._sessionInProgress) { + this.sendEvent(type, extendedInfo, metadata); + } else { + this._eventQueue.push({ type, info: extendedInfo, metadata }); + } + } - private onPlaying = () => { + private handlePlaying = () => { this.logDebug('onPlaying'); - void this.queueOrSendEvent(EventType.play, { playhead: this._player.currentTime }); + this.queueOrSendEvent(EventType.play); }; - private onPause = () => { + private handlePause = () => { this.logDebug('onPause'); - void this.queueOrSendEvent(EventType.pauseStart, { playhead: this._player.currentTime }); + this.queueOrSendEvent(EventType.pauseStart); }; - private onWaiting = () => { + private handleTimeUpdate = () => { + this.queueOrSendEvent(EventType.updatePlayhead); + }; + + private handleWaiting = () => { this.logDebug('onWaiting'); - void this.queueOrSendEvent(EventType.bufferStart, { playhead: this._player.currentTime }); + this.queueOrSendEvent(EventType.bufferStart); }; - private onEnded = async () => { - this.logDebug('onEnded'); - void this.queueOrSendEvent(EventType.sessionComplete, { playhead: this._player.currentTime }); - this.reset(); + private handleSeeking = () => { + this.logDebug('onSeeking'); + this.queueOrSendEvent(EventType.seekStart); }; - private onSourceChange = () => { - this.logDebug('onSourceChange'); - void this.maybeEndSession(); + private handleSeeked = () => { + this.logDebug('onSeeked'); + this.queueOrSendEvent(EventType.seekEnd); }; - private onAddVideoTrack = (event: AddTrackEvent) => { - (event.track as MediaTrack).addEventListener('activequalitychanged', this.onActiveQualityChanged); + private handleEnded = () => { + this.logDebug('onEnded'); + this.queueOrSendEvent(EventType.sessionComplete); + this.reset(); }; - private onRemoveVideoTrack = (event: RemoveTrackEvent) => { - (event.track as MediaTrack).removeEventListener('activequalitychanged', this.onActiveQualityChanged); + private handleSourceChange = () => { + this.logDebug('onSourceChange'); + this.maybeEndSession(); }; - private onActiveQualityChanged = (event: QualityEvent<'activequalitychanged'>) => { + private handleQualityChanged = (event: QualityEvent<'activequalitychanged'>) => { + const quality = event.quality as VideoQuality; void this.queueOrSendEvent(EventType.bitrateChange, { - playhead: this._player.currentTime, - qoeDataDetails: { - bitrate: event.quality?.bandwidth ?? 0, - }, + qoeDataDetails: this._media?.createQoEObject(quality?.bandwidth ?? 0, 0, quality?.frameRate ?? 0, 0), }); }; - private onAddTextTrack = (event: AddTrackEvent) => { + private handleAddTextTrack = (event: AddTrackEvent) => { const track = event.track as TextTrack; if (track.kind === 'chapters') { - track.addEventListener('entercue', this.onEnterCue); - track.addEventListener('exitcue', this.onExitCue); + track.addEventListener('entercue', this.handleEnterCue); + track.addEventListener('exitcue', this.handleExitCue); } }; - private onRemoveTextTrack = (event: RemoveTrackEvent) => { + private handleRemoveTextTrack = (event: RemoveTrackEvent) => { const track = event.track as TextTrack; if (track.kind === 'chapters') { - track.removeEventListener('entercue', this.onEnterCue); - track.removeEventListener('exitcue', this.onExitCue); + track.removeEventListener('entercue', this.handleEnterCue); + track.removeEventListener('exitcue', this.handleExitCue); } }; - private onEnterCue = (event: TextTrackEnterCueEvent) => { + private handleAddVideoTrack = (event: AddTrackEvent) => { + (event.track as MediaTrack).addEventListener('activequalitychanged', this.handleQualityChanged); + }; + + private handleRemoveVideoTrack = (event: RemoveTrackEvent) => { + (event.track as MediaTrack).removeEventListener('activequalitychanged', this.handleQualityChanged); + }; + + private handleEnterCue = (event: TextTrackEnterCueEvent) => { const chapterCue = event.cue; if (this._currentChapter && this._currentChapter.endTime !== chapterCue.startTime) { - void this.queueOrSendEvent(EventType.chapterSkip, { playhead: this._player.currentTime }); + this.queueOrSendEvent(EventType.chapterSkip); } - void this.queueOrSendEvent(EventType.chapterStart, { - playhead: this._player.currentTime, - chapterDetails: calculateChapterDetails(chapterCue), - customMetadata: this._customMetadata, - }); + this.queueOrSendEvent( + EventType.chapterStart, + this._media?.createChapterObject( + sanitiseChapterId(chapterCue.id), + idToInt(chapterCue.id, 1), + Math.trunc(chapterCue.endTime), + Math.trunc(chapterCue.endTime - chapterCue.startTime), + ), + this._customMetadata, + ); this._currentChapter = chapterCue; }; - private onExitCue = () => { - void this.queueOrSendEvent(EventType.chapterComplete, { playhead: this._player.currentTime }); + private handleExitCue = () => { + this.queueOrSendEvent(EventType.chapterComplete); }; - private onError = (error: ErrorEvent) => { - void this.setError({ - name: error.errorObject.name, - source: ErrorSource.PLAYER, - }); + private handleError = (error: ErrorEvent) => { + this.setError(error.errorObject.code.toString()); }; - private onAdBreakBegin = (event: AdBreakEvent<'adbreakbegin'>) => { - const podDetails = calculateAdvertisingPodDetails(event.adBreak, this._adBreakPodIndex); - void this.queueOrSendEvent(EventType.adBreakStart, { - playhead: this._player.currentTime, - advertisingPodDetails: podDetails, - }); - if (podDetails.index > this._adBreakPodIndex) { + private handleAdBreakBegin = (event: AdBreakEvent<'adbreakbegin'>) => { + this.logDebug('onAdBreakBegin'); + const currentAdBreakTimeOffset = event.adBreak.timeOffset; + let index: number; + if (currentAdBreakTimeOffset === 0) { + index = 0; + } else if (currentAdBreakTimeOffset < 0) { + index = -1; + } else { + index = this._adBreakPodIndex + 1; + } + this.queueOrSendEvent(EventType.adBreakStart, this._media?.createAdBreakObject(PROP_NA, index, currentAdBreakTimeOffset)); + if (index > this._adBreakPodIndex) { this._adBreakPodIndex++; } }; - private onAdBegin = (event: AdEvent<'adbegin'>) => { - void this.queueOrSendEvent(EventType.adStart, { - playhead: this._player.currentTime, - advertisingDetails: calculateAdvertisingDetails(event.ad, this._adPodPosition), - customMetadata: this._customMetadata, - }); - this._adPodPosition++; - }; - - private onAdEnd = () => { - void this.queueOrSendEvent(EventType.adComplete, { playhead: this._player.currentTime }); + private handleAdBreakEnd = () => { + this.logDebug('onAdBreakEnd'); + this._adPodPosition = 1; + this.queueOrSendEvent(EventType.adBreakComplete); }; - private onAdSkip = () => { - void this.queueOrSendEvent(EventType.adSkip, { playhead: this._player.currentTime }); + private handleAdBegin = (event: AdEvent<'adbegin'>) => { + this.logDebug('onAdBegin'); + this.queueOrSendEvent( + EventType.adStart, + this._media?.createAdObject(PROP_NA, PROP_NA, this._adPodPosition, event.ad.duration ? Math.trunc(event.ad.duration) : 0), + this._customMetadata, + ); + this._adPodPosition++; }; - private onAdBreakEnd = () => { - this._adPodPosition = 1; - void this.queueOrSendEvent(EventType.adBreakComplete, { playhead: this._player.currentTime }); + private handleAdEnd = () => { + this.logDebug('onAdEnd'); + this.queueOrSendEvent(EventType.adComplete); }; - private onBeforeUnload = () => { - void this.maybeEndSession(); + private handleAdSkip = () => { + this.logDebug('onAdSkip'); + this.queueOrSendEvent(EventType.adSkip); }; - private async maybeEndSession(): Promise { - this.logDebug(`maybeEndSession`); - if (this.hasSessionStarted()) { - await this.queueOrSendEvent(EventType.sessionEnd, { playhead: this._player.currentTime }); - this._sessionId = undefined; - } - this.reset(); - return Promise.resolve(); - } - /** * Start a new session, but only if: * - no existing session has is in progress; @@ -315,11 +384,11 @@ export class AdobeEdgeHandler { * @param mediaLengthSec * @private */ - private async maybeStartSession(mediaLengthSec?: number): Promise { + private maybeStartSession(mediaLengthSec?: number) { const mediaLength = this.getContentLength(mediaLengthSec); const hasValidSource = this._player.source !== undefined; const hasValidDuration = isValidDuration(mediaLength); - const isPlayingAd = this._player.ads?.playing ?? false; + const isPlayingAd = this._player.ads?.playing; this.logDebug( `maybeStartSession -`, @@ -329,27 +398,61 @@ export class AdobeEdgeHandler { `isPlayingAd: ${isPlayingAd}`, ); - if (this._sessionInProgress || !hasValidSource || !hasValidDuration || isPlayingAd) { - this.logDebug('maybeStartSession - NOT started'); + if (this._sessionInProgress) { + this.logDebug('maybeStartSession - NOT started: already in progress'); return; } - const sessionDetails = { - ID: 'N/A', - name: this._player?.source?.metadata?.title ?? 'N/A', - channel: 'N/A', - contentType: this.getContentType(), - playerName: 'THEOplayer', - length: mediaLength, - }; - - await this.startSession(sessionDetails, this._customMetadata); - if (!this.hasSessionStarted()) { + if (isPlayingAd) { + this.logDebug('maybeStartSession - NOT started: playing ad'); return; } + if (!hasValidSource || !hasValidDuration) { + this.logDebug(`maybeStartSession - NOT started: invalid ${hasValidSource ? 'duration' : 'source'}`); + return; + } + + this._tracker?.trackSessionStart( + this._media?.createMediaObject( + this._player?.source?.metadata?.title ?? PROP_NA, + this._player?.source?.metadata?.id ?? PROP_NA, + mediaLength, + this.getContentType(), + this._media.MediaType.Video, + ), + this._customMetadata, + ); + this._sessionInProgress = true; - this.logDebug('maybeStartSession - STARTED', `sessionId: ${this.sessionId}`); + + // Post any queued events now that the session has started. + this._eventQueue.forEach((event) => this.sendEvent(event.type, event.info, event.metadata)); + this._eventQueue = []; + + this.logDebug('maybeStartSession - started'); + } + + private onLoadedMetadata = () => { + this.logDebug('onLoadedMetadata'); + + // NOTE: In case of a pre-roll ad: + // - on Android & iOS, the onLoadedMetadata is sent *after* a pre-roll has finished; + // - on Web, onLoadedMetadata is sent twice, once before the pre-roll, where player.duration is still NaN, + // and again after the pre-roll with a correct duration. + this.maybeStartSession(this._player.duration); + }; + + private onBeforeUnload = () => { + this.maybeEndSession(); + }; + + private maybeEndSession() { + this.logDebug(`maybeEndSession`); + if (this._sessionInProgress) { + this.queueOrSendEvent(EventType.sessionEnd); + } + this.reset(); } private getContentLength(mediaLengthSec?: number): number { @@ -360,135 +463,23 @@ export class AdobeEdgeHandler { return this._player.duration === Infinity ? ContentType.LIVE : ContentType.VOD; } - reset(): void { + reset() { this.logDebug('reset'); + this._eventQueue = []; this._adBreakPodIndex = 0; this._adPodPosition = 1; this._sessionInProgress = false; this._currentChapter = undefined; - this._sessionId = undefined; - this._hasSessionFailed = false; - this._eventQueue = []; } - async destroy(): Promise { - await this.maybeEndSession(); + destroy() { + this.maybeEndSession(); this.removeEventListeners(); } setDebug(debug: boolean) { this._debug = debug; - this._alloyClient('setDebug', { enabled: debug }); - } - - get sessionId(): string | undefined { - return this._sessionId; - } - - hasSessionStarted(): boolean { - return this._sessionId !== undefined; - } - - hasSessionFailed(): boolean { - return this._hasSessionFailed; - } - - async setError(errorDetails: AdobeErrorDetails, qoeDataDetails?: AdobeQoeDataDetails) { - return this.queueOrSendEvent(EventType.error, { playhead: this._player.currentTime, qoeDataDetails, errorDetails }); - } - - async statesUpdate( - playhead: number | undefined, - statesStart?: AdobePlayerStateData[], - statesEnd?: AdobePlayerStateData[], - qoeDataDetails?: AdobeQoeDataDetails, - ) { - return this.queueOrSendEvent(EventType.statesUpdate, { - playhead, - qoeDataDetails, - statesStart, - statesEnd, - }); - } - - /** - * Start a manually-tracked media sessions. - * - * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/createmediasession} - */ - async startSession(sessionDetails: AdobeSessionDetails, customMetadata?: AdobeCustomMetadataDetails[], qoeDataDetails?: AdobeQoeDataDetails) { - try { - const result = await this._alloyClient('createMediaSession', { - xdm: { - eventType: EventType.sessionStart, - timestamp: new Date().toISOString(), - mediaCollection: { - playhead: 0, - sessionDetails, - qoeDataDetails, - customMetadata, - }, - identityMap: this._customIdentityMap, - }, - }); - - this._sessionId = result.sessionId; - - // empty queue - if (this._sessionId && this._eventQueue.length !== 0) { - this._eventQueue.forEach((doPostEvent) => doPostEvent()); - this._eventQueue = []; - } - } catch (e) { - console.error(TAG, `Failed to start session. ${JSON.stringify(e)}`); - this._hasSessionFailed = true; - } - } - - async queueOrSendEvent(eventType: EventType, mediaDetails: AdobeMediaDetails) { - // Do not bother queueing the event in case starting the session has failed - if (this.hasSessionFailed()) { - return; - } - const doPostEvent = () => { - return this.sendMediaEvent(eventType, mediaDetails); - }; - - // If the session has already started, do not queue but send it directly. - if (!this.hasSessionStarted()) { - this._eventQueue.push(doPostEvent); - } else { - return doPostEvent(); - } - } - - /** - * Use the sendMediaEvent command to track media playbacks, pauses, completions, player state updates, and other - * related events. - * {@link https://experienceleague.adobe.com/en/docs/experience-platform/collection/js/commands/sendmediaevent} - */ - private async sendMediaEvent(eventType: EventType, mediaDetails: AdobeMediaDetails) { - // Make sure we are positing data with a valid sessionID. - if (!this._sessionId) { - console.error(TAG, 'Invalid sessionID'); - return; - } - - try { - this._alloyClient('sendMediaEvent', { - xdm: { - eventType, - mediaCollection: { - ...mediaDetails, - playhead: sanitisePlayhead(mediaDetails.playhead), - sessionID: this._sessionId, - }, - identityMap: this._customIdentityMap, - }, - }); - } catch (e) { - console.error(TAG, `Failed to send event: ${JSON.stringify(e)}`); - } + this._alloyClient?.('setDebug', { enabled: debug }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -502,3 +493,5 @@ export class AdobeEdgeHandler { function findAlloyClient(datastreamId: string, orgId: string): AlloyClient | undefined { return createdClients.find((client) => client.datastreamId === datastreamId && client.orgId === orgId)?.client; } + +export { AdobeEdgeHandler }; diff --git a/adobe-edge/src/internal/web/EventType.ts b/adobe-edge/src/internal/web/EventType.ts index b65ca7de..707a28e6 100644 --- a/adobe-edge/src/internal/web/EventType.ts +++ b/adobe-edge/src/internal/web/EventType.ts @@ -1,19 +1,22 @@ export enum EventType { - sessionStart = 'media.sessionStart', - play = 'media.play', - bitrateChange = 'media.bitrateChange', - bufferStart = 'media.bufferStart', - pauseStart = 'media.pauseStart', - adBreakStart = 'media.adBreakStart', - adStart = 'media.adStart', - adComplete = 'media.adComplete', - adSkip = 'media.adSkip', - adBreakComplete = 'media.adBreakComplete', - chapterStart = 'media.chapterStart', - chapterSkip = 'media.chapterSkip', - chapterComplete = 'media.chapterComplete', - error = 'media.error', - sessionEnd = 'media.sessionEnd', - sessionComplete = 'media.sessionComplete', - statesUpdate = 'media.statesUpdate', + sessionStart = 'sessionStart', + play = 'play', + bitrateChange = 'bitrateChange', + bufferStart = 'bufferStart', + pauseStart = 'pauseStart', + adBreakStart = 'adBreakStart', + adStart = 'adStart', + adComplete = 'adComplete', + adSkip = 'adSkip', + adBreakComplete = 'adBreakComplete', + chapterStart = 'chapterStart', + chapterSkip = 'chapterSkip', + chapterComplete = 'chapterComplete', + error = 'error', + sessionEnd = 'sessionEnd', + sessionComplete = 'sessionComplete', + statesUpdate = 'statesUpdate', + updatePlayhead = 'updatePlayhead', + seekStart = 'seekStart', + seekEnd = 'seekEnd', } diff --git a/adobe-edge/src/internal/web/Media.ts b/adobe-edge/src/internal/web/Media.ts new file mode 100644 index 00000000..f7b941fa --- /dev/null +++ b/adobe-edge/src/internal/web/Media.ts @@ -0,0 +1,120 @@ +/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/ban-types */ +/** + * Internal types not exported by the Adobe SDK. + */ +type AlloyClient = Function; +export default AlloyClient; + +export type StreamType = { + readonly VOD: 'vod'; + readonly Live: 'live'; + readonly Linear: 'linear'; + readonly Podcast: 'podcast'; + readonly Audiobook: 'audiobook'; + readonly AOD: 'aod'; +}; + +export type MediaType = { + readonly Video: 'video'; + readonly Audio: 'audio'; +}; + +export type MediaTracker = { + trackSessionStart: (mediaObject: any, contextData?: any) => any; + trackPlay: () => any; + trackPause: () => any; + trackSessionEnd: () => any; + trackComplete: () => any; + trackError: (errorId: any) => any; + trackEvent: (eventType: any, info: any, context: any) => any; + updatePlayhead: (time: any) => void; + updateQoEObject: (qoeObject: any) => void; + destroy: () => void; +}; + +export type MediaObject = + | { + sessionDetails: { + name: any; + friendlyName: any; + length: number; + streamType: any; + contentType: any; + }; + } + | { + sessionDetails?: undefined; + }; + +export type AdBreakObject = + | { + advertisingPodDetails: { + friendlyName: any; + offset: any; + index: any; + }; + } + | { + advertisingPodDetails?: undefined; + }; + +export type AdObject = + | { + advertisingDetails: { + friendlyName: any; + name: any; + podPosition: any; + length: any; + }; + } + | { + advertisingDetails?: undefined; + }; + +type ChapterObject = + | { + chapterDetails: { + friendlyName: any; + offset: any; + index: any; + length: any; + }; + } + | { + chapterDetails?: undefined; + }; + +export type StateObject = + | { + name: any; + } + | { + name?: undefined; + }; + +export type QoEObject = + | { + bitrate: any; + droppedFrames: any; + framesPerSecond: any; + timeToStart: any; + } + | { + bitrate?: undefined; + droppedFrames?: undefined; + framesPerSecond?: undefined; + timeToStart?: undefined; + }; + +export type Media = { + getInstance: () => MediaTracker; + createMediaObject: (friendlyName: any, name: any, length: any, contentType: any, streamType: any) => MediaObject; + createAdBreakObject: (name: any, position: any, startTime: any) => AdBreakObject; + createAdObject: (name: any, id: any, position: any, length: any) => AdObject; + createChapterObject: (name: any, position: any, length: any, startTime: any) => ChapterObject; + createStateObject: (stateName: any) => StateObject; + createQoEObject: (bitrate: any, droppedFrames: any, fps: any, startupTime: any) => QoEObject; + + StreamType: StreamType; + MediaType: MediaType; +}; diff --git a/adobe-edge/src/internal/web/Utils.ts b/adobe-edge/src/internal/web/Utils.ts index 80aa2582..3a7fe026 100644 --- a/adobe-edge/src/internal/web/Utils.ts +++ b/adobe-edge/src/internal/web/Utils.ts @@ -1,10 +1,15 @@ -import type { Ad, AdBreak, TextTrackCue } from 'react-native-theoplayer'; -import type { - AdobeAdvertisingDetails, - AdobeAdvertisingPodDetails, - AdobeChapterDetails, - AdobeEdgeWebConfig, -} from '@theoplayer/react-native-analytics-adobe-edge'; +import type { AdobeCustomMetadataDetails, AdobeEdgeWebConfig } from '@theoplayer/react-native-analytics-adobe-edge'; + +const PROP_NA = 'N/A'; + +/** + * Sanitise the current media length. + * + * - In case of a live stream, set it to 24h. + */ +export function sanitiseContentLength(mediaLengthSec: number): number { + return mediaLengthSec === Infinity ? 86400 : Math.trunc(mediaLengthSec); +} /** * Sanitise the current playhead in seconds. Adobe expects an integer value. @@ -26,61 +31,39 @@ export function sanitisePlayhead(playheadInSec?: number): number { return Math.trunc(playheadInSec); } -/** - * Sanitise the current media length. - * - * - In case of a live stream, set it to 24h. - */ -export function sanitiseContentLength(mediaLengthSec: number): number { - return mediaLengthSec === Infinity ? 86400 : Math.trunc(mediaLengthSec); -} - -export function sanitiseConfig(config: AdobeEdgeWebConfig): AdobeEdgeWebConfig { - return { - ...config, - streamingMedia: { - ...config.streamingMedia, - channel: config.streamingMedia?.channel || 'defaultChannel', - playerName: config.streamingMedia?.playerName || 'THEOplayer', - }, - }; -} - export function isValidDuration(v: number | undefined): boolean { return v !== undefined && !Number.isNaN(v); } -export function calculateAdvertisingPodDetails(adBreak: AdBreak, lastPodIndex: number): AdobeAdvertisingPodDetails { - const currentAdBreakTimeOffset = adBreak.timeOffset; - let podIndex: number; - if (currentAdBreakTimeOffset === 0) { - podIndex = 0; - } else if (currentAdBreakTimeOffset < 0) { - podIndex = -1; - } else { - podIndex = lastPodIndex++; +export function toAdobeCustomMetadataDetails(details: AdobeCustomMetadataDetails[]): { [key: string]: string } { + const map: { [key: string]: string } = {}; + for (const item of details) { + if (item.name && item.value) { + map[item.name] = item.value; + } } - return { - index: podIndex ?? 0, - offset: Math.trunc(currentAdBreakTimeOffset), - }; + return map; } -export function calculateAdvertisingDetails(ad: Ad, podPosition: number): AdobeAdvertisingDetails { - return { - podPosition, - length: ad.duration ? Math.trunc(ad.duration) : 0, - name: 'NA', - playerName: 'THEOplayer', - }; +export function sanitiseChapterId(id?: string): string { + if (!id || id.trim().length === 0) { + return PROP_NA; + } + return id; +} + +export function idToInt(id?: string, otherwise: number = 0): number { + const intId = Number(id); + return isNaN(intId) ? otherwise : intId; } -export function calculateChapterDetails(cue: TextTrackCue): AdobeChapterDetails { - const id = Number(cue.id); - const index = isNaN(id) ? 0 : id; +export function sanitiseConfig(config: AdobeEdgeWebConfig): AdobeEdgeWebConfig { return { - length: Math.trunc(cue.endTime - cue.startTime), - offset: Math.trunc(cue.startTime), - index, + ...config, + streamingMedia: { + ...config.streamingMedia, + channel: config.streamingMedia?.channel || 'defaultChannel', + playerName: config.streamingMedia?.playerName || 'THEOplayer', + }, }; } From 3e933197777aae72637c45b00f760f0507005fc0 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Thu, 15 Jan 2026 15:51:16 +0100 Subject: [PATCH 39/70] Update hook --- adobe-edge/README.md | 21 +++++++++++++++++---- adobe-edge/src/api/hooks/useAdobe.ts | 8 ++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/adobe-edge/README.md b/adobe-edge/README.md index 9a8ea2ec..8372b689 100644 --- a/adobe-edge/README.md +++ b/adobe-edge/README.md @@ -29,7 +29,7 @@ Create the connector by providing the `THEOplayer` instance and a configuration Web and mobile platforms. ```tsx -import { useAdobe } from '@theoplayer/react-native-analytics-adobe-edge'; +import {useAdobe} from '@theoplayer/react-native-analytics-adobe-edge'; const config = { web: { @@ -42,10 +42,23 @@ const config = { environmentId: 'abcdef123456/abcdef123456/launch-1234567890abcdef1234567890abcdef12', debugEnabled: true, }, -}; +} + +/** + * An optional custom identity map to associate the media session with user identities. + */ +const customIdentityMap = { + EMAIL: [ + { + id: 'user@example.com', + authenticatedState: 'authenticated', + primary: false, + }, + ], +} const App = () => { - const [adobe, initAdobe] = useAdobe(config); + const [adobe, initAdobe] = useAdobe(config, customIdentityMap); const onPlayerReady = (player: THEOplayer) => { // Initialize Adobe connector @@ -64,7 +77,7 @@ such as duration or whether it is a live or vod. The connector allows passing or updating the current asset's metadata at any time: ```typescript -import { AdobeCustomMetadataDetails } from "@theoplayer/react-native-analytics-adobe-edge"; +import {AdobeCustomMetadataDetails} from "@theoplayer/react-native-analytics-adobe-edge"; const onUpdateMetadata = () => { const metadata: AdobeCustomMetadataDetails[] = [ diff --git a/adobe-edge/src/api/hooks/useAdobe.ts b/adobe-edge/src/api/hooks/useAdobe.ts index 6635e965..72e7c980 100644 --- a/adobe-edge/src/api/hooks/useAdobe.ts +++ b/adobe-edge/src/api/hooks/useAdobe.ts @@ -2,8 +2,12 @@ import { PlayerEventType, THEOplayer } from 'react-native-theoplayer'; import { RefObject, useEffect, useRef } from 'react'; import { AdobeConnector } from '../AdobeConnector'; import { AdobeEdgeConfig } from '../AdobeEdgeConfig'; +import { AdobeIdentityMap } from '../details/AdobeIdentityMap'; -export function useAdobe(config: AdobeEdgeConfig): [RefObject, (player: THEOplayer | undefined) => void] { +export function useAdobe( + config: AdobeEdgeConfig, + customIdentityMap?: AdobeIdentityMap, +): [RefObject, (player: THEOplayer | undefined) => void] { const connector = useRef(undefined); const theoPlayer = useRef(undefined); @@ -13,7 +17,7 @@ export function useAdobe(config: AdobeEdgeConfig): [RefObject Date: Thu, 15 Jan 2026 16:44:59 +0100 Subject: [PATCH 40/70] Allow override by customMetadata --- adobe-edge/src/internal/web/AdobeEdgeHandler.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts index 01df7b7c..68a942f8 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -413,10 +413,15 @@ class AdobeEdgeHandler { return; } + // Allow overriding metadata with custom metadata set via updateMetadata(). + const mergedMetadata = { + ...this._player?.source?.metadata, + ...this._customMetadata, + }; this._tracker?.trackSessionStart( this._media?.createMediaObject( - this._player?.source?.metadata?.title ?? PROP_NA, - this._player?.source?.metadata?.id ?? PROP_NA, + mergedMetadata.friendlyName || mergedMetadata.title || PROP_NA, + mergedMetadata.name || mergedMetadata.id || PROP_NA, mediaLength, this.getContentType(), this._media.MediaType.Video, From be87ad91683e663fd74487309bb242987b68c9f2 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 09:14:12 +0100 Subject: [PATCH 41/70] Simplify api --- .../adobe/edge/AdobeEdgeHandler.kt | 9 +- .../edge/ReactTHEOplayerAdobeEdgeModule.kt | 21 +- .../reactnative/adobe/edge/Utils.kt | 12 +- adobe-edge/src/api/AdobeConnector.ts | 11 +- .../api/details/AdobeAdvertisingDetails.ts | 49 ---- .../api/details/AdobeAdvertisingPodDetails.ts | 19 -- .../src/api/details/AdobeChapterDetails.ts | 30 --- .../api/details/AdobeCustomMetadataDetails.ts | 12 - .../src/api/details/AdobeErrorDetails.ts | 17 -- .../api/details/AdobeImplementationDetails.ts | 25 -- .../src/api/details/AdobeMediaDetails.ts | 52 ---- adobe-edge/src/api/details/AdobeMetadata.ts | 2 + .../src/api/details/AdobePlayerStateData.ts | 18 -- .../src/api/details/AdobeQoeDataDetails.ts | 87 ------- .../src/api/details/AdobeSessionDetails.ts | 240 ------------------ adobe-edge/src/api/details/barrel.ts | 13 +- .../src/internal/AdobeConnectorAdapter.ts | 8 +- .../internal/AdobeConnectorAdapterNative.ts | 10 +- .../src/internal/AdobeConnectorAdapterWeb.ts | 12 +- .../src/internal/web/AdobeEdgeConnector.ts | 10 +- .../src/internal/web/AdobeEdgeHandler.ts | 22 +- adobe-edge/src/internal/web/Utils.ts | 12 +- 22 files changed, 55 insertions(+), 636 deletions(-) delete mode 100644 adobe-edge/src/api/details/AdobeAdvertisingDetails.ts delete mode 100644 adobe-edge/src/api/details/AdobeAdvertisingPodDetails.ts delete mode 100644 adobe-edge/src/api/details/AdobeChapterDetails.ts delete mode 100644 adobe-edge/src/api/details/AdobeCustomMetadataDetails.ts delete mode 100644 adobe-edge/src/api/details/AdobeErrorDetails.ts delete mode 100644 adobe-edge/src/api/details/AdobeImplementationDetails.ts delete mode 100644 adobe-edge/src/api/details/AdobeMediaDetails.ts create mode 100644 adobe-edge/src/api/details/AdobeMetadata.ts delete mode 100644 adobe-edge/src/api/details/AdobePlayerStateData.ts delete mode 100644 adobe-edge/src/api/details/AdobeQoeDataDetails.ts delete mode 100644 adobe-edge/src/api/details/AdobeSessionDetails.ts diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt index aea11b37..8ffaf369 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -36,6 +36,7 @@ import com.theoplayer.android.api.event.track.texttrack.list.TextTrackListEventT import com.theoplayer.android.api.player.Player import com.theoplayer.android.api.player.track.texttrack.TextTrackKind import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCue +import kotlin.collections.toMutableMap typealias AddTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.AddTrackEvent typealias RemoveTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.RemoveTrackEvent @@ -77,7 +78,7 @@ data class QueuedEvent( const val PROP_CURRENT_TIME = "currentTime" const val PROP_ERROR_ID = "errorId" -const val PROP_NA = "NA" +const val PROP_NA = "N/A" class AdobeEdgeHandler( private val player: Player, @@ -485,10 +486,12 @@ class AdobeEdgeHandler( return } + // Allow overriding metadata with custom metadata set via updateMetadata(). + val mergedMetadata = (player.source?.metadata?.data?.mapValues { it.value as String } ?: emptyMap()) + customMetadata tracker.trackSessionStart( Media.createMediaObject( - player.source?.metadata?.get("title") ?: "N/A", - player.source?.metadata?.get("id") ?: "N/A", + mergedMetadata["friendlyName"] ?: mergedMetadata["title"] ?: PROP_NA, + mergedMetadata["name"] ?: mergedMetadata["id"] ?: PROP_NA, mediaLength, calculateStreamType(), Media.MediaType.Video, diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt index 2d466286..7c8e5d6f 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/ReactTHEOplayerAdobeEdgeModule.kt @@ -11,7 +11,6 @@ private const val TAG = "AdobeEdgeModule" private const val PROP_ENVIRONMENT_ID = "environmentId" private const val PROP_DEBUG_ENABLED = "debugEnabled" -private const val PROP_NAME = "name" @Suppress("unused") class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : @@ -61,18 +60,20 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : */ @ReactMethod fun setDebug(tag: Int, debug: Boolean) { - adobeConnectors[tag]?.setLoggingMode(when (debug) { - true -> LoggingMode.DEBUG - false -> LoggingMode.ERROR - }) + adobeConnectors[tag]?.setLoggingMode( + when (debug) { + true -> LoggingMode.DEBUG + false -> LoggingMode.ERROR + } + ) } /** * Sets customMetadataDetails which will be passed for the session start request. */ @ReactMethod - fun updateMetadata(tag: Int, metadataList: ReadableArray) { - adobeConnectors[tag]?.updateMetadata(metadataList.toAdobeCustomMetadataDetails()) + fun updateMetadata(tag: Int, customMetadataDetails: ReadableMap) { + adobeConnectors[tag]?.updateMetadata(customMetadataDetails.toAdobeCustomMetadataDetails()) } /** @@ -87,8 +88,8 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : * Dispatch error event to adobe */ @ReactMethod - fun setError(tag: Int, errorDetails: ReadableMap) { - adobeConnectors[tag]?.setError(errorDetails.getString(PROP_NAME) ?: "NA") + fun setError(tag: Int, errorId: String) { + adobeConnectors[tag]?.setError(errorId) } /** @@ -100,7 +101,7 @@ class ReactTHEOplayerAdobeModule(context: ReactApplicationContext) : * @param customMetadataDetails media details information. */ @ReactMethod - fun stopAndStartNewSession(tag: Int, customMetadataDetails: ReadableArray) { + fun stopAndStartNewSession(tag: Int, customMetadataDetails: ReadableMap) { adobeConnectors[tag]?.stopAndStartNewSession(customMetadataDetails.toAdobeCustomMetadataDetails()) } diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt index 37d05c93..e943a804 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt @@ -2,13 +2,9 @@ package com.theoplayer.reactnative.adobe.edge import com.adobe.marketing.mobile.edge.identity.AuthenticatedState import com.adobe.marketing.mobile.edge.identity.IdentityItem -import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.adobe.marketing.mobile.edge.identity.IdentityMap -private const val PROP_NAME = "name" -private const val PROP_VALUE = "value" - fun sanitiseContentLength(mediaLength: Double?): Int { return if (mediaLength == Double.POSITIVE_INFINITY) { 86400 } else mediaLength?.toInt() ?: 0 } @@ -29,12 +25,8 @@ fun isValidDuration(v: Double?): Boolean { return v != null && !v.isNaN() } -fun ReadableArray.toAdobeCustomMetadataDetails() : HashMap { - return hashMapOf().apply { - toArrayList() - .map { e -> (e as? ReadableMap) } - .filter { e -> e != null && e.hasKey(PROP_NAME) && e.hasKey(PROP_VALUE) } - } +fun ReadableMap.toAdobeCustomMetadataDetails() : HashMap { + return toHashMap().mapValues { it.value?.toString() ?: "" } as HashMap } fun ReadableMap.toAdobeIdentityMap(): IdentityMap { diff --git a/adobe-edge/src/api/AdobeConnector.ts b/adobe-edge/src/api/AdobeConnector.ts index 2cc510e5..9426a54b 100644 --- a/adobe-edge/src/api/AdobeConnector.ts +++ b/adobe-edge/src/api/AdobeConnector.ts @@ -1,12 +1,11 @@ import type { THEOplayer } from 'react-native-theoplayer'; import { AdobeConnectorAdapterNative } from '../internal/AdobeConnectorAdapterNative'; -import type { AdobeCustomMetadataDetails } from './details/AdobeCustomMetadataDetails'; -import type { AdobeErrorDetails } from './details/AdobeErrorDetails'; import { Platform } from 'react-native'; import { AdobeConnectorAdapter } from '../internal/AdobeConnectorAdapter'; import { AdobeConnectorAdapterWeb } from '../internal/AdobeConnectorAdapterWeb'; import { AdobeEdgeConfig } from './AdobeEdgeConfig'; import { AdobeIdentityMap } from './details/AdobeIdentityMap'; +import { AdobeMetadata } from './details/AdobeMetadata'; export class AdobeConnector { private connectorAdapter?: AdobeConnectorAdapter; @@ -33,7 +32,7 @@ export class AdobeConnector { /** * Sets customMetadataDetails which will be passed for the session start request. */ - updateMetadata(customMetadataDetails: AdobeCustomMetadataDetails[]): void { + updateMetadata(customMetadataDetails: AdobeMetadata): void { this.connectorAdapter?.updateMetadata(customMetadataDetails); } @@ -71,8 +70,8 @@ export class AdobeConnector { /** * Dispatch error event to adobe */ - setError(errorDetails: AdobeErrorDetails): void { - this.connectorAdapter?.setError(errorDetails); + setError(errorId: string): void { + this.connectorAdapter?.setError(errorId); } /** @@ -92,7 +91,7 @@ export class AdobeConnector { * * @param customMetadataDetails media details information. */ - stopAndStartNewSession(customMetadataDetails: AdobeCustomMetadataDetails[]): void { + stopAndStartNewSession(customMetadataDetails: AdobeMetadata): void { void this.connectorAdapter?.stopAndStartNewSession(customMetadataDetails); } diff --git a/adobe-edge/src/api/details/AdobeAdvertisingDetails.ts b/adobe-edge/src/api/details/AdobeAdvertisingDetails.ts deleted file mode 100644 index f146769c..00000000 --- a/adobe-edge/src/api/details/AdobeAdvertisingDetails.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Advertising details information. - * - * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/advertisingdetails.schema.md} - */ -export interface AdobeAdvertisingDetails { - // ID of the ad. Any integer and/or letter combination. - ID?: string; - - // Company/Brand whose product is featured in the ad. - advertiser?: string; - - // ID of the ad campaign. - campaignID?: string; - - // ID of the ad creative. - creativeID?: string; - - // URL of the ad creative. - creativeURL?: string; - - // Ad is completed. - isCompleted?: boolean; - - // Ad is started. - isStarted?: boolean; - - // Length of video ad in seconds. - length: number; - - // Friendly name of the ad. In reporting, “Ad Name” is the classification and “Ad Name (variable)” is the eVar. - name: string; - - // Placement ID of the ad. - placementID?: string; - - // The name of the player responsible for rendering the ad. - playerName: string; - - // The index of the ad inside the parent ad start, for example, the first ad has index 0 and the second ad has - // index 1. - podPosition: number; - - // ID of the ad site. - siteID?: string; - - // The total amount of time, in seconds, spent watching the ad (i.e., the number of seconds played). - timePlayed?: number; -} diff --git a/adobe-edge/src/api/details/AdobeAdvertisingPodDetails.ts b/adobe-edge/src/api/details/AdobeAdvertisingPodDetails.ts deleted file mode 100644 index a04e3415..00000000 --- a/adobe-edge/src/api/details/AdobeAdvertisingPodDetails.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Advertising Pod details information. - * - * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/advertisingpoddetails.schema.md} - */ -export interface AdobeAdvertisingPodDetails { - // The ID of the ad break. - ID?: string; - - // The friendly name of the Ad Break. - friendlyName?: string; - - // The index of the ad inside the parent ad break start, for example, the first ad has index 0 and the second ad - // has index 1. - index: number; - - // The offset of the ad break inside the content, in seconds. - offset: number; -} diff --git a/adobe-edge/src/api/details/AdobeChapterDetails.ts b/adobe-edge/src/api/details/AdobeChapterDetails.ts deleted file mode 100644 index c758e81e..00000000 --- a/adobe-edge/src/api/details/AdobeChapterDetails.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Chapter details information. - * - * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/chapterdetails.schema.md} - */ -export interface AdobeChapterDetails { - // The ID of the ad break. - ID?: string; - - // The friendly name of the Ad Break. - friendlyName?: string; - - // The position (index, integer) of the chapter inside the content. - index: number; - - // Chapter is completed. - isCompleted?: boolean; - - // Chapter is started. - isStarted?: boolean; - - // The length of the chapter, in seconds. - length: number; - - // The offset of the chapter inside the content (in seconds) from the start. - offset: number; - - // The time spent on the chapter, in seconds. - timePlayed?: number; -} diff --git a/adobe-edge/src/api/details/AdobeCustomMetadataDetails.ts b/adobe-edge/src/api/details/AdobeCustomMetadataDetails.ts deleted file mode 100644 index 1e99e850..00000000 --- a/adobe-edge/src/api/details/AdobeCustomMetadataDetails.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Custom metadata details information. - * - * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/custommetadatadetails.schema.md} - */ -export type AdobeCustomMetadataDetails = { - // The name of the custom field. - name?: string; - - // The value of the custom field. - value?: string; -}; diff --git a/adobe-edge/src/api/details/AdobeErrorDetails.ts b/adobe-edge/src/api/details/AdobeErrorDetails.ts deleted file mode 100644 index 56b265fa..00000000 --- a/adobe-edge/src/api/details/AdobeErrorDetails.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Error details information. - * - * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/errordetails.schema.md} - */ -export interface AdobeErrorDetails { - // The error ID. - name: string; - - // The error source. - source: ErrorSource; -} - -export enum ErrorSource { - PLAYER = 'player', - EXTERNAL = 'external', -} diff --git a/adobe-edge/src/api/details/AdobeImplementationDetails.ts b/adobe-edge/src/api/details/AdobeImplementationDetails.ts deleted file mode 100644 index 07d1b260..00000000 --- a/adobe-edge/src/api/details/AdobeImplementationDetails.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Details about the SDK, library, or service used in an application or web page implementation of a service. - * - * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/implementationdetails.schema.md} - */ -export interface AdobeImplementationDetails { - // The environment of the implementation - environment?: AdobeEnvironment; - - // SDK or endpoint identifier. All SDKs or endpoints are identified through a URI, including extensions. - name?: string; - - // The version identifier of the API, e.g h.18. - version?: string; -} - -/** - * The environment of the implementation. - */ -export enum AdobeEnvironment { - BROWSER = 'browser', - APP = 'app', - SERVER = 'server', - IOT = 'iot', -} diff --git a/adobe-edge/src/api/details/AdobeMediaDetails.ts b/adobe-edge/src/api/details/AdobeMediaDetails.ts deleted file mode 100644 index b2449048..00000000 --- a/adobe-edge/src/api/details/AdobeMediaDetails.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { AdobeSessionDetails } from './AdobeSessionDetails'; -import type { AdobeQoeDataDetails } from './AdobeQoeDataDetails'; -import type { AdobeCustomMetadataDetails } from './AdobeCustomMetadataDetails'; -import type { AdobeAdvertisingDetails } from './AdobeAdvertisingDetails'; -import type { AdobeAdvertisingPodDetails } from './AdobeAdvertisingPodDetails'; -import type { AdobeChapterDetails } from './AdobeChapterDetails'; -import type { AdobeErrorDetails } from './AdobeErrorDetails'; -import type { AdobePlayerStateData } from './AdobePlayerStateData'; - -/** - * Media details information. - * - * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/mediadetails.schema.md} - */ -export interface AdobeMediaDetails { - // If the content is live, the playhead must be the current second of the day, 0 <= playhead < 86400. - // If the content is recorded, the playhead must be the current second of content, 0 <= playhead < content length. - playhead?: number; - - // Identifies an instance of a content stream unique to an individual playback. - sessionID?: string; - - // Session details information related to the experience event. - sessionDetails?: AdobeSessionDetails; - - // Advertising details information related to the experience event. - advertisingDetails?: AdobeAdvertisingDetails; - - // Advertising Pod details information - advertisingPodDetails?: AdobeAdvertisingPodDetails; - - // Chapter details information related to the experience event. - chapterDetails?: AdobeChapterDetails; - - // Error details information related to the experience event. - errorDetails?: AdobeErrorDetails; - - // Qoe data details information related to the experience event. - qoeDataDetails?: AdobeQoeDataDetails; - - // The list of states start. - statesStart?: AdobePlayerStateData[]; - - // The list of states end. - statesEnd?: AdobePlayerStateData[]; - - // The list of states. - states?: AdobePlayerStateData[]; - - // The list of custom metadata. - customMetadata?: AdobeCustomMetadataDetails[]; -} diff --git a/adobe-edge/src/api/details/AdobeMetadata.ts b/adobe-edge/src/api/details/AdobeMetadata.ts new file mode 100644 index 00000000..04cc7197 --- /dev/null +++ b/adobe-edge/src/api/details/AdobeMetadata.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AdobeMetadata = { [key: string]: any }; diff --git a/adobe-edge/src/api/details/AdobePlayerStateData.ts b/adobe-edge/src/api/details/AdobePlayerStateData.ts deleted file mode 100644 index c27dd970..00000000 --- a/adobe-edge/src/api/details/AdobePlayerStateData.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Player state data information. - * - * {@link https://github.com/adobe/xdm/blob/master/components/datatypes/playerstatedata.schema.json} - */ -export interface AdobePlayerStateData { - // The name of the player state. - name: string; - - // Whether or not the player state is set on that state. - isSet?: boolean; - - // The number of times that player state was set on the stream. - count?: number; - - // he total duration of that player state. - time?: number; -} diff --git a/adobe-edge/src/api/details/AdobeQoeDataDetails.ts b/adobe-edge/src/api/details/AdobeQoeDataDetails.ts deleted file mode 100644 index 88d5bc2a..00000000 --- a/adobe-edge/src/api/details/AdobeQoeDataDetails.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Qoe data details information related to the experience event. - * - * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/qoedatadetails.schema.md} - */ -export interface AdobeQoeDataDetails { - // The average bitrate (in kbps). The value is predefined buckets at 100kbps intervals. The Average Bitrate is - // computed as a weighted average of all bitrate values related to the play duration that occurred during a playback - // session. - bitrateAverage?: string; - - // The bitrate value (in kbps). - bitrate?: number; - - // The average bitrate (in kbps, integer). This metric is computed as a weighted average of all bitrate - // values related to the play duration that occurred during a playback session. - bitrateAverageBucket?: number; - - // The number of streams in which bitrate changes occurred. This metric is set to true only if at least one bitrate - // change event occurred during a playback session. - hasBitrateChangeImpactedStreams?: boolean; - - // The number of bitrate changes. This value is computed as a sum of all bitrate change events that occurred - // during a playback session. - bitrateChangeCount?: number; - - // The number of streams in which frames were dropped. This metric is set to true only if at least one frame was - // dropped during a playback session. - hasDroppedFrameImpactedStreams?: boolean; - - // The number of frames dropped during playback of the main content. - droppedFrames?: number; - - // The number of times a user quit the video before its start. This metric is set to true only if no content was - // rendered, regardless of ads. - isDroppedBeforeStart?: boolean; - - // The current value of the stream frame-rate (in frames per second). The field is mapped to the fps field on the - // close call and can be accessed through processing rules. - framesPerSecond?: number; - - // Describes the duration (in seconds) passed between video load and start. - timeToStart?: number; - - // The number of streams impacted by buffering. This metric is set to true only if at least one buffer event - // occurred during a playback session. - hasBufferImpactedStreams?: boolean; - - // The number of buffer events. This metric is computed as a count of the different buffer states that occurred - // during a playback session. This is a count of how many times the player enters a buffer state from other states, - // e.g., playing or pausing. - bufferCount?: number; - - // The total amount of time, in seconds, spent buffering. This value is computed as a sum of all buffer events - // durations that occurred during a playback session. - bufferTime?: number; - - // The number of streams in which an error event occurred (i.e., trackError was called during the playback session, - // and a type=error heartbeat call was generated). This metric is set to true only if at least one error occurred - // during playback. - hasErrorImpactedStreams?: boolean; - - // The number of errors that occurred (Integer). This value is computed as a sum of all error events that occurred - // during a playback session. - errorCount?: number; - - // The number of streams in which a stalled event occurred. This metric is set to true only if at least one stall - // occurred during playback. - hasStallImpactedStreams?: boolean; - - // The number of times the playback was stalled during a playback session. - stallCount?: number; - - // The total time (seconds; integer) the playback was stalled during a playback session. - stallTime?: number; - - // The unique error IDs generated by the player SDK. Customers must provide the error codes/ids at implementation - // time via provided error APIs. - playerSdkErrors?: string[]; - - // The unique error IDs from any external source, e.g., CDN errors. Customers must provide the error codes/ids at - // implementation time via provided error APIs. - externalErrors?: string[]; - - // The unique error IDs generated by Media SDK during playback. - mediaSdkErrors?: string[]; -} diff --git a/adobe-edge/src/api/details/AdobeSessionDetails.ts b/adobe-edge/src/api/details/AdobeSessionDetails.ts deleted file mode 100644 index 9ff9a2f4..00000000 --- a/adobe-edge/src/api/details/AdobeSessionDetails.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * Session details information related to the experience event. - * - * {@link https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/sessiondetails.schema.md} - */ -export interface AdobeSessionDetails { - // This identifies an instance of a content stream unique to an individual playback. - ID?: string; - - // The number of ads started during the playback. - adCount?: number; - - // The type of ad loaded as defined by each customer's internal representation. - adLoad?: string; - - // The name of the album that the music recording or video belongs to. - album?: string; - - // The SDK version used by the player. This could have any custom value that makes sense for your player. - appVersion?: string; - - // The name of the album artist or group performing the music recording or video. - artist?: string; - - // This is the unique identifier for the content of the media asset, such as the TV series episode identifier, - // movie asset identifier, or live event identifier. Typically, these IDs are derived from metadata authorities such - // as EIDR, TMS/Gracenote, or Rovi. These identifiers can also be from other proprietary or in-house systems. - assetID?: string; - - // Name of the media author. - author?: string; - - // Describes the average content time spent for a specific media item - i.e. the total content time spent divided - // by the length for all the playback sessions. - averageMinuteAudience?: number; - - // Distribution channel from where the content was played. - channel: string; - - // The number of chapters started during the playback. - chapterCount?: number; - - // The type of the stream delivery. Available values per Stream Type: Audio: “song”, “podcast”, “audiobook”, - // “radio”; Video: “VoD”, “Live”, “Linear”, “UGC”, “DVoD”. Customers can provide custom values for this parameter. - contentType: ContentType; - - // A property that defines the time of the day when the content was broadcast or played. This could have any - // value set as necessary by customers. - dayPart?: string; - - // The number of the episode. - episode?: string; - - // The estimated number of video or audio streams per each individual content. - estimatedStreams?: number; - - // The type of feed, which can either represent actual feed-related data such as EAST HD or SD, or the source of - // the feed like a URL. - feed?: string; - - // The date when the content first aired on television. Any date format is acceptable, - // but Adobe recommends: YYYY-MM-DD. - firstAirDate?: string; - - // The date when the content first aired on any digital channel or platform. Any date format is acceptable but - // Adobe recommends: YYYY-MM-DD. - firstDigitalDate?: string; - - // This is the “friendly” (human-readable) name of the content. - friendlyName?: string; - - // Type or grouping of content as defined by content producer. Values should be comma delimited in variable - // implementation. - genre?: string; - - // Indicates if one or more pauses occurred during the playback of a single media item. - hasPauseImpactedStreams?: boolean; - - // Indicates that the playhead passed the 10% marker of media based on stream length. The marker is only counted - // once, even if seeking backwards. If seeking forward, markers that are skipped are not counted. - hasProgress10?: boolean; - - // Indicates that the playhead passed the 25% marker of media based on stream length. Marker only counted once, - // even if seeking backwards. If seeking forward, markers that are skipped are not counted. - hasProgress25?: boolean; - - // Indicates that the playhead passed the 50% marker of media based on stream length. Marker only counted once, - // even if seeking backwards. If seeking forward, markers that are skipped are not counted. - hasProgress50?: boolean; - - // Indicates that the playhead passed the 75% marker of media based on stream length. Marker only counted once, - // even if seeking backwards. If seeking forward, markers that are skipped are not counted. - hasProgress75?: boolean; - - // Indicates that the playhead passed the 95% marker of media based on stream length. Marker only counted once, - // even if seeking backwards. If seeking forward, markers that are skipped are not counted. - hasProgress95?: boolean; - - // Marks each playback that was resumed after more than 30 minutes of buffer, pause, or stall period. - hasResume?: boolean; - - // Indicates when at least one frame, not necessarily the first has been viewed. - hasSegmentView?: boolean; - - // The user has been authorized via Adobe authentication. - isAuthorized?: boolean; - - // Indicates if a timed media asset was watched to completion, this does not necessarily mean the viewer watched - // the whole video; viewer could have skipped ahead. - isCompleted?: boolean; - - // The stream was played locally on the device after being downloaded. - isDownloaded?: boolean; - - // Set to true when the hit is federated (i.e., received by the customer as part of a federated data share, - // rather than their own implementation). - isFederated?: boolean; - - // First frame of media is consumed. If the user drops during ad, buffering, etc., then there would be no - // “Content Start” event. - isPlayed?: boolean; - - // Load event for the media. (This occurs when the viewer clicks the Play button). - // This would count even if there are pre-roll ads, buffering, errors, and so on. - isViewed?: boolean; - - // Name of the record label. - label?: string; - - // Clip Length/Runtime - This is the maximum length (or duration) of the content being consumed (in seconds). - length: number; - - // MVPD provided via Adobe authentication. - mvpd?: string; - - // Content ID of the content, which can be used to tie back to other industry / CMS IDs. - name: string; - - // The network/channel name. - network?: string; - - // Creator of the content. - originator?: string; - - // The number of pause periods that occurred during playback. - pauseCount?: number; - - // Describes the duration in seconds in which playback was paused by the user. - pauseTime?: number; - - // Name of the content player. - playerName: string; - - // Name of the audio content publisher. - publisher?: string; - - // Rating as defined by TV Parental Guidelines. - rating?: string; - - // The season number the show belongs to. Season Series is required only if the show is part of a series. - season?: string; - - // Indicates the amount of time, in seconds, that passed between the user's last known interaction and the moment - // the session was closed. - secondsSinceLastCall?: number; - - // The interval that describes the part of the content that has been viewed in minutes. - segment?: string; - - // Program/Series Name. Program Name is required only if the show is part of a series. - show?: string; - - // The type of content for example, trailer or full episode. - showType?: string; - - // The radio station name on which the audio is played. - station?: string; - - // Format of the stream (HD, SD). - streamFormat?: string; - - // The type of the media stream. - streamType?: StreamType; - - // Sums the event duration (in seconds) for all events of type PLAY on the main content. - timePlayed?: number; - - // Describes the total amount of time spent by a user on a specific timed media asset, which includes time spent - // watching ads. - totalTimePlayed?: number; - - // Describes the sum of the unique intervals seen by a user on a timed media asset - i.e. the length playback - // intervals viewed multiple times are only counted once. - uniqueTimePlayed?: number; -} - -/** - * The type of the stream delivery. Available values per Stream Type: Audio: “song”, “podcast”, “audiobook”, - * “radio”; Video: “VoD”, “Live”, “Linear”, “UGC”, “DVoD”. Customers can provide custom values for this parameter. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/sessiondetails.schema.md#xdmcontenttype-known-values - */ -export enum ContentType { - // Video-on-demand - VOD = 'VOD', - - // Live streaming - LIVE = 'Live', - - // Linear playback of the media asset - LINEAR = 'Linear', - - // User-generated content - UGC = 'UGC', - - // Downloaded video-on-demand - DVOD = 'DVOD', - - // Radio show - RADIO = 'Radio', - - // Audio podcast - PODCAST = 'Podcast', - - // Audiobook - AUDIOBOOK = 'Audiobook', - - // Song - SONG = 'Song', -} - -/** - * The type of the media stream. - * - * https://github.com/adobe/xdm/blob/master/docs/reference/datatypes/sessiondetails.schema.md#xdmstreamtype - */ -export enum StreamType { - VIDEO = 'video', - AUDIO = 'audio', -} diff --git a/adobe-edge/src/api/details/barrel.ts b/adobe-edge/src/api/details/barrel.ts index 6c25e09a..1ee9941b 100644 --- a/adobe-edge/src/api/details/barrel.ts +++ b/adobe-edge/src/api/details/barrel.ts @@ -1,14 +1,3 @@ -export type { AdobeAdvertisingDetails } from './AdobeAdvertisingDetails'; -export type { AdobeAdvertisingPodDetails } from './AdobeAdvertisingPodDetails'; -export type { AdobeChapterDetails } from './AdobeChapterDetails'; -export type { AdobeCustomMetadataDetails } from './AdobeCustomMetadataDetails'; -export type { AdobeErrorDetails } from './AdobeErrorDetails'; -export { ErrorSource } from './AdobeErrorDetails'; -export type { AdobeImplementationDetails, AdobeEnvironment } from './AdobeImplementationDetails'; -export type { AdobeMediaDetails } from './AdobeMediaDetails'; -export type { AdobePlayerStateData } from './AdobePlayerStateData'; -export type { AdobeQoeDataDetails } from './AdobeQoeDataDetails'; -export { ContentType, StreamType } from './AdobeSessionDetails'; -export type { AdobeSessionDetails } from './AdobeSessionDetails'; export type { AdobeIdentityMap } from './AdobeIdentityMap'; export type { AdobeIdentityItem } from './AdobeIdentityItem'; +export type { AdobeMetadata } from './AdobeMetadata'; diff --git a/adobe-edge/src/internal/AdobeConnectorAdapter.ts b/adobe-edge/src/internal/AdobeConnectorAdapter.ts index 49612678..ff608685 100644 --- a/adobe-edge/src/internal/AdobeConnectorAdapter.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapter.ts @@ -1,15 +1,15 @@ -import { AdobeCustomMetadataDetails, AdobeErrorDetails, AdobeIdentityMap } from '@theoplayer/react-native-analytics-adobe-edge'; +import { AdobeIdentityMap, AdobeMetadata } from '@theoplayer/react-native-analytics-adobe-edge'; export interface AdobeConnectorAdapter { setDebug(debug: boolean): void; - updateMetadata(metadata: AdobeCustomMetadataDetails[]): void; + updateMetadata(metadata: AdobeMetadata): void; setCustomIdentityMap(customIdentityMap: AdobeIdentityMap): void; - setError(metadata: AdobeErrorDetails): void; + setError(errorId: string): void; - stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]): Promise; + stopAndStartNewSession(metadata?: AdobeMetadata): Promise; destroy(): Promise; } diff --git a/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts b/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts index c5de3337..d35b5b7f 100644 --- a/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapterNative.ts @@ -1,6 +1,6 @@ import type { NativeHandleType, THEOplayer } from 'react-native-theoplayer'; import { NativeModules } from 'react-native'; -import { AdobeCustomMetadataDetails, AdobeErrorDetails, AdobeIdentityMap } from '@theoplayer/react-native-analytics-adobe-edge'; +import { AdobeIdentityMap, AdobeMetadata } from '@theoplayer/react-native-analytics-adobe-edge'; import { AdobeConnectorAdapter } from './AdobeConnectorAdapter'; import { AdobeEdgeMobileConfig } from '../api/AdobeEdgeMobileConfig'; @@ -27,7 +27,7 @@ export class AdobeConnectorAdapterNative implements AdobeConnectorAdapter { } } - updateMetadata(metadata: AdobeCustomMetadataDetails[]) { + updateMetadata(metadata: AdobeMetadata) { try { NativeModules.AdobeEdgeModule.updateMetadata(this.nativeHandle || -1, metadata); } catch (error: unknown) { @@ -43,15 +43,15 @@ export class AdobeConnectorAdapterNative implements AdobeConnectorAdapter { } } - setError(errorDetails: AdobeErrorDetails) { + setError(errorId: string) { try { - NativeModules.AdobeEdgeModule.setError(this.nativeHandle || -1, errorDetails); + NativeModules.AdobeEdgeModule.setError(this.nativeHandle || -1, errorId); } catch (error: unknown) { console.error(TAG, `${ERROR_MSG}: ${error}`); } } - async stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]) { + async stopAndStartNewSession(metadata?: AdobeMetadata) { try { NativeModules.AdobeEdgeModule.stopAndStartNewSession(this.nativeHandle || -1, metadata ?? []); } catch (error: unknown) { diff --git a/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts b/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts index fd6171fc..1bb916d3 100644 --- a/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts +++ b/adobe-edge/src/internal/AdobeConnectorAdapterWeb.ts @@ -1,5 +1,5 @@ import type { THEOplayer } from 'react-native-theoplayer'; -import { AdobeCustomMetadataDetails, AdobeErrorDetails, AdobeIdentityMap } from '@theoplayer/react-native-analytics-adobe-edge'; +import { AdobeIdentityMap, AdobeMetadata } from '@theoplayer/react-native-analytics-adobe-edge'; import { AdobeConnectorAdapter } from './AdobeConnectorAdapter'; import { AdobeEdgeWebConfig } from '../api/AdobeEdgeWebConfig'; import { AdobeEdgeConnector } from './web/AdobeEdgeConnector'; @@ -16,7 +16,7 @@ export class AdobeConnectorAdapterWeb implements AdobeConnectorAdapter { this.connector.setDebug(debug); } - updateMetadata(metadata: AdobeCustomMetadataDetails[]): void { + updateMetadata(metadata: AdobeMetadata): void { this.connector.updateMetadata(metadata); } @@ -24,12 +24,12 @@ export class AdobeConnectorAdapterWeb implements AdobeConnectorAdapter { this.connector?.setCustomIdentityMap(customIdentityMap); } - setError(errorDetails: AdobeErrorDetails): void { - void this.connector.setError(errorDetails); + setError(errorId: string): void { + void this.connector.setError(errorId); } - async stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]): Promise { - await this.connector.stopAndStartNewSession(metadata); + async stopAndStartNewSession(metadata?: AdobeMetadata): Promise { + this.connector.stopAndStartNewSession(metadata); } async destroy(): Promise { diff --git a/adobe-edge/src/internal/web/AdobeEdgeConnector.ts b/adobe-edge/src/internal/web/AdobeEdgeConnector.ts index e4f8d849..f818396c 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeConnector.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeConnector.ts @@ -1,5 +1,5 @@ import { AdobeEdgeHandler } from './AdobeEdgeHandler'; -import { AdobeCustomMetadataDetails, AdobeEdgeWebConfig, AdobeErrorDetails, AdobeIdentityMap } from '@theoplayer/react-native-analytics-adobe-edge'; +import { AdobeEdgeWebConfig, AdobeIdentityMap, AdobeMetadata } from '@theoplayer/react-native-analytics-adobe-edge'; import { ChromelessPlayer } from 'theoplayer'; export class AdobeEdgeConnector { @@ -18,11 +18,11 @@ export class AdobeEdgeConnector { * * @param metadata object of key value pairs. */ - stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]) { + stopAndStartNewSession(metadata?: AdobeMetadata) { return this._handler.stopAndStartNewSession(metadata); } - updateMetadata(metadata: AdobeCustomMetadataDetails[]) { + updateMetadata(metadata: AdobeMetadata) { this._handler.updateMetadata(metadata); } @@ -34,8 +34,8 @@ export class AdobeEdgeConnector { this._handler.setDebug(debug); } - setError(errorDetails: AdobeErrorDetails) { - return this._handler.setError(errorDetails.name); + setError(errorId: string) { + return this._handler.setError(errorId); } destroy() { diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts index 68a942f8..73c1a905 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -1,14 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { AdobeCustomMetadataDetails, AdobeIdentityMap, ContentType } from '@theoplayer/react-native-analytics-adobe-edge'; -import { - idToInt, - isValidDuration, - sanitiseChapterId, - sanitiseConfig, - sanitiseContentLength, - sanitisePlayhead, - toAdobeCustomMetadataDetails, -} from './Utils'; +import { AdobeIdentityMap, AdobeMetadata } from '@theoplayer/react-native-analytics-adobe-edge'; +import { idToInt, isValidDuration, sanitiseChapterId, sanitiseConfig, sanitiseContentLength, sanitisePlayhead } from './Utils'; import { createInstance } from '@adobe/alloy'; import { AdobeEdgeWebConfig } from '../../api/AdobeEdgeWebConfig'; import { @@ -128,8 +120,8 @@ class AdobeEdgeHandler { this.setDebug(debugEnabled || false); } - updateMetadata(metadata: AdobeCustomMetadataDetails[]) { - this._customMetadata = { ...this._customMetadata, ...toAdobeCustomMetadataDetails(metadata) }; + updateMetadata(metadata: AdobeMetadata) { + this._customMetadata = { ...this._customMetadata, ...metadata }; } setCustomIdentityMap(customIdentityMap: AdobeIdentityMap) { @@ -140,7 +132,7 @@ class AdobeEdgeHandler { this.queueOrSendEvent(EventType.error, { PROP_ERROR_ID: errorId }); } - stopAndStartNewSession(metadata?: AdobeCustomMetadataDetails[]) { + stopAndStartNewSession(metadata?: AdobeMetadata) { this.maybeEndSession(); if (metadata) { this.updateMetadata(metadata); @@ -464,8 +456,8 @@ class AdobeEdgeHandler { return sanitiseContentLength(mediaLengthSec !== undefined ? mediaLengthSec : this._player.duration); } - private getContentType(): ContentType { - return this._player.duration === Infinity ? ContentType.LIVE : ContentType.VOD; + private getContentType(): string { + return this._player.duration === Infinity ? 'Live' : 'VOD'; } reset() { diff --git a/adobe-edge/src/internal/web/Utils.ts b/adobe-edge/src/internal/web/Utils.ts index 3a7fe026..32473ee1 100644 --- a/adobe-edge/src/internal/web/Utils.ts +++ b/adobe-edge/src/internal/web/Utils.ts @@ -1,4 +1,4 @@ -import type { AdobeCustomMetadataDetails, AdobeEdgeWebConfig } from '@theoplayer/react-native-analytics-adobe-edge'; +import type { AdobeEdgeWebConfig } from '@theoplayer/react-native-analytics-adobe-edge'; const PROP_NA = 'N/A'; @@ -35,16 +35,6 @@ export function isValidDuration(v: number | undefined): boolean { return v !== undefined && !Number.isNaN(v); } -export function toAdobeCustomMetadataDetails(details: AdobeCustomMetadataDetails[]): { [key: string]: string } { - const map: { [key: string]: string } = {}; - for (const item of details) { - if (item.name && item.value) { - map[item.name] = item.value; - } - } - return map; -} - export function sanitiseChapterId(id?: string): string { if (!id || id.trim().length === 0) { return PROP_NA; From 14aa6d7890ff35e3bff19d6e67f03db2061840cf Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 09:15:33 +0100 Subject: [PATCH 42/70] Pass NA instead of N/A --- .../com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt | 2 +- adobe-edge/src/internal/web/AdobeEdgeHandler.ts | 2 +- adobe-edge/src/internal/web/Utils.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt index 8ffaf369..aab95614 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -78,7 +78,7 @@ data class QueuedEvent( const val PROP_CURRENT_TIME = "currentTime" const val PROP_ERROR_ID = "errorId" -const val PROP_NA = "N/A" +const val PROP_NA = "NA" class AdobeEdgeHandler( private val player: Player, diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts index 73c1a905..a275b799 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -24,7 +24,7 @@ const TAG = 'AdobeConnector'; const PROP_PLAYHEAD = 'playhead'; const PROP_ERROR_ID = 'errorId'; -const PROP_NA = 'N/A'; +const PROP_NA = 'NA'; /** * Alloy globally stores clients by name. We are allowed create clients with the same config only once. diff --git a/adobe-edge/src/internal/web/Utils.ts b/adobe-edge/src/internal/web/Utils.ts index 32473ee1..0e855fc8 100644 --- a/adobe-edge/src/internal/web/Utils.ts +++ b/adobe-edge/src/internal/web/Utils.ts @@ -1,6 +1,6 @@ import type { AdobeEdgeWebConfig } from '@theoplayer/react-native-analytics-adobe-edge'; -const PROP_NA = 'N/A'; +const PROP_NA = 'NA'; /** * Sanitise the current media length. From 803911242468f2ef30a7849921fa92bb84acc5d5 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 09:44:40 +0100 Subject: [PATCH 43/70] Access player on main thread --- .../adobe/edge/AdobeEdgeHandler.kt | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt index aab95614..9e70e70f 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -36,7 +36,9 @@ import com.theoplayer.android.api.event.track.texttrack.list.TextTrackListEventT import com.theoplayer.android.api.player.Player import com.theoplayer.android.api.player.track.texttrack.TextTrackKind import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCue -import kotlin.collections.toMutableMap +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch typealias AddTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.AddTrackEvent typealias RemoveTextTrackEvent = com.theoplayer.android.api.event.track.texttrack.list.RemoveTrackEvent @@ -120,9 +122,9 @@ class AdobeEdgeHandler( private val onAdEnd = EventListener { handleAdEnd() } private val onAdSkip = EventListener { handleAdSkip() } + private val scope = CoroutineScope(Dispatchers.Main) private val tracker = Media.createTracker(trackerConfig) private val eventQueue = mutableListOf() - private fun logDebug(message: String) { if (loggingMode >= LoggingMode.DEBUG) { Log.d(TAG, message) @@ -156,15 +158,17 @@ class AdobeEdgeHandler( } fun stopAndStartNewSession(metadata: Map?) { - maybeEndSession() - metadata?.let { - updateMetadata(it) - } - maybeStartSession() - if (player.isPaused) { - handlePause() - } else { - handlePlaying() + scope.launch { + maybeEndSession() + metadata?.let { + updateMetadata(it) + } + maybeStartSession() + if (player.isPaused) { + handlePause() + } else { + handlePlaying() + } } } From 2cd05ff53650ecdc902d29ea4a0499fa155911f9 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 09:45:00 +0100 Subject: [PATCH 44/70] Avoid concurrent modification exception --- .../com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt index 9e70e70f..e99e992e 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -505,10 +505,10 @@ class AdobeEdgeHandler( sessionInProgress = true - // Post any queued events now that the session has started. if (eventQueue.isNotEmpty()) { - eventQueue.forEach { event -> sendEvent(event.type, event.info, event.metadata) } + val queuedEvents = eventQueue.toList() eventQueue.clear() + queuedEvents.forEach { event -> sendEvent(event.type, event.info, event.metadata) } } logDebug("maybeStartSession - STARTED") From 069f0b7d5db16e84d446a111b157fdc3600cdfb8 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 09:54:59 +0100 Subject: [PATCH 45/70] Clear custom metadata on reset --- .../com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt | 3 ++- adobe-edge/src/internal/web/AdobeEdgeHandler.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt index e99e992e..f539976a 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -94,7 +94,7 @@ class AdobeEdgeHandler( private var adPodPosition = 1 private var isPlayingAd = false - private var customMetadata = mutableMapOf() + private var customMetadata: MutableMap = mutableMapOf() private var currentChapter: TextTrackCue? = null private var loggingMode: LoggingMode = LoggingMode.ERROR private val onPlaying = EventListener { handlePlaying() } @@ -533,6 +533,7 @@ class AdobeEdgeHandler( isPlayingAd = false sessionInProgress = false currentChapter = null + customMetadata = mutableMapOf() } fun destroy() { diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts index a275b799..ecbabaad 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -467,6 +467,7 @@ class AdobeEdgeHandler { this._adPodPosition = 1; this._sessionInProgress = false; this._currentChapter = undefined; + this._customMetadata = {}; } destroy() { From 3f2a32f75be33d6b1f6edcd3b6b1093ab9488f4c Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 16 Jan 2026 10:09:12 +0100 Subject: [PATCH 46/70] Move and rename AdobeEdgeUtils --- .../AdobeEdgeUtils.swift} | 7 +++---- adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) rename adobe-edge/ios/{TheoplayerAdobeEdgeRCTAdobeEdgeUtils.swift => Connector/AdobeEdgeUtils.swift} (85%) diff --git a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeUtils.swift b/adobe-edge/ios/Connector/AdobeEdgeUtils.swift similarity index 85% rename from adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeUtils.swift rename to adobe-edge/ios/Connector/AdobeEdgeUtils.swift index 79dbd5eb..97f624e6 100644 --- a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeUtils.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeUtils.swift @@ -1,13 +1,12 @@ -// TheoplayerAdobeEdgeRCTAdobeEdgeUtils.swift - import Foundation import THEOplayerSDK +import AEPEdgeIdentity -class TheoplayerAdobeEdgeRCTAdobeEdgeUtils { +class AdobeEdgeUtils { class func toAdobeCustomMetadataDetails(_ array: [[String: Any]]) -> [String: String] { var result = [String: String]() for item in array { - let stringsItem = TheoplayerAdobeEdgeRCTAdobeEdgeUtils.toStringMap(item) + let stringsItem = AdobeEdgeUtils.toStringMap(item) if let name = stringsItem["name"], let value = stringsItem["value"] { result[name] = value } diff --git a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift index 0ada683a..5b60e238 100644 --- a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift +++ b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift @@ -27,8 +27,8 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { DispatchQueue.main.async { if let view = self.view(for: node), let player = view.player { - let trackerConfig: [String: String] = TheoplayerAdobeEdgeRCTAdobeEdgeUtils.toStringMap(config as? [String: Any] ?? [:]) let connector = AdobeEdgeConnector(player: player, trackerConfig: trackerConfig) + let trackerConfig: [String: String] = AdobeEdgeUtils.toStringMap(config as? [String: Any] ?? [:]) connector.setLoggingMode(self.debug ? .debug : .error) self.connectors[node] = connector self.log("added connector to view \(node)") @@ -55,7 +55,7 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { DispatchQueue.main.async { if let connector = self.connectors[node], let newMetadata = metadata as? [[String: Any]] { - connector.updateMetadata(TheoplayerAdobeEdgeRCTAdobeEdgeUtils.toAdobeCustomMetadataDetails(newMetadata)) + connector.updateMetadata(AdobeEdgeUtils.toAdobeCustomMetadataDetails(newMetadata)) } } } @@ -76,7 +76,7 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { DispatchQueue.main.async { if let connector = self.connectors[node], let newMetadata = customMetadataDetails as? [[String: Any]] { - connector.stopAndStartNewSession(TheoplayerAdobeEdgeRCTAdobeEdgeUtils.toAdobeCustomMetadataDetails(newMetadata)) + connector.stopAndStartNewSession(AdobeEdgeUtils.toAdobeCustomMetadataDetails(newMetadata)) } } } From 95b49cb32ffdc30e25bfae840a2bcc02f9ddffaa Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 16 Jan 2026 10:11:44 +0100 Subject: [PATCH 47/70] Bridge customIdentityMap on iOS --- .../TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift | 19 ++++++++++++++++--- adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m | 6 +++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift index 5b60e238..ec260108 100644 --- a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift +++ b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift @@ -19,16 +19,18 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { return false } - @objc(initialize:config:) - func initialize(_ node: NSNumber, config: NSDictionary) -> Void { + @objc(initialize:config:customIdentityMap:) + func initialize(_ node: NSNumber, config: NSDictionary, customIdentityMap: NSDictionary) -> Void { log("initialize triggered.") self.debug = config["debugEnabled"] as? Bool ?? false + DispatchQueue.main.async { if let view = self.view(for: node), let player = view.player { - let connector = AdobeEdgeConnector(player: player, trackerConfig: trackerConfig) let trackerConfig: [String: String] = AdobeEdgeUtils.toStringMap(config as? [String: Any] ?? [:]) + let customIdentityMap = customIdentityMap as? [String: Any] + let connector = AdobeEdgeConnector(player: player, trackerConfig: trackerConfig, customIdentityMap: customIdentityMap) connector.setLoggingMode(self.debug ? .debug : .error) self.connectors[node] = connector self.log("added connector to view \(node)") @@ -60,6 +62,17 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { } } + @objc(setCustomIdentityMap:customIdentityMap:) + func setCustomIdentityMap(_ node: NSNumber, customIdentityMap: NSDictionary) -> Void { + log("setCustomIdentityMap triggered.") + DispatchQueue.main.async { + if let connector = self.connectors[node], + let newCustomIdentityMap = customIdentityMap as? [String: Any] { + connector.setCustomIdentityMap(newCustomIdentityMap) + } + } + } + @objc(setError:errorDetails:) func setError(_ node: NSNumber, errorDetails: [String:Any]) -> Void { log("setError triggered.") diff --git a/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m b/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m index 87026e24..2e030849 100644 --- a/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m +++ b/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m @@ -8,7 +8,8 @@ @interface RCT_EXTERN_REMAP_MODULE(AdobeEdgeModule, THEOplayerAdobeEdgeRCTAdobeEdgeAPI, NSObject) RCT_EXTERN_METHOD(initialize:(nonnull NSNumber *)node - config:(NSDictionary)config) + config:(NSDictionary)config + customIdentityMap:(NSDictionary *)customIdentityMap) RCT_EXTERN_METHOD(setDebug:(nonnull NSNumber *)node debug:(BOOL)debug) @@ -16,6 +17,9 @@ @interface RCT_EXTERN_REMAP_MODULE(AdobeEdgeModule, THEOplayerAdobeEdgeRCTAdobeE RCT_EXTERN_METHOD(updateMetadata:(nonnull NSNumber *)node metadata:(NSArray *)metadata) +RCT_EXTERN_METHOD(setCustomIdentityMap:(nonnull NSNumber *)node + customIdentityMap:(NSDictionary *)customIdentityMap) + RCT_EXTERN_METHOD(setError:(nonnull NSNumber *)node errorDetails:(NSDictionary *)errorDetails) From b1cbdbcd44e632cfae9dd04b46b40a8ee8b25c39 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 16 Jan 2026 10:12:15 +0100 Subject: [PATCH 48/70] Forward identityMap to handler --- adobe-edge/ios/Connector/AdobeEdgeConnector.swift | 9 +++++++-- adobe-edge/ios/Connector/AdobeEdgeUtils.swift | 12 ++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeConnector.swift b/adobe-edge/ios/Connector/AdobeEdgeConnector.swift index 9c4644df..aeb4950e 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeConnector.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeConnector.swift @@ -6,17 +6,22 @@ import Foundation import THEOplayerSDK import UIKit import AEPServices +import AEPEdgeIdentity class AdobeEdgeConnector { private var handler: AdobeEdgeHandler - init(player: THEOplayer, trackerConfig: [String:String]) { - self.handler = AdobeEdgeHandler(player: player, trackerConfig: trackerConfig) + init(player: THEOplayer, trackerConfig: [String:String], customIdentityMap: [String:Any]? = nil) { + self.handler = AdobeEdgeHandler(player: player, trackerConfig: trackerConfig, customIdentityMap: customIdentityMap) } func updateMetadata(_ metadata: [String:String]) -> Void { self.handler.updateMetadata(metadata) } + func setCustomIdentityMap(_ customIdentityMap: [String:Any]) -> Void { + self.handler.setCustomIdentityMap(customIdentityMap) + } + func stopAndStartNewSession(_ metadata: [String:String]) -> Void { self.handler.stopAndStartNewSession(metadata) } diff --git a/adobe-edge/ios/Connector/AdobeEdgeUtils.swift b/adobe-edge/ios/Connector/AdobeEdgeUtils.swift index 97f624e6..ac5b21c1 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeUtils.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeUtils.swift @@ -1,3 +1,5 @@ +// AdobeEdgeUtils.swift + import Foundation import THEOplayerSDK import AEPEdgeIdentity @@ -30,5 +32,15 @@ class AdobeEdgeUtils { return result } + + class func toIdentityMap(_ map: [String: Any]) -> IdentityMap? { + guard let jsonData = try? JSONSerialization.data(withJSONObject: map) else { + return nil + } + guard let identityMap = try? JSONDecoder().decode(IdentityMap.self, from: jsonData) else { + return nil + } + return identityMap + } } From ffc29bd81c2dda027f7792610ce2ff95285d764f Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 16 Jan 2026 10:12:42 +0100 Subject: [PATCH 49/70] Process identityMap on iOS --- adobe-edge/ios/Connector/AdobeEdgeHandler.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index 12082e55..e2a7174d 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -8,7 +8,7 @@ import UIKit import AEPServices import AEPCore import AEPEdgeMedia -import AEPEdgeConsent +import AEPEdgeIdentity let CONTENT_PING_INTERVAL = 10.0 let AD_PING_INTERVAL = 1.0 @@ -65,9 +65,12 @@ class AdobeEdgeHandler { } } - init(player: THEOplayer, trackerConfig: [String:String]) { + init(player: THEOplayer, trackerConfig: [String:String], customIdentityMap: [String:Any]? = nil) { self.player = player self.trackerConfig = trackerConfig + if let identityMap = customIdentityMap { + self.setCustomIdentityMap(identityMap) + } self.addEventListeners() let environmentId = trackerConfig["environmentId"] ?? "MissingEnvironmentID" @@ -90,6 +93,12 @@ class AdobeEdgeHandler { self.customMetadata.merge(metadata) { (_, new) in new } } + func setCustomIdentityMap(_ customIdentityMap: [String:Any]) -> Void { + if let identityMap = AdobeEdgeUtils.toIdentityMap(customIdentityMap) { + Identity.updateIdentities(with: identityMap) + } + } + func setError(_ errorId: String) -> Void { self.queueOrSendEvent(event: AdobeEdgeEvent(type: .ERROR, info: [PROP_ERRORID: errorId])) } From 2800659cd434802bb178f47a2fe2477748438c20 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 10:10:03 +0100 Subject: [PATCH 50/70] Update e2e test --- apps/e2e/src/tests/AdobeEdge.spec.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/e2e/src/tests/AdobeEdge.spec.ts b/apps/e2e/src/tests/AdobeEdge.spec.ts index c9a7970a..6870830a 100644 --- a/apps/e2e/src/tests/AdobeEdge.spec.ts +++ b/apps/e2e/src/tests/AdobeEdge.spec.ts @@ -17,16 +17,28 @@ export default function (spec: TestScope) { debugEnabled: true, }, mobile: { - appId: 'launch-1234567890abcdef1234567890abcdef12', + environmentId: 'abcdef012345/abcdef012345/launch-abcdef012345-development', debugEnabled: true, }, }); }, () => { - connector.stopAndStartNewSession([ - { name: 'title', value: 'test' }, - { name: 'custom1', value: 'value1' }, - ]); + connector.stopAndStartNewSession({ + friendlyName: 'New Session', + }); + connector.updateMetadata({ + custom1: 'value1', + custom2: 'value2', + }); + connector.setCustomIdentityMap({ + EMAIL: [ + { + id: 'user@example.com', + authenticatedState: 'authenticated', + primary: false, + }, + ], + }); }, () => { connector.destroy(); From 426569fc83294151344c010ec6dde78e2b5d5762 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 10:14:14 +0100 Subject: [PATCH 51/70] Add error --- apps/e2e/src/tests/AdobeEdge.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/e2e/src/tests/AdobeEdge.spec.ts b/apps/e2e/src/tests/AdobeEdge.spec.ts index 6870830a..baa62c19 100644 --- a/apps/e2e/src/tests/AdobeEdge.spec.ts +++ b/apps/e2e/src/tests/AdobeEdge.spec.ts @@ -39,6 +39,7 @@ export default function (spec: TestScope) { }, ], }); + connector.setError('testError'); }, () => { connector.destroy(); From a8cdfa73398604aa233477774e97a27834de6c3c Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 10:25:55 +0100 Subject: [PATCH 52/70] Fix props --- adobe-edge/src/internal/web/AdobeEdgeHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts index ecbabaad..3d256896 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -129,7 +129,7 @@ class AdobeEdgeHandler { } setError(errorId: string) { - this.queueOrSendEvent(EventType.error, { PROP_ERROR_ID: errorId }); + this.queueOrSendEvent(EventType.error, { [PROP_ERROR_ID]: errorId }); } stopAndStartNewSession(metadata?: AdobeMetadata) { @@ -218,7 +218,7 @@ class AdobeEdgeHandler { } private queueOrSendEvent(type: EventType, info: EventInfo = {}, metadata: EventMetadata = {}) { - const extendedInfo = { ...info, playhead: sanitisePlayhead(this._player.currentTime) }; + const extendedInfo = { ...info, [PROP_PLAYHEAD]: sanitisePlayhead(this._player.currentTime) }; if (this._sessionInProgress) { this.sendEvent(type, extendedInfo, metadata); } else { From 6d095f9d7a2d650e332e1a54a5361ef08f719570 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 10:36:39 +0100 Subject: [PATCH 53/70] Fix exports --- adobe-edge/src/api/details/barrel.ts | 6 +++--- adobe-edge/src/index.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/adobe-edge/src/api/details/barrel.ts b/adobe-edge/src/api/details/barrel.ts index 1ee9941b..2bcc1fdf 100644 --- a/adobe-edge/src/api/details/barrel.ts +++ b/adobe-edge/src/api/details/barrel.ts @@ -1,3 +1,3 @@ -export type { AdobeIdentityMap } from './AdobeIdentityMap'; -export type { AdobeIdentityItem } from './AdobeIdentityItem'; -export type { AdobeMetadata } from './AdobeMetadata'; +export * from './AdobeIdentityMap'; +export * from './AdobeIdentityItem'; +export * from './AdobeMetadata'; diff --git a/adobe-edge/src/index.ts b/adobe-edge/src/index.ts index 02753948..76a743d4 100644 --- a/adobe-edge/src/index.ts +++ b/adobe-edge/src/index.ts @@ -1,7 +1,7 @@ export { AdobeConnector } from './api/AdobeConnector'; export type { AdobeEdgeConfig } from './api/AdobeEdgeConfig'; -export type { AdobeEdgeMobileConfig } from './api/AdobeEdgeMobileConfig'; -export type { AdobeEdgeWebConfig } from './api/AdobeEdgeWebConfig'; +export * from './api/AdobeEdgeMobileConfig'; +export * from './api/AdobeEdgeWebConfig'; export * from './api/details/barrel'; export { useAdobe } from './api/hooks/useAdobe'; export { sdkVersions } from './internal/version/Version'; From 84629d34f94becb9fa80ac785d0984d62349dc38 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 10:39:26 +0100 Subject: [PATCH 54/70] Add changeset --- .changeset/gold-fans-visit.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/gold-fans-visit.md diff --git a/.changeset/gold-fans-visit.md b/.changeset/gold-fans-visit.md new file mode 100644 index 00000000..c3524e9b --- /dev/null +++ b/.changeset/gold-fans-visit.md @@ -0,0 +1,5 @@ +--- +'@theoplayer/react-native-analytics-adobe-edge': major +--- + +Updated the connector to use the latest Adobe Experience Platform Mobile and Web SDKs. From 7f9eee9cb997cd2ba5b1f653a90235a671c77ad6 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 12:00:41 +0100 Subject: [PATCH 55/70] Fix playhead for live content --- .../theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt | 2 +- .../java/com/theoplayer/reactnative/adobe/edge/Utils.kt | 6 +++--- adobe-edge/src/internal/web/AdobeEdgeHandler.ts | 2 +- adobe-edge/src/internal/web/Utils.ts | 7 ++++--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt index f539976a..f9b11258 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -284,7 +284,7 @@ class AdobeEdgeHandler( logDebug("onTimeUpdate") queueOrSendEvent( EventType.PLAYHEAD_UPDATE, - mapOf(PROP_CURRENT_TIME to sanitisePlayhead(event.currentTime)) + mapOf(PROP_CURRENT_TIME to sanitisePlayhead(event.currentTime, player.duration)) ) } diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt index e943a804..1fb3401a 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt @@ -9,11 +9,11 @@ fun sanitiseContentLength(mediaLength: Double?): Int { return if (mediaLength == Double.POSITIVE_INFINITY) { 86400 } else mediaLength?.toInt() ?: 0 } -fun sanitisePlayhead(playhead: Double?): Int { - if (playhead == null) { +fun sanitisePlayhead(playhead: Double?, mediaLength: Double?): Int { + if (playhead == null || mediaLength == null) { return 0 } - if (playhead == Double.POSITIVE_INFINITY) { + if (mediaLength == Double.POSITIVE_INFINITY) { // If content is live, the playhead must be the current second of the day. val now = System.currentTimeMillis() return ((now / 1000) % 86400).toInt() diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts index 3d256896..3f321b1b 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -218,7 +218,7 @@ class AdobeEdgeHandler { } private queueOrSendEvent(type: EventType, info: EventInfo = {}, metadata: EventMetadata = {}) { - const extendedInfo = { ...info, [PROP_PLAYHEAD]: sanitisePlayhead(this._player.currentTime) }; + const extendedInfo = { ...info, [PROP_PLAYHEAD]: sanitisePlayhead(this._player.currentTime, this._player.duration) }; if (this._sessionInProgress) { this.sendEvent(type, extendedInfo, metadata); } else { diff --git a/adobe-edge/src/internal/web/Utils.ts b/adobe-edge/src/internal/web/Utils.ts index 0e855fc8..07c4e2a6 100644 --- a/adobe-edge/src/internal/web/Utils.ts +++ b/adobe-edge/src/internal/web/Utils.ts @@ -18,12 +18,13 @@ export function sanitiseContentLength(mediaLengthSec: number): number { * - If infinite (live stream), set it to the current second of the day. * * @param playheadInSec + * @param mediaLengthSec */ -export function sanitisePlayhead(playheadInSec?: number): number { - if (!playheadInSec || isNaN(playheadInSec)) { +export function sanitisePlayhead(playheadInSec?: number, mediaLengthSec?: number): number { + if (!playheadInSec || isNaN(playheadInSec) || !mediaLengthSec) { return 0; } - if (playheadInSec === Infinity) { + if (mediaLengthSec === Infinity) { // If content is live, the playhead must be the current second of the day. const date = new Date(); return date.getSeconds() + 60 * (date.getMinutes() + 60 * date.getHours()); From 78c8a4b15190b451be4247061edf1fd8207f2af8 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 16 Jan 2026 12:16:35 +0100 Subject: [PATCH 56/70] Adjust iOS bridge to updated API --- .../TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift | 18 +++++++++--------- adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift index ec260108..90727d95 100644 --- a/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift +++ b/adobe-edge/ios/TheoplayerAdobeEdgeRCTAdobeEdgeAPI.swift @@ -52,12 +52,12 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { } @objc(updateMetadata:metadata:) - func updateMetadata(_ node: NSNumber, metadata: [NSDictionary]) -> Void { + func updateMetadata(_ node: NSNumber, metadata: NSDictionary) -> Void { log("updateMetadata triggered.") DispatchQueue.main.async { if let connector = self.connectors[node], - let newMetadata = metadata as? [[String: Any]] { - connector.updateMetadata(AdobeEdgeUtils.toAdobeCustomMetadataDetails(newMetadata)) + let newMetadata = metadata as? [String: Any] { + connector.updateMetadata(AdobeEdgeUtils.toStringMap(newMetadata)) } } } @@ -73,23 +73,23 @@ class THEOplayerAdobeEdgeRCTAdobeEdgeAPI: NSObject, RCTBridgeModule { } } - @objc(setError:errorDetails:) - func setError(_ node: NSNumber, errorDetails: [String:Any]) -> Void { + @objc(setError:errorId:) + func setError(_ node: NSNumber, errorId: String) -> Void { log("setError triggered.") DispatchQueue.main.async { if let connector = self.connectors[node] { - connector.setError(errorDetails["name"] as? String ?? "NA") + connector.setError(errorId) } } } @objc(stopAndStartNewSession:customMetadataDetails:) - func stopAndStartNewSession(_ node: NSNumber, customMetadataDetails: [NSDictionary]) -> Void { + func stopAndStartNewSession(_ node: NSNumber, customMetadataDetails: NSDictionary) -> Void { log("stopAndStartNewSession triggered") DispatchQueue.main.async { if let connector = self.connectors[node], - let newMetadata = customMetadataDetails as? [[String: Any]] { - connector.stopAndStartNewSession(AdobeEdgeUtils.toAdobeCustomMetadataDetails(newMetadata)) + let newMetadata = customMetadataDetails as? [String: Any] { + connector.stopAndStartNewSession(AdobeEdgeUtils.toStringMap(newMetadata)) } } } diff --git a/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m b/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m index 2e030849..b9833171 100644 --- a/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m +++ b/adobe-edge/ios/TheoplayerAdobeEdgeRCTBridge.m @@ -15,16 +15,16 @@ @interface RCT_EXTERN_REMAP_MODULE(AdobeEdgeModule, THEOplayerAdobeEdgeRCTAdobeE debug:(BOOL)debug) RCT_EXTERN_METHOD(updateMetadata:(nonnull NSNumber *)node - metadata:(NSArray *)metadata) + metadata:(NSDictionary *)metadata) RCT_EXTERN_METHOD(setCustomIdentityMap:(nonnull NSNumber *)node customIdentityMap:(NSDictionary *)customIdentityMap) RCT_EXTERN_METHOD(setError:(nonnull NSNumber *)node - errorDetails:(NSDictionary *)errorDetails) + errorId:(NSString *)errorId) RCT_EXTERN_METHOD(stopAndStartNewSession:(nonnull NSNumber *)node - customMetadataDetails:(NSArray *)customMetadataDetails) + customMetadataDetails:(NSDictionary *)customMetadataDetails) RCT_EXTERN_METHOD(destroy:(nonnull NSNumber *)node) From 83f82909c0c423c9b74b3c27259246ab4dbf0037 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 16 Jan 2026 12:16:49 +0100 Subject: [PATCH 57/70] Drop unused method --- adobe-edge/ios/Connector/AdobeEdgeUtils.swift | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeUtils.swift b/adobe-edge/ios/Connector/AdobeEdgeUtils.swift index ac5b21c1..ea2d47c5 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeUtils.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeUtils.swift @@ -5,17 +5,6 @@ import THEOplayerSDK import AEPEdgeIdentity class AdobeEdgeUtils { - class func toAdobeCustomMetadataDetails(_ array: [[String: Any]]) -> [String: String] { - var result = [String: String]() - for item in array { - let stringsItem = AdobeEdgeUtils.toStringMap(item) - if let name = stringsItem["name"], let value = stringsItem["value"] { - result[name] = value - } - } - return result - } - class func toStringMap(_ map: [String: Any]) -> [String: String] { var result = [String: String]() for (key, value) in map { From f199e11e2a1f5901b0bff5e4496d51f14decea37 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 16 Jan 2026 12:17:38 +0100 Subject: [PATCH 58/70] Use merged metadata for name and id lookup on iOS --- adobe-edge/ios/Connector/AdobeEdgeHandler.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index e2a7174d..605b1b22 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -413,10 +413,12 @@ class AdobeEdgeHandler { return } - let metadata: [String: Any] = player.source?.metadata?.metadataKeys ?? [:] + var metadata: [String: Any] = player.source?.metadata?.metadataKeys ?? [:] + metadata.merge(self.customMetadata) { (_, new) in new } + if let mediaObject = Media.createMediaObjectWith( - name: metadata["title"] as? String ?? PROP_NA, - id: metadata["id"] as? String ?? PROP_NA, + name: metadata["friendlyName"] as? String ?? metadata["title"] as? String ?? PROP_NA, + id: metadata["name"] as? String ?? metadata["id"] as? String ?? PROP_NA, length: mediaLength, streamType: streamType, mediaType: MediaType.Video From 771f3a028bbe3d83f3aae2ca759119068ec02c1c Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 16 Jan 2026 12:17:56 +0100 Subject: [PATCH 59/70] Clear metadata on reset --- adobe-edge/ios/Connector/AdobeEdgeHandler.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index 605b1b22..5deffdf5 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -483,6 +483,7 @@ class AdobeEdgeHandler { self.sessionInProgress = false self.currentChapter = nil self.eventQueue.removeAll() + self.customMetadata.removeAll() } func destroy() -> Void { From bddb418502e4e71b65f61109c2b3c881439e18d7 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 16 Jan 2026 12:18:26 +0100 Subject: [PATCH 60/70] Use mediaLength to sanitise playhead --- adobe-edge/ios/Connector/AdobeEdgeHandler.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index 5deffdf5..555bed5b 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -244,10 +244,15 @@ class AdobeEdgeHandler { } private func handleTimeUpdate(event: TimeUpdateEvent) { - guard self.player != nil else { return } + guard let player = self.player else { return } //self.logDebug("onTimeUpdate") - self.queueOrSendEvent(event: AdobeEdgeEvent(type: .PLAYHEAD_UPDATE, info: [PROP_CURRENTTIME: self.sanitisePlayhead(event.currentTime)])) + self.queueOrSendEvent( + event: AdobeEdgeEvent( + type: .PLAYHEAD_UPDATE, + info: [PROP_CURRENTTIME: self.sanitisePlayhead(playhead: event.currentTime, mediaLength: player.duration)] + ) + ) } func handleWaiting(event: WaitingEvent) -> Void { @@ -502,12 +507,12 @@ class AdobeEdgeHandler { return MediaConstants.StreamType.LIVE } - private func sanitisePlayhead(_ playhead: Double?) -> Int { - guard let playhead = playhead else { + private func sanitisePlayhead(playhead: Double?, mediaLength: Double?) -> Int { + guard let playhead = playhead, let mediaLength = mediaLength else { return 0 } - if playhead == Double.infinity { + if mediaLength == Double.infinity { // If content is live, the playhead must be the current second of the day. let now = Date().timeIntervalSince1970 return Int(now.truncatingRemainder(dividingBy: 86400)) From 226e8811b9d881986f1f53013f303d08aa0bf432 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 16 Jan 2026 16:34:15 +0100 Subject: [PATCH 61/70] Fix adBreak position reporting. --- adobe-edge/ios/Connector/AdobeEdgeHandler.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index 555bed5b..e88b3a4b 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -331,10 +331,10 @@ class AdobeEdgeHandler { self.logDebug("onAdBreakBegin") self.isPlayingAd = true let currentAdBreakTimeOffset = event.ad?.timeOffset ?? 0 - let breakIndex = currentAdBreakTimeOffset < 0 ? -1 : (currentAdBreakTimeOffset == 0 ? 0 : self.adBreakPodIndex + 1) - let adBreakObject = Media.createAdBreakObjectWith(name: PROP_NA, position: breakIndex, startTime: currentAdBreakTimeOffset) + let position = currentAdBreakTimeOffset <= 0 ? 1 : self.adBreakPodIndex + 1 + let adBreakObject = Media.createAdBreakObjectWith(name: PROP_NA, position: position, startTime: currentAdBreakTimeOffset) self.queueOrSendEvent(event: AdobeEdgeEvent(type: .AD_BREAK_START, info: adBreakObject)) - if (breakIndex > self.adBreakPodIndex) { + if (position > self.adBreakPodIndex) { self.adBreakPodIndex += 1 } } From 0101bfffd1ea742b832e3f523645a7ca9cc1249d Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 15:00:22 +0100 Subject: [PATCH 62/70] adBreakObject position should be 1-based --- .../reactnative/adobe/edge/AdobeEdgeHandler.kt | 10 +++++----- adobe-edge/src/internal/web/AdobeEdgeHandler.ts | 15 +++++++-------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt index f9b11258..d8a7321f 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -393,20 +393,20 @@ class AdobeEdgeHandler( logDebug("onAdBreakBegin") isPlayingAd = true val currentAdBreakTimeOffset = event.adBreak.timeOffset - val index = when { - currentAdBreakTimeOffset == 0 -> 0 - currentAdBreakTimeOffset < 0 -> -1 + // The pod position should start at 1. + val position = when { + currentAdBreakTimeOffset <= 0 -> 1 else -> adBreakPodIndex + 1 } queueOrSendEvent( EventType.AD_BREAK_START, Media.createAdBreakObject( PROP_NA, - index, + position, currentAdBreakTimeOffset ) ) - if (index > adBreakPodIndex) { + if (position > adBreakPodIndex) { adBreakPodIndex++ } } diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts index 3f321b1b..660d510a 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -326,16 +326,15 @@ class AdobeEdgeHandler { private handleAdBreakBegin = (event: AdBreakEvent<'adbreakbegin'>) => { this.logDebug('onAdBreakBegin'); const currentAdBreakTimeOffset = event.adBreak.timeOffset; - let index: number; - if (currentAdBreakTimeOffset === 0) { - index = 0; - } else if (currentAdBreakTimeOffset < 0) { - index = -1; + let position: number; + // The pod position should start at 1. + if (currentAdBreakTimeOffset <= 0) { + position = 1; } else { - index = this._adBreakPodIndex + 1; + position = this._adBreakPodIndex + 1; } - this.queueOrSendEvent(EventType.adBreakStart, this._media?.createAdBreakObject(PROP_NA, index, currentAdBreakTimeOffset)); - if (index > this._adBreakPodIndex) { + this.queueOrSendEvent(EventType.adBreakStart, this._media?.createAdBreakObject(PROP_NA, position, currentAdBreakTimeOffset)); + if (position > this._adBreakPodIndex) { this._adBreakPodIndex++; } }; From 8b0e8477fd10a13c5b188e7c98a7ebc7f431d827 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 15:10:56 +0100 Subject: [PATCH 63/70] Update docs --- adobe-edge/README.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/adobe-edge/README.md b/adobe-edge/README.md index 8372b689..7fd2752b 100644 --- a/adobe-edge/README.md +++ b/adobe-edge/README.md @@ -36,6 +36,7 @@ const config = { datastreamId: 'abcde123-abcd-1234-abcd-abcde1234567', orgId: 'ADB3LETTERSANDNUMBERS@AdobeOrg', edgeBasePath: 'ee', + edgeDomain: 'my.domain.com', debugEnabled: true, }, mobile: { @@ -58,7 +59,7 @@ const customIdentityMap = { } const App = () => { - const [adobe, initAdobe] = useAdobe(config, customIdentityMap); + const [adobe, initAdobe] = useAdobe(config, /* optional */ customIdentityMap); const onPlayerReady = (player: THEOplayer) => { // Initialize Adobe connector @@ -87,3 +88,39 @@ const onUpdateMetadata = () => { adobe.current?.updateMetadata(metadata); }; ``` + +### Setting an custom identity map + +Besides passing a custom identity map during initialization, you can also set or update the identity map at any time: + +```typescript +import {AdobeIdentityMap} from "@theoplayer/react-native-analytics-adobe-edge"; + +const onUpdateIdentityMap = () => { + const identityMap: AdobeIdentityMap = { + CUSTOMER_ID: [ + { + id: 'customer-12345', + authenticatedState: 'authenticated', + primary: true, + }, + ], + }; + adobe.current?.setIdentityMap(identityMap); +}; +``` + +### Starting a new session during a live stream + +By default, the connector will start a new session when a new asset is loaded. However, during live streams, you might +want to start a new session +periodically when a new program starts. You can do this by calling `stopAndStartNewSession` with the new program's +metadata: + +```typescript +const onNewProgram = () => { + adobe.current?.stopAndStartNewSession({ + 'friendlyName': 'Evening News', + }); +}; +``` From df8a850d68f00cd605ba8666ae77f3936729207c Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 15:45:43 +0100 Subject: [PATCH 64/70] Use local time zone --- .../java/com/theoplayer/reactnative/adobe/edge/Utils.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt index 1fb3401a..1831c36a 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/Utils.kt @@ -4,6 +4,7 @@ import com.adobe.marketing.mobile.edge.identity.AuthenticatedState import com.adobe.marketing.mobile.edge.identity.IdentityItem import com.facebook.react.bridge.ReadableMap import com.adobe.marketing.mobile.edge.identity.IdentityMap +import java.util.Calendar fun sanitiseContentLength(mediaLength: Double?): Int { return if (mediaLength == Double.POSITIVE_INFINITY) { 86400 } else mediaLength?.toInt() ?: 0 @@ -15,8 +16,10 @@ fun sanitisePlayhead(playhead: Double?, mediaLength: Double?): Int { } if (mediaLength == Double.POSITIVE_INFINITY) { // If content is live, the playhead must be the current second of the day. - val now = System.currentTimeMillis() - return ((now / 1000) % 86400).toInt() + val calendar = Calendar.getInstance() + return calendar.get(Calendar.SECOND) + + 60 * (calendar.get(Calendar.MINUTE) + + 60 * calendar.get(Calendar.HOUR_OF_DAY)) } return playhead.toInt() } From 1755bcfb22e206d2801aa50fc45b62d95048ca92 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 16 Jan 2026 16:43:05 +0100 Subject: [PATCH 65/70] Use local time for live playhead calculation --- adobe-edge/ios/Connector/AdobeEdgeHandler.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index e88b3a4b..af7765e1 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -514,8 +514,14 @@ class AdobeEdgeHandler { if mediaLength == Double.infinity { // If content is live, the playhead must be the current second of the day. - let now = Date().timeIntervalSince1970 - return Int(now.truncatingRemainder(dividingBy: 86400)) + let now = Date() + let calendar = Calendar.current + let seconds = + calendar.component(.hour, from: now) * 3600 + + calendar.component(.minute, from: now) * 60 + + calendar.component(.second, from: now) + + return seconds } return Int(playhead) From ff83624f6699ad4f4d824b18c060f3d8d858d88c Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Fri, 16 Jan 2026 17:23:54 +0100 Subject: [PATCH 66/70] Remove alle references to Consent module --- adobe-edge/ios/Connector/AdobeEdgeHandler.swift | 2 -- adobe-edge/react-native-theoplayer-adobe-edge.podspec | 1 - 2 files changed, 3 deletions(-) diff --git a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift index af7765e1..6c9aa05c 100644 --- a/adobe-edge/ios/Connector/AdobeEdgeHandler.swift +++ b/adobe-edge/ios/Connector/AdobeEdgeHandler.swift @@ -77,8 +77,6 @@ class AdobeEdgeHandler { MobileCore.setLogLevel(.error) MobileCore.initialize(appId: environmentId) { self.logDebug("MobileCore successfully initialized with App ID: \(environmentId)") - // let collectConsent = ["collect": ["val": "y"]] - // Consent.update(with: ["consents": collectConsent]) } self.logDebug("Connector Initialized.") diff --git a/adobe-edge/react-native-theoplayer-adobe-edge.podspec b/adobe-edge/react-native-theoplayer-adobe-edge.podspec index 9a6329ea..d5f087ff 100644 --- a/adobe-edge/react-native-theoplayer-adobe-edge.podspec +++ b/adobe-edge/react-native-theoplayer-adobe-edge.podspec @@ -21,7 +21,6 @@ Pod::Spec.new do |s| s.dependency 'AEPEdge', '~> 5.0' s.dependency 'AEPEdgeIdentity', '~> 5.0' s.dependency 'AEPEdgeMedia', '~> 5.0' - s.dependency 'AEPEdgeConsent', '~> 5.0' # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. From 049454f94bc3f8fb3dec44703e9223db7407bde5 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 17:29:30 +0100 Subject: [PATCH 67/70] Fix metadata conversion --- .../com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt index d8a7321f..dc4b72dd 100644 --- a/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt +++ b/adobe-edge/android/src/main/java/com/theoplayer/reactnative/adobe/edge/AdobeEdgeHandler.kt @@ -491,7 +491,8 @@ class AdobeEdgeHandler( } // Allow overriding metadata with custom metadata set via updateMetadata(). - val mergedMetadata = (player.source?.metadata?.data?.mapValues { it.value as String } ?: emptyMap()) + customMetadata + val mergedMetadata = (player.source?.metadata?.data?.mapValues { it.value.toString() } + ?: emptyMap()) + customMetadata tracker.trackSessionStart( Media.createMediaObject( mergedMetadata["friendlyName"] ?: mergedMetadata["title"] ?: PROP_NA, From f75e9c0ca40d2baf6890b13c2adf99c18ed59e86 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 17:53:12 +0100 Subject: [PATCH 68/70] Fix qoe object --- adobe-edge/src/internal/web/AdobeEdgeHandler.ts | 4 ++-- adobe-edge/src/internal/web/Utils.ts | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts index 660d510a..b083c394 100644 --- a/adobe-edge/src/internal/web/AdobeEdgeHandler.ts +++ b/adobe-edge/src/internal/web/AdobeEdgeHandler.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { AdobeIdentityMap, AdobeMetadata } from '@theoplayer/react-native-analytics-adobe-edge'; -import { idToInt, isValidDuration, sanitiseChapterId, sanitiseConfig, sanitiseContentLength, sanitisePlayhead } from './Utils'; +import { idToInt, isValidDuration, sanitiseChapterId, sanitiseConfig, sanitiseContentLength, sanitiseNumber, sanitisePlayhead } from './Utils'; import { createInstance } from '@adobe/alloy'; import { AdobeEdgeWebConfig } from '../../api/AdobeEdgeWebConfig'; import { @@ -269,7 +269,7 @@ class AdobeEdgeHandler { private handleQualityChanged = (event: QualityEvent<'activequalitychanged'>) => { const quality = event.quality as VideoQuality; void this.queueOrSendEvent(EventType.bitrateChange, { - qoeDataDetails: this._media?.createQoEObject(quality?.bandwidth ?? 0, 0, quality?.frameRate ?? 0, 0), + qoeDataDetails: this._media?.createQoEObject(sanitiseNumber(quality?.bandwidth), 0, sanitiseNumber(quality?.frameRate), 0), }); }; diff --git a/adobe-edge/src/internal/web/Utils.ts b/adobe-edge/src/internal/web/Utils.ts index 07c4e2a6..675111b1 100644 --- a/adobe-edge/src/internal/web/Utils.ts +++ b/adobe-edge/src/internal/web/Utils.ts @@ -32,6 +32,13 @@ export function sanitisePlayhead(playheadInSec?: number, mediaLengthSec?: number return Math.trunc(playheadInSec); } +export function sanitiseNumber(v?: number): number { + if (v === undefined || v === null || Number.isNaN(v)) { + return 0; + } + return v; +} + export function isValidDuration(v: number | undefined): boolean { return v !== undefined && !Number.isNaN(v); } From a7d0399c8eea41af93111b2cbbe5f3358f970703 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 18:08:13 +0100 Subject: [PATCH 69/70] Fix e2e gradle deps --- apps/e2e/android/app/build.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/e2e/android/app/build.gradle b/apps/e2e/android/app/build.gradle index ac364dbb..b7ba82f1 100644 --- a/apps/e2e/android/app/build.gradle +++ b/apps/e2e/android/app/build.gradle @@ -112,12 +112,12 @@ def safeExtGet(prop, fallback) { } dependencies { -// implementation project(path: ':react-native-theoplayer-analytics-adobe') - implementation project(path: ':react-native-theoplayer-analytics-adobe-edge') -// implementation project(path: ':react-native-theoplayer-analytics-comscore') -// implementation project(path: ':react-native-theoplayer-analytics-conviva') -// implementation project(path: ':react-native-theoplayer-analytics-nielsen') -// implementation project(path: ':react-native-theoplayer-yospace') + implementation project(path: ':react-native-theoplayer-analytics-adobe') + implementation project(path: ':react-native-theoplayer-analytics-adobe-edge') + implementation project(path: ':react-native-theoplayer-analytics-comscore') + implementation project(path: ':react-native-theoplayer-analytics-conviva') + implementation project(path: ':react-native-theoplayer-analytics-nielsen') + implementation project(path: ':react-native-theoplayer-yospace') // implementation project(path: ':react-native-theoplayer-analytics-adscript') // implementation project(path: ':react-native-theoplayer-analytics-gemius') From 5dd747eb56abe938440727aa4a122ea0323eccba Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 16 Jan 2026 19:17:06 +0100 Subject: [PATCH 70/70] Disable e2e test on iOS --- apps/e2e/ios/Podfile | 2 -- apps/e2e/src/tests/index.ts | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/e2e/ios/Podfile b/apps/e2e/ios/Podfile index aaa4fd6e..0c011f07 100755 --- a/apps/e2e/ios/Podfile +++ b/apps/e2e/ios/Podfile @@ -32,7 +32,6 @@ target 'ReactNativeTHEOplayer' do pod 'react-native-theoplayer-conviva', :path => '../../../conviva' pod 'react-native-theoplayer-nielsen', :path => '../../../nielsen' pod 'react-native-theoplayer-adobe', :path => '../../../adobe' - pod 'react-native-theoplayer-adobe-edge', :path => '../../../adobe-edge' google_cast_redirect @@ -49,7 +48,6 @@ target 'ReactNativeTHEOplayer-tvOS' do pod 'react-native-theoplayer-conviva', :path => '../../../conviva' pod 'react-native-theoplayer-nielsen', :path => '../../../nielsen' pod 'react-native-theoplayer-adobe', :path => '../../../adobe' - pod 'react-native-theoplayer-adobe-edge', :path => '../../../adobe-edge' end diff --git a/apps/e2e/src/tests/index.ts b/apps/e2e/src/tests/index.ts index ea2a6c07..56a5931e 100644 --- a/apps/e2e/src/tests/index.ts +++ b/apps/e2e/src/tests/index.ts @@ -7,7 +7,9 @@ import Nielsen from './Nielsen.spec'; import Yospace from './Yospace.spec'; import { Platform } from 'react-native'; -const tests = [Adobe, AdobeNative, AdobeEdge, Comscore, Conviva, Nielsen]; +const tests = Platform.OS === 'ios' ? [Adobe, AdobeNative, Comscore, Conviva, Nielsen] : + [Adobe, AdobeNative, AdobeEdge, Comscore, Conviva, Nielsen]; + if (Platform.OS === 'android') { tests.push(Yospace); }