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. 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. 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/README.md b/bitmovin/README.md index c14a6b9f..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 { @@ -38,8 +47,8 @@ const defaultMetadata: DefaultMetadata = { cdnProvider: 'akamai', customUserId: 'custom-user-id-1234', customData: { - customData0: 'value0', - customData1: 'value1' + customData1: 'value1', + customData2: 'value2' } }; @@ -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/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) 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..a38b6915 --- /dev/null +++ b/bitmovin/android/src/main/java/com/theoplayer/reactnative/bitmovin/BitmovinHandler.kt @@ -0,0 +1,98 @@ +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() + + /** + * !! 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 { +// collector.sourceMetadata = it +// } + currentSourceMetadata?.let { + collector.sourceMetadata = it + } + currentSourceMetadata = null + collector.attachPlayer(player) + } + + @Suppress("unused") + 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() + } } } 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/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..54e87399 100644 --- a/bitmovin/ios/Connector/BitmovinConnector.swift +++ b/bitmovin/ios/Connector/BitmovinHandler.swift @@ -5,26 +5,30 @@ 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 { - 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)") } @@ -40,7 +44,37 @@ class BitmovinConnector { } func destroy() -> Void { + self.detachListeners() self.theoplayerCollector.detach() log("Bitmovin Connector destroyed.") } + + // MARK: - event handling + private func onSourceChange(event: SourceChangeEvent) { + 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.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 { 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/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/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 { diff --git a/bitmovin/src/api/CustomData.ts b/bitmovin/src/api/CustomData.ts index b0918f35..adfe18fa 100644 --- a/bitmovin/src/api/CustomData.ts +++ b/bitmovin/src/api/CustomData.ts @@ -11,8 +11,26 @@ * 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 { + /** + * 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; } 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..de5901be 100644 --- a/bitmovin/src/internal/BitmovinConnectorAdapter.web.ts +++ b/bitmovin/src/internal/BitmovinConnectorAdapter.web.ts @@ -3,27 +3,29 @@ 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'; + +const BITMOVIN_ANALYTICS_AUGMENTED_MARKER = '__bitmovinAnalyticsHasBeenSetup'; 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 +33,14 @@ export class BitmovinConnectorAdapter { } destroy() { - // Nothing to do. + /** + * 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; + } } } 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, + }; +} 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",