From 477e621ac454f981a84acb4e0b79d54ba9c72301 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 6 Feb 2026 12:24:27 +0100 Subject: [PATCH 01/14] Update android dependency --- bitmovin/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitmovin/android/build.gradle b/bitmovin/android/build.gradle index 67374084..f068f4b4 100644 --- a/bitmovin/android/build.gradle +++ b/bitmovin/android/build.gradle @@ -78,7 +78,7 @@ rootProject.allprojects { // The Bitmovin connector requires at least THEOplayer SDK v10.0.0. def theoplayer_sdk_version = safeExtGet('THEOplayer_sdk', '[10.0.0, 11.0.0)') def kotlin_version = safeExtGet("THEOplayerBitmovin_kotlinVersion", "2.2.10") -def bitmovin_version = safeExtGet("THEOplayerBitmovin_bitmovinVersion", "3.23.0-alpha2") +def bitmovin_version = safeExtGet("THEOplayerBitmovin_bitmovinVersion", "3.23.0-alpha3") // By default, take the connector version that aligns with the THEOplayer SDK version. def theoplayer_bitmovin_connector_version = safeExtGet('THEOplayerBitmovin_connectorVersion', theoplayer_sdk_version) From 5e73999bec8ae6492adb20c5930fca88476d0c88 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 6 Feb 2026 12:24:53 +0100 Subject: [PATCH 02/14] Add onSourceChange --- apps/e2e/src/components/menu/sources.json | 9 +- .../reactnative/bitmovin/BitmovinAdapter.kt | 37 +++++++- .../reactnative/bitmovin/BitmovinHandler.kt | 89 +++++++++++++++++++ .../bitmovin/ReactTHEOplayerBitmovinModule.kt | 43 +++++---- 4 files changed, 154 insertions(+), 24 deletions(-) create mode 100644 bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinHandler.kt diff --git a/apps/e2e/src/components/menu/sources.json b/apps/e2e/src/components/menu/sources.json index c0448af1..a1bf7f5f 100644 --- a/apps/e2e/src/components/menu/sources.json +++ b/apps/e2e/src/components/menu/sources.json @@ -15,7 +15,14 @@ "album": "React-Native THEOplayer demos", "mediaUri": "https://theoplayer.com", "displayIconUri": "https://cdn.theoplayer.com/video/sintel_old/poster.jpg", - "artist": "THEOplayer" + "artist": "THEOplayer", + "videoId": "sintel-video-id", + "path": "/videos/sintel", + "isLive": false, + "customData": { + "customData1": "value1", + "customData2": "value2" + } }, "textTracks": [{ "kind": "chapters", diff --git a/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinAdapter.kt b/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinAdapter.kt index 144049b9..25c0065c 100644 --- a/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinAdapter.kt +++ b/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinAdapter.kt @@ -1,5 +1,6 @@ package com.theoplayer.reactnative.bitmovin +import android.util.Log import com.bitmovin.analytics.api.AnalyticsConfig import com.bitmovin.analytics.api.CustomData import com.bitmovin.analytics.api.DefaultMetadata @@ -7,6 +8,7 @@ import com.bitmovin.analytics.api.LogLevel import com.bitmovin.analytics.api.RetryPolicy import com.bitmovin.analytics.api.SourceMetadata import com.facebook.react.bridge.ReadableMap +import org.json.JSONObject private const val PROP_LICENSE_KEY = "licenseKey" private const val PROP_AD_TRACKING_DISABLED = "adTrackingDisabled" @@ -25,6 +27,8 @@ private const val PROP_CUSTOM_USER_ID = "customUserId" object BitmovinAdapter { + private const val TAG = "BitmovinAdapter" + /** * Create an AnalyticsConfig object. * @@ -94,7 +98,8 @@ object BitmovinAdapter { * * https://developer.bitmovin.com/playback/docs/setup-analytics-android-v3 */ - fun parseDefaultMetadata(metadata: ReadableMap): DefaultMetadata { + fun parseDefaultMetadata(metadata: ReadableMap?): DefaultMetadata? { + if (metadata == null) return null return DefaultMetadata.Builder() .apply { if (metadata.hasKey(PROP_CDN_PROVIDER)) { @@ -115,11 +120,35 @@ object BitmovinAdapter { * * https://developer.bitmovin.com/playback/docs/configuration-analytics */ - fun parseCustomData(data: ReadableMap?): CustomData { - return CustomData.Builder().apply { + fun parseCustomData(data: ReadableMap?, buildUpon: CustomData.Builder? = null): CustomData { + return buildCustomData({ key -> data?.getString(key) }, buildUpon) + } + + /** + * Create a CustomData object. + * + * https://developer.bitmovin.com/playback/docs/configuration-analytics + */ + fun parseCustomDataFromJSON( + json: JSONObject?, + buildUpon: CustomData.Builder? = null + ): CustomData { + return buildCustomData({ key -> json?.opt(key) }, buildUpon) + } + + fun buildCustomData( + getValue: (String) -> Any?, + buildUpon: CustomData.Builder? = null + ): CustomData { + return (buildUpon ?: CustomData.Builder()).apply { for (i in 1..50) { val method = CustomData.Builder::class.java.getMethod("setCustomData$i", String::class.java) - method.invoke(this, data?.getString("${PROP_CUSTOM_DATA}$i")) + val value = getValue("${PROP_CUSTOM_DATA}$i") + if (value is String) { + method.invoke(this, value) + } else if (value != null) { + Log.w(TAG, "CustomData${i} is not a String: $value") + } } }.build() } diff --git a/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinHandler.kt b/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinHandler.kt new file mode 100644 index 00000000..73adb305 --- /dev/null +++ b/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinHandler.kt @@ -0,0 +1,89 @@ +package com.theoplayer.reactnative.bitmovin + +import android.content.Context +import android.util.Log +import com.bitmovin.analytics.api.AnalyticsConfig +import com.bitmovin.analytics.api.CustomData +import com.bitmovin.analytics.api.DefaultMetadata +import com.bitmovin.analytics.api.SourceMetadata +import com.bitmovin.analytics.theoplayer.api.ITHEOplayerCollector +import com.theoplayer.android.api.event.EventListener +import com.theoplayer.android.api.event.player.PlayerEventTypes +import com.theoplayer.android.api.event.player.SourceChangeEvent +import com.theoplayer.android.api.player.Player +import com.theoplayer.android.api.source.metadata.MetadataDescription +import com.theoplayer.reactnative.bitmovin.BitmovinAdapter.parseCustomDataFromJSON +import org.json.JSONObject +import kotlin.apply + +class BitmovinHandler( + context: Context, + private val player: Player, + config: AnalyticsConfig, + defaultMetadata: DefaultMetadata? +) { + + private val collector: ITHEOplayerCollector = ITHEOplayerCollector.create(context, config) + private val onSourceChange = EventListener { handleSourceChange() } + var currentSourceMetadata: SourceMetadata? = null + + init { + player.addEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) + defaultMetadata?.let { + collector.defaultMetadata = it + } + } + + var customData: CustomData + get() = collector.customData + set(value) { + collector.customData = value + } + + fun programChange(sourceMetadata: SourceMetadata) { + collector.programChange(sourceMetadata) + } + + fun sendCustomDataEvent(customData: CustomData) { + collector.sendCustomDataEvent(customData) + } + + private fun handleSourceChange() { + Log.d("BitmovinConnector", "Handling source change event") + // Detach player before setting new SourceMetadata. + collector.detachPlayer() + /** + * Merge the current source metadata with the new metadata from the playe source, if available. + * Prioritize the player source's metadata. + */ + mergeSourceMetadata(currentSourceMetadata, player.source?.metadata)?.let { + collector.sourceMetadata = it + } + currentSourceMetadata = null + collector.attachPlayer(player) + } + + private fun mergeSourceMetadata( + sourceMetadata: SourceMetadata?, metadata: MetadataDescription? + ): SourceMetadata? { + if (metadata == null) return sourceMetadata + return SourceMetadata.Builder().apply { + setTitle(metadata.get("title") ?: sourceMetadata?.title) + setVideoId(metadata.get("videoId") ?: sourceMetadata?.videoId) + setPath(metadata.get("path") ?: sourceMetadata?.path) + setCdnProvider(metadata.get("cdnProvider") ?: sourceMetadata?.cdnProvider) + setIsLive(metadata.get("isLive") ?: sourceMetadata?.isLive) + val customData = (metadata.get("customData") as? JSONObject?)?.let { + parseCustomDataFromJSON( + json = it, + buildUpon = sourceMetadata?.customData?.buildUpon()) + } ?: sourceMetadata?.customData + customData?.let { setCustomData(it) } + }.build() + } + + fun destroy() { + collector.detachPlayer() + player.removeEventListener(PlayerEventTypes.SOURCECHANGE, onSourceChange) + } +} diff --git a/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/ReactTHEOplayerBitmovinModule.kt b/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/ReactTHEOplayerBitmovinModule.kt index 95d392ce..ce9abe63 100644 --- a/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/ReactTHEOplayerBitmovinModule.kt +++ b/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/ReactTHEOplayerBitmovinModule.kt @@ -1,17 +1,17 @@ +@file:Suppress("unused") package com.theoplayer.reactnative.bitmovin -import com.bitmovin.analytics.theoplayer.api.ITHEOplayerCollector import com.facebook.react.bridge.* import com.theoplayer.ReactTHEOplayerView import com.theoplayer.util.ViewResolver private const val TAG = "BitmovinModule" -class ReactTHEOplayerBitmovinModule(context: ReactApplicationContext) : +class ReactTHEOplayerBitmovinModule(val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { private val viewResolver: ViewResolver = ViewResolver(context) - private var bitmovinConnectors: HashMap = HashMap() + private var bitmovinConnectors: HashMap = HashMap() override fun getName(): String { return TAG @@ -22,44 +22,49 @@ class ReactTHEOplayerBitmovinModule(context: ReactApplicationContext) : viewResolver.resolveViewByTag(tag) { view: ReactTHEOplayerView? -> view?.player?.let { player -> // Optionally destroy any existing connector for this player. - bitmovinConnectors[tag]?.detachPlayer() - val connector = ITHEOplayerCollector.create( - view.context, - BitmovinAdapter.parseConfig(config) + bitmovinConnectors[tag]?.destroy() + bitmovinConnectors[tag] = BitmovinHandler( + context = view.context, + player = player, + config = BitmovinAdapter.parseConfig(config), + defaultMetadata = BitmovinAdapter.parseDefaultMetadata(defaultMetadata) ) - defaultMetadata?.let { - connector.defaultMetadata = BitmovinAdapter.parseDefaultMetadata(it) - } - connector.attachPlayer(player) - bitmovinConnectors[tag] = connector } } } @ReactMethod fun updateSourceMetadata(tag: Int, metadata: ReadableMap) { - bitmovinConnectors[tag]?.sourceMetadata = BitmovinAdapter.parseSourceMetadata(metadata) + context.runOnUiQueueThread { + bitmovinConnectors[tag]?.currentSourceMetadata = BitmovinAdapter.parseSourceMetadata(metadata) + } } @ReactMethod fun updateCustomData(tag: Int, customData: ReadableMap) { - // TODO: resolve threading issue first - //bitmovinConnectors[tag]?.customData = BitmovinAdapter.parseCustomData(customData) + context.runOnUiQueueThread { + bitmovinConnectors[tag]?.customData = BitmovinAdapter.parseCustomData(customData) + } } @ReactMethod fun programChange(tag: Int, sourceMetadata: ReadableMap) { - // NYI - //bitmovinConnectors[tag]?.programChange(BitmovinAdapter.parseSourceMetadata(sourceMetadata)) + context.runOnUiQueueThread { + bitmovinConnectors[tag]?.programChange(BitmovinAdapter.parseSourceMetadata(sourceMetadata)) + } } @ReactMethod fun sendCustomDataEvent(tag: Int, customData: ReadableMap) { - bitmovinConnectors[tag]?.sendCustomDataEvent(BitmovinAdapter.parseCustomData(customData)) + context.runOnUiQueueThread { + bitmovinConnectors[tag]?.sendCustomDataEvent(BitmovinAdapter.parseCustomData(customData)) + } } @ReactMethod fun destroy(tag: Int) { - bitmovinConnectors[tag]?.detachPlayer() + context.runOnUiQueueThread { + bitmovinConnectors[tag]?.destroy() + } } } From f19fa167f382d6a5b332520da42e453eb555556b Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 6 Feb 2026 12:25:09 +0100 Subject: [PATCH 03/14] Update docs --- bitmovin/README.md | 4 ++-- bitmovin/src/api/CustomData.ts | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/bitmovin/README.md b/bitmovin/README.md index c14a6b9f..737c4396 100644 --- a/bitmovin/README.md +++ b/bitmovin/README.md @@ -38,8 +38,8 @@ const defaultMetadata: DefaultMetadata = { cdnProvider: 'akamai', customUserId: 'custom-user-id-1234', customData: { - customData0: 'value0', - customData1: 'value1' + customData1: 'value1', + customData2: 'value2' } }; diff --git a/bitmovin/src/api/CustomData.ts b/bitmovin/src/api/CustomData.ts index b0918f35..7e869ace 100644 --- a/bitmovin/src/api/CustomData.ts +++ b/bitmovin/src/api/CustomData.ts @@ -11,6 +11,14 @@ * Additional customData fields can be activated, up to 50 in total. These additional fields may incur a cost depending on the type of data stored. * Please reach out to us via the support form in the dashboard if you'd like more information. * + * @example + * ```ts + * const customData: CustomData = { + * customData1: 'customData1 value', + * customData2: 'customData2 value', + * }; + * ``` + * * {@link https://developer.bitmovin.com/playback/docs/configuration-analytics} */ export interface CustomData { From 76c1e7a9d533d89c948212d4b25de8301d66f3cb Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 6 Feb 2026 15:05:59 +0100 Subject: [PATCH 04/14] Add web collector dependency --- bitmovin/package.json | 3 +++ package-lock.json | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/bitmovin/package.json b/bitmovin/package.json index aa4de2d5..feb520cf 100644 --- a/bitmovin/package.json +++ b/bitmovin/package.json @@ -80,5 +80,8 @@ } ] ] + }, + "dependencies": { + "bitmovin-analytics": "^2.50.0-beta.1" } } diff --git a/package-lock.json b/package-lock.json index 04f60d61..38da5b00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -134,8 +134,11 @@ }, "bitmovin": { "name": "@theoplayer/react-native-analytics-bitmovin", - "version": "1.0.0-alpha.9", - "license": "SEE LICENSE AT https://www.theoplayer.com/terms", + "version": "1.0.0-alpha.10", + "license": "BSD-3-Clause-Clear", + "dependencies": { + "bitmovin-analytics": "^2.50.0-beta.1" + }, "peerDependencies": { "react": "*", "react-native": "*", @@ -166,7 +169,7 @@ }, "conviva": { "name": "@theoplayer/react-native-analytics-conviva", - "version": "1.11.1", + "version": "1.12.0", "license": "SEE LICENSE AT https://www.theoplayer.com/terms", "dependencies": { "@convivainc/conviva-js-coresdk": "^4.8.0", @@ -5297,6 +5300,12 @@ "node": ">=4" } }, + "node_modules/bitmovin-analytics": { + "version": "2.50.0-beta.1", + "resolved": "https://registry.npmjs.org/bitmovin-analytics/-/bitmovin-analytics-2.50.0-beta.1.tgz", + "integrity": "sha512-l7p/UNlFAYC+xfO/DBXhNXU+ucqiGATjXGFw9VHEzpLwX2vFXflX3aHyFCZTr6pdkeyrP929fTA3bmCkQPWXxw==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.2", "license": "MIT", From 9f0f6b6a4be588adf47420e3e4d46576aa866b90 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 6 Feb 2026 15:06:27 +0100 Subject: [PATCH 05/14] Extend api with web properties --- bitmovin/src/api/AnalyticsConfig.ts | 72 +++++++++++++++++++++++++++++ bitmovin/src/api/CustomData.ts | 10 ++++ 2 files changed, 82 insertions(+) diff --git a/bitmovin/src/api/AnalyticsConfig.ts b/bitmovin/src/api/AnalyticsConfig.ts index 16d97573..2c77315f 100644 --- a/bitmovin/src/api/AnalyticsConfig.ts +++ b/bitmovin/src/api/AnalyticsConfig.ts @@ -64,4 +64,76 @@ export interface AnalyticsConfig { * Initial source metadata to be associated with the playback session. */ sourceMetadata?: SourceMetadata; + + /** + * The device type. + * {@link https://developer.bitmovin.com/playback/docs/analytics-api-fields} + * + * @platform web + */ + deviceType?: string; + + /** + * The device class. + * {@link https://developer.bitmovin.com/playback/docs/analytics-api-fields} + * + * @platform web + */ + deviceClass?: 'Console' | 'Desktop' | 'Other' | 'Phone' | 'STB' | 'Tablet' | 'TV' | 'Wearable'; + + /** + * Player name. + * {@link https://developer.bitmovin.com/playback/docs/analytics-api-fields} + * + * @platform web + */ + player?: string; + + /** + * UUID that is persisted across play sessions + * {@link https://developer.bitmovin.com/playback/docs/analytics-api-fields} + * + * @platform web + */ + userId?: string; + + /** + * Enable or disable the analytics collector. + * + * @platform web + */ + enabled?: boolean; + + /** + * Very similar to randomizeUserId if this is true we save a random UUID in a cookie for cross-referencing sessions. + * {@link https://developer.bitmovin.com/playback/docs/configuration-analytics#optional-configuration} + * + * @platform web + * @defaultValue `true + */ + cookiesEnabled?: boolean; + + /** + * {@link https://developer.bitmovin.com/playback/docs/configuration-analytics#optional-configuration} + * + * @platform web + */ + cookiesDomain?: string; + + /** + * Set cookies `max-age` attribute. + * + * {@link https://developer.bitmovin.com/playback/docs/configuration-analytics#optional-configuration} + * + * @platform web + * @defaultValue `1 year` + */ + cookiesMaxAge?: number; + + /** + * {@link https://developer.bitmovin.com/playback/docs/configuration-analytics#optional-configuration} + * + * @platform web + */ + origin?: string; } diff --git a/bitmovin/src/api/CustomData.ts b/bitmovin/src/api/CustomData.ts index 7e869ace..adfe18fa 100644 --- a/bitmovin/src/api/CustomData.ts +++ b/bitmovin/src/api/CustomData.ts @@ -22,5 +22,15 @@ * {@link https://developer.bitmovin.com/playback/docs/configuration-analytics} */ export interface CustomData { + /** + * The customData fields using 1-based indexing, e.g. customData1, customData2, ..., customData50. + */ [key: `customData${number}`]: string | undefined; + + /** + * For testing purposes, Bitmovin Analytics provides an Experiment field to differentiate between testing groups. + * A popular application for this data field is to use it for testing a new player version, before rolling it out to all your viewers. + * {@link https://developer.bitmovin.com/playback/docs/configuration-analytics#optional-metadata-fields} + */ + experimentName?: string; } From fad2e522bf7687f7257fefacbcf669a70adc2659 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 6 Feb 2026 15:06:46 +0100 Subject: [PATCH 06/14] Implement web connector --- .../src/internal/BitmovinConnectorAdapter.ts | 2 +- .../internal/BitmovinConnectorAdapter.web.ts | 18 +++---- .../src/internal/web/BitmovinAdapterWeb.ts | 48 +++++++++++++++++++ 3 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 bitmovin/src/internal/web/BitmovinAdapterWeb.ts diff --git a/bitmovin/src/internal/BitmovinConnectorAdapter.ts b/bitmovin/src/internal/BitmovinConnectorAdapter.ts index 150be9a6..0387d7dd 100644 --- a/bitmovin/src/internal/BitmovinConnectorAdapter.ts +++ b/bitmovin/src/internal/BitmovinConnectorAdapter.ts @@ -1,5 +1,5 @@ import type { NativeHandleType, THEOplayer } from 'react-native-theoplayer'; -import { NativeModules, Platform } from 'react-native'; +import { NativeModules } from 'react-native'; import { AnalyticsConfig } from '../api/AnalyticsConfig'; import { SourceMetadata } from '../api/SourceMetadata'; import { CustomData } from '../api/CustomData'; diff --git a/bitmovin/src/internal/BitmovinConnectorAdapter.web.ts b/bitmovin/src/internal/BitmovinConnectorAdapter.web.ts index 3f630e27..81acc3e3 100644 --- a/bitmovin/src/internal/BitmovinConnectorAdapter.web.ts +++ b/bitmovin/src/internal/BitmovinConnectorAdapter.web.ts @@ -3,27 +3,27 @@ import { AnalyticsConfig } from '../api/AnalyticsConfig'; import { ChromelessPlayer } from 'theoplayer'; import { CustomData, SourceMetadata } from '@theoplayer/react-native-analytics-bitmovin'; import { DefaultMetadata } from '../api/DefaultMetadata'; +import { CustomDataValues, THEOplayerAdapter } from 'bitmovin-analytics'; +import { buildWebConfigFromDefaultMetadata, buildWebConfigFromSourceMetadata, buildWebSourceMetadata } from './web/BitmovinAdapterWeb'; export class BitmovinConnectorAdapter { - // private integration: TheoCollector; + private readonly integration: THEOplayerAdapter; constructor(player: THEOplayer, config: AnalyticsConfig, defaultMetadata?: DefaultMetadata) { - // this.integration = new TheoCollector(config, player.nativeHandle as ChromelessPlayer); - // if (defaultMetadata) { - // this.integration.defaultMetadata = defaultMetadata; - // } + const webConfig = buildWebConfigFromDefaultMetadata(config, defaultMetadata); + this.integration = new THEOplayerAdapter(webConfig, player.nativeHandle as ChromelessPlayer); } updateSourceMetadata(sourceMetadata: SourceMetadata): void { - // this.integration.sourceMetadata = sourceMetadata; + this.integration.sourceChange(buildWebConfigFromSourceMetadata(sourceMetadata)); } updateCustomData(customData: CustomData): void { - // Not supported in web SDK + this.integration.setCustomData(customData as CustomDataValues); } programChange(sourceMetadata: SourceMetadata): void { - // NYI + this.integration.programChange(buildWebSourceMetadata(sourceMetadata)); } sendCustomDataEvent(customData: CustomData): void { @@ -31,6 +31,6 @@ export class BitmovinConnectorAdapter { } destroy() { - // Nothing to do. + console.log('Destroying Bitmovin Analytics Connector'); } } diff --git a/bitmovin/src/internal/web/BitmovinAdapterWeb.ts b/bitmovin/src/internal/web/BitmovinAdapterWeb.ts new file mode 100644 index 00000000..638b1e02 --- /dev/null +++ b/bitmovin/src/internal/web/BitmovinAdapterWeb.ts @@ -0,0 +1,48 @@ +import { AnalyticsConfig, DefaultMetadata, SourceMetadata } from '@theoplayer/react-native-analytics-bitmovin'; +import { AnalyticsConfig as WebAnalyticsConfig, CollectorConfig, CustomDataValues } from 'bitmovin-analytics'; + +export function buildWebConfigFromDefaultMetadata(config: AnalyticsConfig, defaultMetadata?: DefaultMetadata): WebAnalyticsConfig { + return { + config: { + backendUrl: config.backendUrl, + enabled: config.enabled, + cookiesEnabled: config.cookiesEnabled, + cookiesDomain: config.cookiesDomain, + cookiesMaxAge: config.cookiesMaxAge, + origin: config.origin, + ssaiEngagementTrackingEnabled: config.ssaiEngagementTrackingEnabled, + } as CollectorConfig, + debug: config.logLevel === 'DEBUG', + key: config.licenseKey, + deviceType: config.deviceType, + deviceClass: config.deviceClass, + player: config.player, + userId: config.userId, + customUserId: defaultMetadata.customUserId, + cdnProvider: defaultMetadata.cdnProvider, + videoId: config.sourceMetadata?.videoId, + title: config?.sourceMetadata?.title, + isLive: config?.sourceMetadata?.isLive, + ...config?.sourceMetadata?.customData, + } as WebAnalyticsConfig; +} + +export function buildWebConfigFromSourceMetadata(sourceMetadata: SourceMetadata): WebAnalyticsConfig { + return { + title: sourceMetadata.title, + videoId: sourceMetadata.videoId, + cdnProvider: sourceMetadata.cdnProvider, + // path: sourceMetadata.path, // Not supported in web SDK + isLive: sourceMetadata.isLive, + ...sourceMetadata.customData, + } as WebAnalyticsConfig; +} + +export function buildWebSourceMetadata(sourceMetadata: SourceMetadata): any { + return { + ...sourceMetadata, + customData: { + ...sourceMetadata.customData, + } as CustomDataValues, + }; +} From 25053ad7fc2ab5c0d4ea0addca831bf0db3e338e Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Mon, 9 Feb 2026 14:00:45 +0100 Subject: [PATCH 07/14] Renamed Connector to handler + added sourcechange event listening on ios --- ...nConnector.swift => BitmovinHandler.swift} | 45 ++++++++++++++++--- .../THEOplayerBitmovinRCTBitmovinAPI.swift | 4 +- 2 files changed, 42 insertions(+), 7 deletions(-) rename bitmovin/ios/Connector/{BitmovinConnector.swift => BitmovinHandler.swift} (52%) diff --git a/bitmovin/ios/Connector/BitmovinConnector.swift b/bitmovin/ios/Connector/BitmovinHandler.swift similarity index 52% rename from bitmovin/ios/Connector/BitmovinConnector.swift rename to bitmovin/ios/Connector/BitmovinHandler.swift index b668b1c0..06985a62 100644 --- a/bitmovin/ios/Connector/BitmovinConnector.swift +++ b/bitmovin/ios/Connector/BitmovinHandler.swift @@ -5,22 +5,26 @@ import Foundation import THEOplayerSDK import CoreCollector -import THEOplayerCollector +import THEOplayerCollector -class BitmovinConnector { +class BitmovinHandler { private let theoplayerCollector: THEOplayerCollector.THEOplayerCollectorApi + private weak var player: THEOplayer? + private var sourceChangeListener: EventListener? + private var currentSourceMetadata: [String:Any]? init(player: THEOplayer, bitmovinConfig: [String:Any], defaultMetadata: [String:Any]? = nil) { + self.player = player let config: AnalyticsConfig = BitmovinAdapter.parseConfig(bitmovinConfig) let metadata: DefaultMetadata = BitmovinAdapter.parseDefaultMetadata(defaultMetadata) self.theoplayerCollector = THEOplayerCollector.THEOplayerCollectorFactory.create(config: config, defaultMetadata: metadata) - self.theoplayerCollector.attach(to: player) + self.attachListeners() log("Bitmovin Connector initialised with config: \(bitmovinConfig) and default metadata: \(defaultMetadata ?? [:])") } func updateSourceMetadata(_ sourceMetadata: [String:Any]) -> Void { - self.theoplayerCollector.sourceMetadata = BitmovinAdapter.parseSourceMetadata(sourceMetadata) - log("Updated source metadata: \(sourceMetadata)") + self.currentSourceMetadata = sourceMetadata + log("Updated current source metadata: \(sourceMetadata)") } func programChange(_ sourceMetadata: [String:Any]) -> Void { @@ -40,7 +44,38 @@ class BitmovinConnector { } func destroy() -> Void { + self.detachListeners() self.theoplayerCollector.detach() log("Bitmovin Connector destroyed.") } + + private func onSourceChange(event: SourceChangeEvent) { + guard let player = self.player else { return } + log("Received SOURCE_CHANGE event from THEOplayer") + + self.theoplayerCollector.detach() + log("Player detached from collector.") + + if let sourceMetadata = self.currentSourceMetadata { + self.theoplayerCollector.sourceMetadata = BitmovinAdapter.parseSourceMetadata(sourceMetadata) + self.currentSourceMetadata = nil + log("SourceMetadata updated on collector.") + } + + self.theoplayerCollector.attach(to: player) + log("Player attached to collector.") + } + + // MARK: - attach/dettach main player Listeners + private func attachListeners() { + guard let player = self.player else { return } + self.sourceChangeListener = player.addEventListener(type: PlayerEventTypes.SOURCE_CHANGE, listener: self.onSourceChange) + } + + private func detachListeners() { + guard let player = self.player else { return } + if let sourceChangeListener = self.sourceChangeListener { + player.removeEventListener(type: PlayerEventTypes.SOURCE_CHANGE, listener: sourceChangeListener) + } + } } diff --git a/bitmovin/ios/THEOplayerBitmovinRCTBitmovinAPI.swift b/bitmovin/ios/THEOplayerBitmovinRCTBitmovinAPI.swift index edfa82a2..cc5301e1 100644 --- a/bitmovin/ios/THEOplayerBitmovinRCTBitmovinAPI.swift +++ b/bitmovin/ios/THEOplayerBitmovinRCTBitmovinAPI.swift @@ -14,7 +14,7 @@ func log(_ text: String) { class THEOplayerBitmovinRCTBitmovinAPI: NSObject, RCTBridgeModule { @objc var bridge: RCTBridge! - var connectors = [NSNumber: BitmovinConnector]() + var connectors = [NSNumber: BitmovinHandler]() static func moduleName() -> String! { return "BitmovinModule" @@ -33,7 +33,7 @@ class THEOplayerBitmovinRCTBitmovinAPI: NSObject, RCTBridgeModule { if let player = theView?.player, let config = bitmovinConfig as? [String:Any] { let metadata = defaultMetadata as? [String:Any] - let connector = BitmovinConnector(player: player, bitmovinConfig: config, defaultMetadata: metadata) + let connector = BitmovinHandler(player: player, bitmovinConfig: config, defaultMetadata: metadata) self.connectors[node] = connector log("added connector to view \(node)") } else { From e529943601b58f276f4375f9d20731e3b56d49ec Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Mon, 9 Feb 2026 15:37:29 +0100 Subject: [PATCH 08/14] Take into account metadata from sourceDescription on ios --- bitmovin/ios/Connector/BitmovinAdapter.swift | 27 ++++++++++++++------ bitmovin/ios/Connector/BitmovinHandler.swift | 17 ++++++------ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/bitmovin/ios/Connector/BitmovinAdapter.swift b/bitmovin/ios/Connector/BitmovinAdapter.swift index a8ad8a43..f93d5f41 100644 --- a/bitmovin/ios/Connector/BitmovinAdapter.swift +++ b/bitmovin/ios/Connector/BitmovinAdapter.swift @@ -43,16 +43,27 @@ class BitmovinAdapter { ssaiEngagementTrackingEnabled: config[BTMVN_PROP_SSAI_ENGAGEMENT_TRACKING_ENABLED] as? Bool ?? false, errorTransformerCallback: nil ) - } + } - class func parseSourceMetadata(_ sourceMetadata: [String:Any]) -> SourceMetadata { + class func parseSourceMetadata(sourceMetadata: [String:Any]?, extractedSourceMetadata: [String:Any]? = nil) -> SourceMetadata { + let videoId = (extractedSourceMetadata?[BTMVN_PROP_VIDEO_ID] as? String) ?? (sourceMetadata?[BTMVN_PROP_VIDEO_ID] as? String) + let title = (extractedSourceMetadata?[BTMVN_PROP_TITLE] as? String) ?? (sourceMetadata?[BTMVN_PROP_TITLE] as? String) + let path = (extractedSourceMetadata?[BTMVN_PROP_PATH] as? String) ?? sourceMetadata?[BTMVN_PROP_PATH] as? String + let isLive = (extractedSourceMetadata?[BTMVN_PROP_IS_LIVE] as? Bool) ?? sourceMetadata?[BTMVN_PROP_IS_LIVE] as? Bool + let cdnProvider = (extractedSourceMetadata?[BTMVN_PROP_CDN_PROVIDER] as? String) ?? sourceMetadata?[BTMVN_PROP_CDN_PROVIDER] as? String + + var customData = extractedSourceMetadata?[BTMVN_PROP_CUSTOM_DATA] as? [String:Any] ?? [:] + if let sourceMetadataCustomData = sourceMetadata?[BTMVN_PROP_CUSTOM_DATA] as? [String:Any] { + customData.merge(sourceMetadataCustomData) { (current, _) in current } + } + return SourceMetadata( - videoId: sourceMetadata[BTMVN_PROP_VIDEO_ID] as? String, - title: sourceMetadata[BTMVN_PROP_TITLE] as? String, - path: sourceMetadata[BTMVN_PROP_PATH] as? String, - isLive: sourceMetadata[BTMVN_PROP_IS_LIVE] as? Bool, - cdnProvider: sourceMetadata[BTMVN_PROP_CDN_PROVIDER] as? String, - customData: BitmovinAdapter.parseCustomData(sourceMetadata[BTMVN_PROP_CUSTOM_DATA] as? [String:Any]) + videoId: videoId, + title: title, + path: path, + isLive: isLive, + cdnProvider: cdnProvider, + customData: BitmovinAdapter.parseCustomData(customData) ) } diff --git a/bitmovin/ios/Connector/BitmovinHandler.swift b/bitmovin/ios/Connector/BitmovinHandler.swift index 06985a62..eaaf911a 100644 --- a/bitmovin/ios/Connector/BitmovinHandler.swift +++ b/bitmovin/ios/Connector/BitmovinHandler.swift @@ -28,7 +28,7 @@ class BitmovinHandler { } func programChange(_ sourceMetadata: [String:Any]) -> Void { - let newSourceMetadata = BitmovinAdapter.parseSourceMetadata(sourceMetadata) + let newSourceMetadata = BitmovinAdapter.parseSourceMetadata(sourceMetadata: sourceMetadata) self.theoplayerCollector.programChange(newSourceMetadata: newSourceMetadata) log("Notified program change with new source metadata: \(sourceMetadata)") } @@ -49,18 +49,19 @@ class BitmovinHandler { log("Bitmovin Connector destroyed.") } + // MARK: - event handling private func onSourceChange(event: SourceChangeEvent) { - guard let player = self.player else { return } + guard let player = self.player, let source = player.source else { return } log("Received SOURCE_CHANGE event from THEOplayer") self.theoplayerCollector.detach() log("Player detached from collector.") - if let sourceMetadata = self.currentSourceMetadata { - self.theoplayerCollector.sourceMetadata = BitmovinAdapter.parseSourceMetadata(sourceMetadata) - self.currentSourceMetadata = nil - log("SourceMetadata updated on collector.") - } + self.theoplayerCollector.sourceMetadata = BitmovinAdapter.parseSourceMetadata( + sourceMetadata: self.currentSourceMetadata, + extractedSourceMetadata: source.metadata?.metadataKeys) + self.currentSourceMetadata = nil + log("SourceMetadata updated on collector.") self.theoplayerCollector.attach(to: player) log("Player attached to collector.") @@ -71,7 +72,7 @@ class BitmovinHandler { guard let player = self.player else { return } self.sourceChangeListener = player.addEventListener(type: PlayerEventTypes.SOURCE_CHANGE, listener: self.onSourceChange) } - + private func detachListeners() { guard let player = self.player else { return } if let sourceChangeListener = self.sourceChangeListener { From 0938469894c34588afc5761b4a91152b3a19568d Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Tue, 10 Feb 2026 11:55:17 +0100 Subject: [PATCH 09/14] Fix collector destruction in strict mode --- .../reactnative/bitmovin/BitmovinHandler.kt | 2 +- .../src/internal/BitmovinConnectorAdapter.web.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinHandler.kt b/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinHandler.kt index 73adb305..487cf325 100644 --- a/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinHandler.kt +++ b/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinHandler.kt @@ -53,7 +53,7 @@ class BitmovinHandler( // Detach player before setting new SourceMetadata. collector.detachPlayer() /** - * Merge the current source metadata with the new metadata from the playe source, if available. + * Merge the current source metadata with the new metadata from the player source, if available. * Prioritize the player source's metadata. */ mergeSourceMetadata(currentSourceMetadata, player.source?.metadata)?.let { diff --git a/bitmovin/src/internal/BitmovinConnectorAdapter.web.ts b/bitmovin/src/internal/BitmovinConnectorAdapter.web.ts index 81acc3e3..de5901be 100644 --- a/bitmovin/src/internal/BitmovinConnectorAdapter.web.ts +++ b/bitmovin/src/internal/BitmovinConnectorAdapter.web.ts @@ -6,6 +6,8 @@ import { DefaultMetadata } from '../api/DefaultMetadata'; import { CustomDataValues, THEOplayerAdapter } from 'bitmovin-analytics'; import { buildWebConfigFromDefaultMetadata, buildWebConfigFromSourceMetadata, buildWebSourceMetadata } from './web/BitmovinAdapterWeb'; +const BITMOVIN_ANALYTICS_AUGMENTED_MARKER = '__bitmovinAnalyticsHasBeenSetup'; + export class BitmovinConnectorAdapter { private readonly integration: THEOplayerAdapter; @@ -31,6 +33,14 @@ export class BitmovinConnectorAdapter { } destroy() { - console.log('Destroying Bitmovin Analytics Connector'); + /** + * We can safely disable the BITMOVIN_ANALYTICS_AUGMENTED_MARKER here to avoid duplicate connectors being attached to the same player instance, + * because we know either the collector or both player and collector were destroyed here. + * This is needed because when using in React, mount effects will trigger twice in development mode. + */ + const container = document.querySelector('.theoplayer-container'); + if (container) { + container[BITMOVIN_ANALYTICS_AUGMENTED_MARKER] = false; + } } } From fa1404a92f59e391f9637f9d6e8316b6bfe5179f Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Tue, 10 Feb 2026 12:13:19 +0100 Subject: [PATCH 10/14] Temporarily disable sourceMetadata merging --- bitmovin/ios/Connector/BitmovinHandler.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bitmovin/ios/Connector/BitmovinHandler.swift b/bitmovin/ios/Connector/BitmovinHandler.swift index eaaf911a..54e87399 100644 --- a/bitmovin/ios/Connector/BitmovinHandler.swift +++ b/bitmovin/ios/Connector/BitmovinHandler.swift @@ -51,15 +51,13 @@ class BitmovinHandler { // MARK: - event handling private func onSourceChange(event: SourceChangeEvent) { - guard let player = self.player, let source = player.source else { return } + guard let player = self.player/*, let source = player.source*/ else { return } log("Received SOURCE_CHANGE event from THEOplayer") self.theoplayerCollector.detach() log("Player detached from collector.") - self.theoplayerCollector.sourceMetadata = BitmovinAdapter.parseSourceMetadata( - sourceMetadata: self.currentSourceMetadata, - extractedSourceMetadata: source.metadata?.metadataKeys) + self.theoplayerCollector.sourceMetadata = BitmovinAdapter.parseSourceMetadata(sourceMetadata: self.currentSourceMetadata/*, extractedSourceMetadata: source.metadata?.metadataKeys*/) self.currentSourceMetadata = nil log("SourceMetadata updated on collector.") From a5199068b2f7ba4a90a5bdd612bdc4ed0cc39aed Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Tue, 10 Feb 2026 13:09:07 +0100 Subject: [PATCH 11/14] Disable source metadata for now --- .../reactnative/bitmovin/BitmovinHandler.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinHandler.kt b/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinHandler.kt index 487cf325..a38b6915 100644 --- a/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinHandler.kt +++ b/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinHandler.kt @@ -52,17 +52,26 @@ class BitmovinHandler( Log.d("BitmovinConnector", "Handling source change event") // Detach player before setting new SourceMetadata. collector.detachPlayer() + + /** + * !! Do not merge source metadata on source change, as the Bitmovin Web collector does not + * support this yet. + */ /** * Merge the current source metadata with the new metadata from the player source, if available. * Prioritize the player source's metadata. */ - mergeSourceMetadata(currentSourceMetadata, player.source?.metadata)?.let { +// mergeSourceMetadata(currentSourceMetadata, player.source?.metadata)?.let { +// collector.sourceMetadata = it +// } + currentSourceMetadata?.let { collector.sourceMetadata = it } currentSourceMetadata = null collector.attachPlayer(player) } + @Suppress("unused") private fun mergeSourceMetadata( sourceMetadata: SourceMetadata?, metadata: MetadataDescription? ): SourceMetadata? { From 0339101b5ba99459e0a5cffabd860abf3e55b727 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Tue, 10 Feb 2026 13:16:48 +0100 Subject: [PATCH 12/14] Update source metadata docs --- bitmovin/README.md | 17 ++++++++++++++--- bitmovin/src/api/BitmovinConnector.ts | 4 ++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/bitmovin/README.md b/bitmovin/README.md index 737c4396..6d0bacbf 100644 --- a/bitmovin/README.md +++ b/bitmovin/README.md @@ -4,12 +4,18 @@ A Bitmovin analytics connector for `@theoplayer/react-native`. ## Installation +Using npm: ```sh npm install @theoplayer/react-native-analytics-bitmovin ``` - [//]: # (npm install @theoplayer/react-native-analytics-bitmovin) +Using yarn: +```sh +yarn add @theoplayer/react-native-analytics-bitmovin +``` +[//]: # (yarn add @theoplayer/react-native-analytics-bitmovin) + ## Usage ### Configuring the connector @@ -19,7 +25,10 @@ Create the connector using the `useBitmovin` hook with the initial configuration Once the player is ready, initialize the connector by calling the `initBitmovin` function with the `THEOplayer` instance and the **default metadata**, which contains properties that do not change during the session. -Finally, when the player source is set, update the **source metadata** by calling the `updateSourceMetadata` function. +Finally, before the player source is set, update first the **source metadata** by calling the `updateSourceMetadata` function. + +> **IMPORTANT:** Call `updateSourceMetadata` before setting the player source, otherwise the source metadata will +> not be included in the analytics events. ```tsx import { @@ -61,10 +70,12 @@ const App = () => { const onPlayerReady = (player: THEOplayer) => { // Initialize connector with player & default metadata. initBitmovin(player, defaultMetadata); - player.source = {/*...*/} // Update source metadata. bitmovin.updateSourceMetadata(sourceMetadata); + + // Set player source after updating source metadata. + player.source = {/*...*/} } return (); diff --git a/bitmovin/src/api/BitmovinConnector.ts b/bitmovin/src/api/BitmovinConnector.ts index 3c7e088e..eecdf5bf 100644 --- a/bitmovin/src/api/BitmovinConnector.ts +++ b/bitmovin/src/api/BitmovinConnector.ts @@ -21,6 +21,10 @@ export class BitmovinConnector { /** * Set or update metadata for the current source. + * + * **IMPORTANT:** Call this method before setting a new source on the player, + * otherwise the metadata will not be associated with the correct source. + * * @param sourceMetadata contains the key value pairs with data. */ updateSourceMetadata(sourceMetadata: SourceMetadata): void { From 52d0b018f278db988cf652dcb40682f64dc84b0d Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Tue, 10 Feb 2026 13:22:58 +0100 Subject: [PATCH 13/14] Add changeset entry --- .changeset/vast-tires-wish.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/vast-tires-wish.md diff --git a/.changeset/vast-tires-wish.md b/.changeset/vast-tires-wish.md new file mode 100644 index 00000000..5ee7ff00 --- /dev/null +++ b/.changeset/vast-tires-wish.md @@ -0,0 +1,5 @@ +--- +'@theoplayer/react-native-analytics-bitmovin': minor +--- + +Added support for Web platforms. From 0164f2adc8d951979134b739659c2199c2144794 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Tue, 10 Feb 2026 13:24:58 +0100 Subject: [PATCH 14/14] Add changeset --- .changeset/olive-buckets-return.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/olive-buckets-return.md diff --git a/.changeset/olive-buckets-return.md b/.changeset/olive-buckets-return.md new file mode 100644 index 00000000..38fe56a2 --- /dev/null +++ b/.changeset/olive-buckets-return.md @@ -0,0 +1,5 @@ +--- +'@theoplayer/react-native-analytics-bitmovin': minor +--- + +Added `programChange` method to allow passing new source metadata during a live stream.