From 30f6e6f050d9b849db993125489d1ae419657b0d Mon Sep 17 00:00:00 2001 From: Matthias Tamegger Date: Tue, 12 Sep 2023 14:19:25 +0200 Subject: [PATCH] feat: upgrade analytics api v3 and the bundle analytics-player --- CHANGELOG.md | 9 + .../player/reactnative/AnalyticsModule.kt | 191 +----------------- .../player/reactnative/PlayerModule.kt | 4 +- .../player/reactnative/SourceModule.kt | 62 ++++-- .../reactnative/converter/JsonConverter.kt | 22 +- example/src/screens/BasicAnalytics.tsx | 52 ++--- ios/AnalyticsModule.m | 9 +- ios/AnalyticsModule.swift | 173 +--------------- ios/PlayerModule.m | 1 + ios/PlayerModule.swift | 25 +++ ios/RCTConvert+BitmovinPlayer.swift | 94 +++------ ios/SourceModule.m | 4 +- ios/SourceModule.swift | 21 +- src/analytics/collector.ts | 135 ------------- src/analytics/config.ts | 102 +++++----- src/analytics/index.ts | 2 +- src/analytics/player.ts | 36 ++++ src/player.ts | 17 +- src/source.ts | 21 +- 19 files changed, 300 insertions(+), 680 deletions(-) delete mode 100644 src/analytics/collector.ts create mode 100644 src/analytics/player.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fd652829..f32657c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,19 @@ - `Player.getAudioTrack` and `Player.getSubtitleTrack` APIs to get currently selected audio and subtitle tracks - `SourceConfig.description` property to allow setting a description for the source - `Player.getThumbnail` and `Source.getThumbnail` APIs to get thumbnail images +- `DefaultMetadata` for configuration of the bundled analytics collector +- `Player.analytics` to access the `AnalyticsApi` and interact with the bundled analytics collector +- `SourceConfig.analyticsSourceMetadata` for extended configuration of the bundled analytics collector ### Changed - Update Bitmovin's native Android SDK version to `3.43.0` +- `AnalyticsConfig` properties to match the bitmovin analytics v3 API + +### Removed + +- `AnalyticsCollector` in favor of the bundled analytics functionality +- `CdnProvider`, as the property on the `AnalyticsConfig` is now a `string` ## [0.10.0] (2023-09-04) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt index 8545df0f..23c9eeb4 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt @@ -1,7 +1,6 @@ package com.bitmovin.player.reactnative -import android.util.Log -import com.bitmovin.analytics.bitmovin.player.api.IBitmovinPlayerCollector +import com.bitmovin.player.api.analytics.AnalyticsApi.Companion.analytics import com.bitmovin.player.reactnative.converter.JsonConverter import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule @@ -11,95 +10,11 @@ private const val MODULE_NAME = "AnalyticsModule" @ReactModule(name = MODULE_NAME) class AnalyticsModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { - /** - * In-memory mapping from `nativeId`s to `BitmovinPlayerCollector` instances. - */ - private val collectors: Registry = mutableMapOf() - /** * JS exported module name. */ override fun getName() = MODULE_NAME - /** - * Fetches the `BitmovinPlayerCollector` instance associated with `nativeId` from the internal registry. - * @param nativeId `BitmovinPlayerCollector` instance ID. - * @return The associated `BitmovinPlayerCollector` instance or `null`. - */ - fun getCollector(nativeId: NativeId?): IBitmovinPlayerCollector? { - if (nativeId == null) { - return null - } - return collectors[nativeId] - } - - /** - * Creates a new `BitmovinPlayerCollector` instance inside the internal registry using the provided `config` object. - * @param config `BitmovinAnalyticsConfig` object received from JS. - */ - @ReactMethod - fun initWithConfig(nativeId: NativeId, config: ReadableMap?) { - uiManager()?.addUIBlock { _ -> - JsonConverter.toAnalyticsConfig(config)?.let { - collectors[nativeId] = IBitmovinPlayerCollector.create(it, context) - } - } - } - - /** - * Detaches and removes the given `BitmovinPlayerCollector` from the internal registry. - * @param nativeId Native Id of the collector instance. - */ - @ReactMethod - fun destroy(nativeId: NativeId) { - uiManager()?.addUIBlock { - if (collectors.containsKey(nativeId)) { - collectors[nativeId]?.detachPlayer() - collectors.remove(nativeId) - } - } - } - - /** - * Attaches a `BitmovinPlayerCollector` to the `Player` instance with native Id equal to `playerId`. - * @param nativeId Native Id of the collector instance. - * @param playerId Native Id of the player instance. - */ - @ReactMethod - fun attach(nativeId: NativeId, playerId: NativeId) { - uiManager()?.addUIBlock { _ -> - playerModule()?.getPlayer(playerId)?.let { - collectors[nativeId]?.attachPlayer(it) - } - } - } - - /** - * Detaches the player object from a `BitmovinPlayerCollector` instance. - * @param nativeId Native Id of the collector instance. - */ - @ReactMethod - fun detach(nativeId: NativeId) { - uiManager()?.addUIBlock { _ -> - collectors[nativeId]?.detachPlayer() - } - } - - /** - * Updates the custom data config for a `BitmovinPlayerCollector` instance. - * @param nativeId Native Id of the collector instance. - * @param json Custom data config json. - */ - @Deprecated("Confusing API naming", replaceWith = ReplaceWith("sendCustomDataEvent(nativeId, json)")) - @ReactMethod - fun setCustomDataOnce(nativeId: NativeId, json: ReadableMap?) { - uiManager()?.addUIBlock { _ -> - JsonConverter.toAnalyticsCustomData(json)?.let { - collectors[nativeId]?.setCustomDataOnce(it) - } - } - } - /** * Sends a sample with the provided custom data. * Does not change the configured custom data of the collector or source. @@ -110,114 +25,20 @@ class AnalyticsModule(private val context: ReactApplicationContext) : ReactConte fun sendCustomDataEvent(nativeId: NativeId, json: ReadableMap?) { uiManager()?.addUIBlock { _ -> JsonConverter.toAnalyticsCustomData(json)?.let { - collectors[nativeId]?.sendCustomDataEvent(it) - } - } - } - - - /** - * Sets the custom data config for a `BitmovinPlayerCollector` instance. - * @param nativeId Native Id of the collector instance. - * @param json Custom data config json. - */ - @ReactMethod - fun setCustomData(nativeId: NativeId, playerId: NativeId?, json: ReadableMap?) { - uiManager()?.addUIBlock { _ -> - val source = playerModule()?.getPlayer(playerId)?.source - val collector = collectors[nativeId] - val customData = JsonConverter.toAnalyticsCustomData(json) - when { - source == null -> Log.d( - "[AnalyticsModule]", "Could not find source for player ($playerId)" - ) - collector == null -> Log.d( - "[AnalyticsModule]", "Could not find analytics collector ($nativeId)" - ) - customData == null -> Log.d( - "[AnalyticsModule]", "Could not convert custom data, thus they are not applied to the active source for the player ($playerId) with the collector ($nativeId)" - ) - else -> collector.setCustomData(source, customData) - } - } - } - - /** - * Gets the current custom data config for a `BitmovinPlayerCollector` instance. - * @param nativeId Native Id of the the collector instance. - * @param promise JS promise object. - */ - @ReactMethod - fun getCustomData(nativeId: NativeId, playerId: NativeId?, promise: Promise) { - uiManager()?.addUIBlock { _ -> - val source = playerModule()?.getPlayer(playerId)?.source - val collector = collectors[nativeId] - when { - source == null -> promise.reject( - "[AnalyticsModule]", "Could not find source for player ($playerId)" - ) - collector == null -> promise.reject( - "[AnalyticsModule]", "Could not find analytics collector ($nativeId)" - ) - else -> promise.resolve(JsonConverter.fromAnalyticsCustomData(collector.getCustomData(source))) - } - } - } - - @ReactMethod - fun addSourceMetadata(nativeId: NativeId, playerId: NativeId?, json: ReadableMap?) { - uiManager()?.addUIBlock { _ -> - val source = playerModule()?.getPlayer(playerId)?.source - val collector = collectors[nativeId] - val sourceMetadata = JsonConverter.toAnalyticsSourceMetadata(json) - when { - source == null -> Log.d( - "[AnalyticsModule]", "Could not find source for player ($playerId)" - ) - collector == null -> Log.d( - "[AnalyticsModule]", "Could not find analytics collector ($nativeId)" - ) - sourceMetadata == null -> Log.d( - "[AnalyticsModule]", "Could not convert source metadata, thus they are not applied to the collector ($nativeId)" - ) - else -> collector.addSourceMetadata(source, sourceMetadata) - } - } - } - - /** - * Sets the source metadata for the current active source of the player associated to `playerId`. - */ - @ReactMethod - fun setSourceMetadata(nativeId: NativeId, playerId: NativeId?, json: ReadableMap?) { - uiManager()?.addUIBlock { _ -> - val source = playerModule()?.getPlayer(playerId)?.source - val collector = collectors[nativeId] - val sourceMetadata = JsonConverter.toAnalyticsSourceMetadata(json) - when { - source == null -> Log.d( - "[AnalyticsModule]", "Could not find source for player ($playerId)" - ) - collector == null -> Log.d( - "[AnalyticsModule]", "Could not find analytics collector ($nativeId)" - ) - sourceMetadata == null -> Log.d( - "[AnalyticsModule]", "Could not convert source metadata, thus they are not applied to the collector ($nativeId)" - ) - else -> collector.setSourceMetadata(source, sourceMetadata) + playerModule()?.getPlayer(nativeId)?.analytics?.sendCustomDataEvent(it) } } } /** - * Gets the current user Id for a `BitmovinPlayerCollector` instance. - * @param nativeId Native Id of the the collector instance. + * Gets the current user Id for a player instance with analytics. + * @param nativeId Native Id of the the player instance. * @param promise JS promise object. */ @ReactMethod - fun getUserId(nativeId: NativeId, promise: Promise) { + fun getUserId(playerId: NativeId, promise: Promise) { uiManager()?.addUIBlock { _ -> - collectors[nativeId]?.let { + playerModule()?.getPlayer(playerId)?.analytics?.let { promise.resolve(it.userId) } } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt index 79017c2d..c403745b 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -56,7 +56,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param analyticsConfigJson `AnalyticsConfig` object received from JS. */ @ReactMethod - fun initWithConfig(nativeId: NativeId, playerConfigJson: ReadableMap?, analyticsConfigJson: ReadableMap?) { + fun initWithAnalyticsConfig(nativeId: NativeId, playerConfigJson: ReadableMap?, analyticsConfigJson: ReadableMap?) { uiManager()?.addUIBlock { if (players.containsKey(nativeId)) { Log.d("[PlayerModule]", "Duplicate player creation for id $nativeId") @@ -64,7 +64,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB } val playerConfig = JsonConverter.toPlayerConfig(playerConfigJson) val analyticsConfig = JsonConverter.toAnalyticsConfig(analyticsConfigJson) - val defaultMetadata = JsonConverter.toAnalyticsDefaultMetadata(analyticsConfigJson) + val defaultMetadata = JsonConverter.toAnalyticsDefaultMetadata(analyticsConfigJson?.getMap("defaultMetadata")) players[nativeId] = if (analyticsConfig == null) { Player.create(context, playerConfig) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt index 04d3a6f5..6079091f 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt @@ -1,8 +1,16 @@ package com.bitmovin.player.reactnative +import android.util.Log +import com.bitmovin.analytics.api.SourceMetadata +import com.bitmovin.player.api.analytics.create import com.bitmovin.player.api.source.Source +import com.bitmovin.player.api.source.SourceConfig import com.bitmovin.player.reactnative.converter.JsonConverter -import com.facebook.react.bridge.* +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.UIManagerModule @@ -33,17 +41,24 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB } /** - * Creates a new `Source` instance inside the internal sources using the provided `config` object. + * Creates a new `Source` instance inside the internal sources using the provided + * `config` and `analyticsSourceMetadata` object as well as an initialized DRM configuration ID. * @param nativeId ID to be associated with the `Source` instance. + * @param drmNativeId ID of the DRM config to use. * @param config `SourceConfig` object received from JS. + * @param analyticsSourceMetadata `SourceMetadata` object received from JS. */ @ReactMethod - fun initWithConfig(nativeId: NativeId, config: ReadableMap?) { + fun initWithAnalyticsConfig( + nativeId: NativeId, + drmNativeId: NativeId?, + config: ReadableMap?, + analyticsSourceMetadata: ReadableMap? + ) { uiManager()?.addUIBlock { - if (!sources.containsKey(nativeId)) { - JsonConverter.toSourceConfig(config)?.let { - sources[nativeId] = Source.create(it) - } + val sourceMetadata = JsonConverter.toAnalyticsSourceMetadata(analyticsSourceMetadata) ?: SourceMetadata() + initializeSource(nativeId, drmNativeId, config) { sourceConfig -> + Source.create(sourceConfig, sourceMetadata) } } } @@ -56,15 +71,36 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB * @param config `SourceConfig` object received from JS. */ @ReactMethod - fun initWithDrmConfig(nativeId: NativeId, drmNativeId: NativeId, config: ReadableMap?) { + fun initWithConfig( + nativeId: NativeId, + drmNativeId: NativeId?, + config: ReadableMap?, + ) { uiManager()?.addUIBlock { - val drmConfig = drmModule()?.getConfig(drmNativeId) - if (!sources.containsKey(nativeId) && drmConfig != null) { - JsonConverter.toSourceConfig(config)?.let { - it.drmConfig = drmConfig - sources[nativeId] = Source.create(it) + initializeSource(nativeId, drmNativeId, config) { sourceConfig -> + Source.create(sourceConfig) + } + } + } + + private fun initializeSource( + nativeId: NativeId, + drmNativeId: NativeId?, + config: ReadableMap?, + action: (SourceConfig) -> Source + ) { + val drmConfig = drmNativeId?.let { drmModule()?.getConfig(it) } + if (!sources.containsKey(nativeId)) { + val sourceConfig = JsonConverter.toSourceConfig(config)?.apply { + if (drmConfig != null) { + this.drmConfig = drmConfig } } + if (sourceConfig == null) { + Log.d("[SourceModule]", "Could not parse SourceConfig") + } else { + sources[nativeId] = action(sourceConfig) + } } } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt index e988986d..f200effb 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt @@ -1,6 +1,5 @@ package com.bitmovin.player.reactnative.converter -import com.bitmovin.analytics.BitmovinAnalyticsConfig import com.bitmovin.analytics.api.AnalyticsConfig import com.bitmovin.analytics.api.CustomData import com.bitmovin.analytics.api.DefaultMetadata @@ -41,6 +40,7 @@ import com.bitmovin.player.api.ui.StyleConfig import com.bitmovin.player.reactnative.extensions.getBooleanOrNull import com.bitmovin.player.reactnative.extensions.getName import com.bitmovin.player.reactnative.extensions.getProperty +import com.bitmovin.player.reactnative.extensions.putBoolean import com.bitmovin.player.reactnative.extensions.putDouble import com.bitmovin.player.reactnative.extensions.putInt import com.bitmovin.player.reactnative.extensions.setProperty @@ -808,11 +808,6 @@ class JsonConverter { it.getBooleanOrNull("randomizeUserId")?.let { randomizeUserId -> setRandomizeUserId(randomizeUserId) } - for (n in 1..30) { - it.getString("customData${n}")?.let { customDataN -> - setProperty("customData${n}", customDataN) - } - } }.build() } @@ -866,7 +861,7 @@ class JsonConverter { * @return The produced JS value or null. */ @JvmStatic - fun fromAnalyticsCustomData(customData: CustomData?): ReadableMap? = customData?.let { + fun fromAnalyticsCustomData(customData: CustomData?): WritableMap? = customData?.let { val json = Arguments.createMap() for (n in 1..30) { it.getProperty("customData${n}")?.let { customDataN -> @@ -892,6 +887,19 @@ class JsonConverter { ) } + @JvmStatic + fun fromAnalyticsSourceMetadata(sourceMetadata: SourceMetadata?): ReadableMap? { + if (sourceMetadata == null) return null + + return fromAnalyticsCustomData(sourceMetadata.customData)?.apply { + putString("title", sourceMetadata.title) + putString("videoId", sourceMetadata.videoId) + putString("cdnProvider", sourceMetadata.cdnProvider) + putString("path", sourceMetadata.path) + putBoolean("isLive", sourceMetadata.isLive) + } + } + /** * Converts any `VideoQuality` value into its json representation. * @param videoQuality `VideoQuality` value. diff --git a/example/src/screens/BasicAnalytics.tsx b/example/src/screens/BasicAnalytics.tsx index 9db9b48d..0f0ffe7c 100644 --- a/example/src/screens/BasicAnalytics.tsx +++ b/example/src/screens/BasicAnalytics.tsx @@ -5,7 +5,6 @@ import { usePlayer, PlayerView, SourceType, - CdnProvider, } from 'bitmovin-player-react-native'; import { useTVGestures } from '../hooks'; @@ -14,22 +13,19 @@ export default function BasicAds() { const player = usePlayer({ analyticsConfig: { - key: '', // `key` is the only required parameter. - playerKey: '', - cdnProvider: CdnProvider.AKAMAI, // Check out `CdnProvider` for more options. - customUserId: 'Custom user ID', - randomizeUserId: false, // Default value is true. - experimentName: 'Experiment name', - videoId: 'MyVideoId', - title: 'Art of Motion', - isLive: false, - ads: false, // Can be changed to `true` in case `advertisingConfig` is also present. - path: '/examples/basic_analytics', - customData1: 'Custom data field 1', - customData2: 'Custom data field 2', - customData3: 'Custom data field 3', - customData4: 'Custom data field 4', - customData5: 'Custom data field 5', + licenseKey: '', // `licenseKey` is the only required parameter. + randomizeUserId: false, + adTrackingDisabled: true, + defaultMetadata: { + cdnProvider: 'akamai', + customUserId: 'Custom user ID from React', + experimentName: 'Experiment name', + customData1: 'Custom data field 1', + customData2: 'Custom data field 2', + customData3: 'Custom data field 3', + customData4: 'Custom data field 4', + customData5: 'Custom data field 5', + }, }, }); @@ -44,10 +40,14 @@ export default function BasicAds() { title: 'Art of Motion', poster: 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/poster.jpg', - }); - player.analyticsCollector?.setCustomData({ - experimentName: 'setCustomData', - customData1: `customized data 1`, + analyticsSourceMetadata: { + videoId: 'MyVideoId', + title: 'Art of Motion', + isLive: false, + path: '/examples/basic_analytics', + customData1: 'Custom data field 1 from source', + experimentName: 'Experiment Name Override', + }, }); return () => { player.destroy(); @@ -55,16 +55,6 @@ export default function BasicAds() { }, [player]) ); - setTimeout(() => { - player.load({ - url: 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8', - type: SourceType.HLS, - title: 'Art of Motion HLS', - poster: - 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/poster.jpg', - }); - }, 10000); - return ( diff --git a/ios/AnalyticsModule.m b/ios/AnalyticsModule.m index 9600f763..c3766806 100644 --- a/ios/AnalyticsModule.m +++ b/ios/AnalyticsModule.m @@ -2,14 +2,7 @@ @interface RCT_EXTERN_REMAP_MODULE(AnalyticsModule, AnalyticsModule, NSObject) -RCT_EXTERN_METHOD(initWithConfig:(NSString *)nativeId config:(nullable id)config) -RCT_EXTERN_METHOD(destroy:(NSString *)nativeId) -RCT_EXTERN_METHOD(attach:(NSString *)nativeId playerId:(NSString *)playerId) -RCT_EXTERN_METHOD(detach:(NSString *)nativeId) -RCT_EXTERN_METHOD(setCustomDataOnce:(NSString *)nativeId json:(nullable id)json) -RCT_EXTERN_METHOD(setCustomData:(NSString *)nativeId playerId:(nullable NSString *)playerId json:(nullable id)json) -RCT_EXTERN_METHOD(getCustomData:(NSString *)nativeId playerId:(nullable NSString *)playerId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(sendCustomDataEvent:(NSString *)nativeId json:(nullable id)json) RCT_EXTERN_METHOD(getUserId:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(addSourceMetadata:(NSString *)nativeId playerId:(nullable NSString *)playerId json:(nullable id)json) @end diff --git a/ios/AnalyticsModule.swift b/ios/AnalyticsModule.swift index 35ba1dd6..fe2ac945 100644 --- a/ios/AnalyticsModule.swift +++ b/ios/AnalyticsModule.swift @@ -11,9 +11,6 @@ class AnalyticsModule: NSObject, RCTBridgeModule { bridge.module(for: PlayerModule.self) as? PlayerModule } - /// In-memory mapping from `nativeId`s to `BitmovinPlayerCollector` instances. - private var collectors: Registry = [:] - /// JS module name. static func moduleName() -> String! { "AnalyticsModule" @@ -30,190 +27,42 @@ class AnalyticsModule: NSObject, RCTBridgeModule { } /** - Retrieves a `BitmovinPlayerCollector` instance from the internal registry for the given `nativeId`. - - Parameter nativeId: Native Id of the collector instance. - - Returns: Collector instance associated with the `nativeId` or `nil`. - */ - @objc func retrieve(_ nativeId: NativeId) -> BitmovinPlayerCollector? { - collectors[nativeId] - } - - /** - Creates a new `BitmovinPlayerCollector` instance inside the internal registry using the provided `config` object. - - Parameter nativeId: ID to associate with the `BitmovinPlayerCollector` instance. - - Parameter config: `BitmovinAnalyticsConfig` object received from JS. - */ - @objc(initWithConfig:config:) - func initWithConfig(_ nativeId: NativeId, config: Any?) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - guard let analyticsConfig = RCTConvert.analyticsConfig(config) else { - return - } - self?.collectors[nativeId] = BitmovinPlayerCollector(config: analyticsConfig) - } - } - - /** - Detaches and removes the given `BitmovinPlayerCollector` from the internal registry. - - Parameter nativeId: Native Id of the collector instance. - */ - @objc(destroy:) - func destroy(_ nativeId: NativeId) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - self?.collectors[nativeId]?.detachPlayer() - self?.collectors[nativeId] = nil - } - } - - /** - Attaches a `BitmovinPlayerCollector` to the `Player` instance with native Id equal to `playerId`. - - Parameter nativeId: Native Id of the collector instance. + Sends a sample with the provided custom data. + Does not change the configured custom data of the collector or source. - Parameter playerId: Native Id of the player instance. - */ - @objc(attach:playerId:) - func attach(_ nativeId: NativeId, playerId: NativeId) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - guard - let collector = self?.collectors[nativeId], - let player = self?.playerModule?.retrieve(playerId) - else { - return - } - collector.attachPlayer(player: player) - } - } - - /** - Detaches the player object from a `BitmovinPlayerCollector` instance. - - Parameter nativeId: Native Id of the collector instance. - */ - @objc(detach:) - func detach(_ nativeId: NativeId) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - guard let collector = self?.collectors[nativeId] else { - return - } - collector.detachPlayer() - } - } - - /** - Updates the custom data config for a `BitmovinPlayerCollector` instance. - - Parameter nativeId: Native Id of the collector instance. - Parameter json: Custom data config json. */ - @objc(setCustomDataOnce:json:) - func setCustomDataOnce(_ nativeId: NativeId, json: Any?) { + @objc(sendCustomDataEvent:json:) + func sendCustomDataEvent(_ playerId: NativeId, json: Any?) { bridge.uiManager.addUIBlock { [weak self] _, _ in guard - let collector = self?.collectors[nativeId], + let playerAnalytics = self?.playerModule?.retrieve(playerId)?.analytics, let customData = RCTConvert.analyticsCustomData(json) else { return } - collector.setCustomDataOnce(customData: customData) - } - } - - /** - Sets the custom data config for a `BitmovinPlayerCollector` instance. - - Parameter nativeId: Native Id of the collector instance. - - Parameter playerId: Native Id of the player instance. - - Parameter json: Custom data config json. - */ - @objc(setCustomData:playerId:json:) - func setCustomData( - _ nativeId: NativeId, - playerId: NativeId?, - json: Any? - ) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - guard - let collector = self?.collectors[nativeId], - let customData = RCTConvert.analyticsCustomData(json), - let playerId = playerId, - let player = self?.bridge[PlayerModule.self]?.retrieve(playerId), - let source = player.source - else { - return - } - collector.apply(customData: customData, for: source) - } - } - - /** - Gets the current custom data config for a `BitmovinPlayerCollector` instance. - - Parameter nativeId: Native Id of the the collector instance. - - Parameter playerId: Native Id of the player instance. - - Parameter resolver: JS promise resolver. - - Parameter rejecter: JS promise rejecter. - */ - @objc(getCustomData:playerId:resolver:rejecter:) - func getCustomData( - _ nativeId: NativeId, - playerId: NativeId?, - resolver resolve: @escaping RCTPromiseResolveBlock, - rejecter reject: @escaping RCTPromiseRejectBlock - ) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - guard - let collector = self?.collectors[nativeId], - let playerId = playerId, - let player = self?.bridge[PlayerModule.self]?.retrieve(playerId), - let source = player.source, - let customData = RCTConvert.toJson(analyticsCustomData: collector.customData(for: source)) - else { - reject("[AnalyticsModule]", "Could not find analytics collector with ID (\(nativeId))", nil) - return - } - resolve(customData) + playerAnalytics.sendCustomDataEvent(customData: customData) } } /** Gets the current user Id for a `BitmovinPlayerCollector` instance. - - Parameter nativeId: Native Id of the the collector instance. + - Parameter playerId: Native Id of the the player instance. - Parameter resolver: JS promise resolver. - Parameter rejecter: JS promise rejecter. */ @objc(getUserId:resolver:rejecter:) func getUserId( - _ nativeId: NativeId, + _ playerId: NativeId, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock ) { bridge.uiManager.addUIBlock { [weak self] _, _ in - guard let collector = self?.collectors[nativeId] else { - reject("[AnalyticsModule]", "Could not find analytics collector with ID (\(nativeId))", nil) - return - } - resolve(collector.getUserId()) - } - } - - /** - Applies the source metadata for the current source via the `BitmovinPlayerCollector` instance. - - Parameter nativeId: Native Id of the collector instance. - - Parameter playerId: Native Id of the player instance. - - Parameter json: Custom data config json. - */ - @objc(addSourceMetadata:playerId:json:) - func addSourceMetadata( - _ nativeId: NativeId, - playerId: NativeId?, - json: Any? - ) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - guard - let collector = self?.collectors[nativeId], - let sourceMetadata = RCTConvert.analyticsSourceMetadata(json), - let playerId = playerId, - let player = self?.bridge[PlayerModule.self]?.retrieve(playerId), - let source = player.source - else { + guard let playerAnalytics = self?.playerModule?.retrieve(playerId)?.analytics else { + reject("[AnalyticsModule]", "Could not find player with ID (\(playerId))", nil) return } - collector.apply(sourceMetadata: sourceMetadata, for: source) + resolve(playerAnalytics.userId) } } } diff --git a/ios/PlayerModule.m b/ios/PlayerModule.m index 1d70a2e9..131923df 100644 --- a/ios/PlayerModule.m +++ b/ios/PlayerModule.m @@ -3,6 +3,7 @@ @interface RCT_EXTERN_REMAP_MODULE(PlayerModule, PlayerModule, NSObject) RCT_EXTERN_METHOD(initWithConfig:(NSString *)nativeId config:(nullable id)config) +RCT_EXTERN_METHOD(initWithAnalyticsConfig:(NSString *)nativeId config:(nullable id)config analyticsConfig:(nullable id)analyticsConfig) RCT_EXTERN_METHOD(loadSource:(NSString *)nativeId sourceNativeId:(NSString *)sourceNativeId) RCT_EXTERN_METHOD(loadOfflineContent:(NSString *)nativeId offlineContentManagerBridgeId:(NSString *)offlineContentManagerBridgeId options:(nullable id)options) RCT_EXTERN_METHOD(unload:(NSString *)nativeId) diff --git a/ios/PlayerModule.swift b/ios/PlayerModule.swift index a30f515b..895f7afd 100644 --- a/ios/PlayerModule.swift +++ b/ios/PlayerModule.swift @@ -48,6 +48,31 @@ class PlayerModule: NSObject, RCTBridgeModule { self?.players[nativeId] = PlayerFactory.create(playerConfig: playerConfig) } } + + /** + Creates a new analytics enabled `Player` instance inside the internal players using the provided `config` and `analyticsConfig` object. + - Parameter config: `PlayerConfig` object received from JS. + - Parameter analyticsConfig: `AnalyticsConfig` object received from JS. + */ + @objc(initWithAnalyticsConfig:config:analyticsConfig:) + func initWithAnalyticsConfig(_ nativeId: NativeId, config: Any?, analyticsConfig: Any?) { + bridge.uiManager.addUIBlock { [weak self] _, _ in + let analyticsConfigJson = analyticsConfig + guard + self?.players[nativeId] == nil, + let playerConfig = RCTConvert.playerConfig(config), + let analyticsConfig = RCTConvert.analyticsConfig(analyticsConfig) + else { + return + } + let defaultMetadata = RCTConvert.analyticsDefaultMetadataFromAnalyticsConfig(analyticsConfigJson) + self?.players[nativeId] = PlayerFactory.create( + playerConfig: playerConfig, + analyticsConfig: analyticsConfig, + defaultMetadata: defaultMetadata ?? DefaultMetadata() + ) + } + } /** Loads the given source configuration into `nativeId`'s `Player` object. diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index bf4d08ba..8201ddee 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -636,79 +636,41 @@ extension RCTConvert { - Parameter json: JS object. - Returns: The associated `BitmovinAnalyticsConfig` value or nil. */ - static func analyticsConfig(_ json: Any?) -> BitmovinAnalyticsConfig? { + static func analyticsConfig(_ json: Any?) -> AnalyticsConfig? { guard let json = json as? [String: Any?], - let key = json["key"] as? String + let key = json["licenseKey"] as? String else { return nil } - let config: BitmovinAnalyticsConfig - if let playerKey = json["playerKey"] as? String { - config = BitmovinAnalyticsConfig(key: key, playerKey: playerKey) - } else { - config = BitmovinAnalyticsConfig(key: key) - } - if let cdnProvider = json["cdnProvider"] as? String { - config.cdnProvider = cdnProvider - } - if let customerUserId = json["customUserId"] as? String { - config.customerUserId = customerUserId - } - if let experimentName = json["experimentName"] as? String { - config.experimentName = experimentName - } - if let videoId = json["videoId"] as? String { - config.videoId = videoId - } - if let title = json["title"] as? String { - config.title = title - } - if let path = json["path"] as? String { - config.path = path - } - if let isLive = json["isLive"] as? Bool { - config.isLive = isLive - } - if let ads = json["ads"] as? Bool { - config.ads = ads - } - if let randomizeUserId = json["randomizeUserId"] as? Bool { - config.randomizeUserId = randomizeUserId - } - config.customData1 = json["customData1"] as? String - config.customData2 = json["customData2"] as? String - config.customData3 = json["customData3"] as? String - config.customData4 = json["customData4"] as? String - config.customData5 = json["customData5"] as? String - config.customData6 = json["customData6"] as? String - config.customData7 = json["customData7"] as? String - config.customData8 = json["customData8"] as? String - config.customData9 = json["customData9"] as? String - config.customData10 = json["customData10"] as? String - config.customData11 = json["customData11"] as? String - config.customData12 = json["customData12"] as? String - config.customData13 = json["customData13"] as? String - config.customData14 = json["customData14"] as? String - config.customData15 = json["customData15"] as? String - config.customData16 = json["customData16"] as? String - config.customData17 = json["customData17"] as? String - config.customData18 = json["customData18"] as? String - config.customData19 = json["customData19"] as? String - config.customData20 = json["customData20"] as? String - config.customData21 = json["customData21"] as? String - config.customData22 = json["customData22"] as? String - config.customData23 = json["customData23"] as? String - config.customData24 = json["customData24"] as? String - config.customData25 = json["customData25"] as? String - config.customData26 = json["customData26"] as? String - config.customData27 = json["customData27"] as? String - config.customData28 = json["customData28"] as? String - config.customData29 = json["customData29"] as? String - config.customData30 = json["customData30"] as? String - config.experimentName = json["experimentName"] as? String + let randomizeUserId = json["randomizeUserId"] as? Bool + let adTrackingDisabled = json["adTrackingDisabled"] as? Bool + + let config = AnalyticsConfig( + licenseKey: key, + randomizeUserId: randomizeUserId ?? false, + adTrackingDisabled: adTrackingDisabled ?? false + ) return config } + + static func analyticsDefaultMetadataFromAnalyticsConfig(_ json: Any?) -> DefaultMetadata? { + guard + let analyticsConfigJson = json as? [String: Any?], + let json = analyticsConfigJson["defaultMetadata"] as? [String: Any?] + else { + return nil + } + let cdnProvider = json["cdnProvider"] as? String + let customUserId = json["customUserId"] as? String + let customData = analyticsCustomData(json) + + return DefaultMetadata( + cdnProvider: cdnProvider, + customUserId: customUserId, + customData: customData ?? CustomData() + ) + } /** Utility method to get an analytics `CustomData` value from a JS object. diff --git a/ios/SourceModule.m b/ios/SourceModule.m index a876c77a..0f07e7eb 100644 --- a/ios/SourceModule.m +++ b/ios/SourceModule.m @@ -2,8 +2,8 @@ @interface RCT_EXTERN_REMAP_MODULE(SourceModule, SourceModule, NSObject) -RCT_EXTERN_METHOD(initWithConfig:(NSString *)nativeId config:(nullable id)config) -RCT_EXTERN_METHOD(initWithDrmConfig:(NSString *)nativeId drmNativeId:(NSString *)drmNativeId config:(nullable id)config) +RCT_EXTERN_METHOD(initWithConfig:(NSString *)nativeId drmNativeId:(NSString *)drmNativeId config:(nullable id)config) +RCT_EXTERN_METHOD(initWithAnalyticsConfig:(NSString *)nativeId drmNativeId:(NSString *)drmNativeId config:(nullable id)config analyticsSourceMetadata:(nullable id)analyticsSourceMetadata) RCT_EXTERN_METHOD(destroy:(NSString *)nativeId) RCT_EXTERN_METHOD( isAttachedToPlayer:(NSString *)nativeId diff --git a/ios/SourceModule.swift b/ios/SourceModule.swift index 2e4a1990..54737553 100644 --- a/ios/SourceModule.swift +++ b/ios/SourceModule.swift @@ -33,20 +33,25 @@ class SourceModule: NSObject, RCTBridgeModule { } /** - Creates a new `Source` instance inside the internal sources using the provided `config` object. + Creates a new `Source` instance inside the internal sources using the provided `config` and `analyticsSourceMetadata` object and an optionally initialized DRM configuration ID. - Parameter nativeId: ID to be associated with the `Source` instance. + - Parameter drmNativeId: ID of the DRM config object to use. - Parameter config: `SourceConfig` object received from JS. + - Parameter analyticsSourceMetadata: `SourceMetadata` object received from JS. */ - @objc(initWithConfig:config:) - func initWithConfig(_ nativeId: NativeId, config: Any?) { + @objc(initWithAnalyticsConfig:drmNativeId:config:analyticsSourceMetadata:) + func initWithAnalyticsConfig(_ nativeId: NativeId, drmNativeId: NativeId?, config: Any?, analyticsSourceMetadata: Any?) { bridge.uiManager.addUIBlock { [weak self] _, _ in + let fairplayConfig = drmNativeId != nil ? self?.getDrmModule()?.retrieve(drmNativeId!) : nil + guard self?.sources[nativeId] == nil, - let sourceConfig = RCTConvert.sourceConfig(config) + let sourceConfig = RCTConvert.sourceConfig(config, drmConfig: fairplayConfig), + let sourceMetadata = RCTConvert.analyticsSourceMetadata(analyticsSourceMetadata) else { return } - self?.sources[nativeId] = SourceFactory.create(from: sourceConfig) + self?.sources[nativeId] = SourceFactory.create(from: sourceConfig, sourceMetadata: sourceMetadata) } } @@ -56,12 +61,12 @@ class SourceModule: NSObject, RCTBridgeModule { - Parameter drmNativeId: ID of the DRM config object to use. - Parameter config: `SourceConfig` object received from JS. */ - @objc(initWithDrmConfig:drmNativeId:config:) - func initWithDrmConfig(_ nativeId: NativeId, drmNativeId: NativeId, config: Any?) { + @objc(initWithConfig:drmNativeId:config:) + func initWithConfig(_ nativeId: NativeId, drmNativeId: NativeId?, config: Any?) { bridge.uiManager.addUIBlock { [weak self] _, _ in guard self?.sources[nativeId] == nil, - let fairplayConfig = self?.getDrmModule()?.retrieve(drmNativeId), + let fairplayConfig = drmNativeId != nil ? self?.getDrmModule()?.retrieve(drmNativeId!) : nil, let sourceConfig = RCTConvert.sourceConfig(config, drmConfig: fairplayConfig) else { return diff --git a/src/analytics/collector.ts b/src/analytics/collector.ts deleted file mode 100644 index 9f0b31f1..00000000 --- a/src/analytics/collector.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { NativeModules } from 'react-native'; -import NativeInstance from '../nativeInstance'; -import { AnalyticsConfig, CustomDataConfig, SourceMetadata } from './config'; - -const AnalyticsModule = NativeModules.AnalyticsModule; - -/** - * Analytics collector that can be attached to a player object in order to collect and send - * its analytics information. - */ -export class AnalyticsCollector extends NativeInstance { - /** - * Whether the native `AnalyticsCollector` object has been created. - */ - isInitialized = false; - - /** - * The native player id that this analytics collector is attached to. - */ - playerId?: string; - - /** - * Whether the native `AnalyticsCollector` object has been disposed. - */ - isDestroyed = false; - - /** - * Initializes a native `BitmovinPlayerCollector` object. - */ - initialize = () => { - if (!this.isInitialized) { - AnalyticsModule.initWithConfig(this.nativeId, this.config); - this.isInitialized = true; - } - }; - - /** - * Disposes the native `BitmovinPlayerCollector` object that has been created - * during initialization. - */ - destroy = () => { - if (!this.isDestroyed) { - AnalyticsModule.destroy(this.nativeId); - this.isDestroyed = true; - this.playerId = undefined; - } - }; - - /** - * Attach a player instance to this analytics plugin. After this is completed, BitmovinAnalytics - * will start monitoring and sending analytics data based on the attached player instance. - * - * @param playerId - Native Id of the player to attach this collector instance. - */ - attach = (playerId: string): void => { - this.playerId = playerId; - AnalyticsModule.attach(this.nativeId, playerId); - }; - - /** - * Detach a player instance from this analytics plugin if there's any attached. If no player is attached, - * nothing happens. - */ - detach = (): void => { - this.playerId = undefined; - AnalyticsModule.detach(this.nativeId); - }; - - /** - * Dynamically updates analytics custom data information. Use this method - * to update your custom data during runtime. - * - * @param customData - Analytics custom data config. - * @deprecated - */ - setCustomDataOnce = (customData: CustomDataConfig) => { - AnalyticsModule.setCustomDataOnce(this.nativeId, customData); - }; - - /** - * Sets the internal analytics custom data state. - * - * @param customData - Analytics custom data config. - */ - setCustomData = (customData: CustomDataConfig) => { - AnalyticsModule.setCustomData(this.nativeId, this.playerId, customData); - }; - - /** - * Gets the current custom data config from the native `BitmovinPlayerCollector` instance. - * - * @returns The current custom data config. - */ - getCustomData = async (): Promise => { - return AnalyticsModule.getCustomData(this.nativeId, this.playerId); - }; - - /** - * Gets the current user id used by the native `BitmovinPlayerCollector` instance. - * - * @returns The current user id. - */ - getUserId = async (): Promise => { - return AnalyticsModule.getUserId(this.nativeId); - }; - - /** - * Sets the source metadata for the current source loaded into the player. - * This method should be called every time a new source is loaded into the player to ensure - * that the analytics data is correct. - * @param sourceMetadata - Source metadata to set. - */ - setSourceMetadata = (sourceMetadata: SourceMetadata) => { - return AnalyticsModule.setSourceMetadata( - this.nativeId, - this.playerId, - sourceMetadata - ); - }; - - /** - * Adds source metadata for the current source loaded into the player. - * This method should be called every time a new source is loaded into the player to ensure - * that the analytics data is correct. - * @param sourceMetadata - Source metadata to set. - * @deprecated - */ - addSourceMetadata = (sourceMetadata: SourceMetadata) => { - return AnalyticsModule.addSourceMetadata( - this.nativeId, - this.playerId, - sourceMetadata - ); - }; -} diff --git a/src/analytics/config.ts b/src/analytics/config.ts index 71b83914..14000a96 100644 --- a/src/analytics/config.ts +++ b/src/analytics/config.ts @@ -1,46 +1,77 @@ -import { NativeInstanceConfig } from '../nativeInstance'; - /** - * Available cdn provider options for AnalyticsConfig. + * Object used to configure the build-in analytics collector. */ -export enum CdnProvider { - BITMOVIN = 'bitmovin', - AKAMAI = 'akamai', - FASTLY = 'fastly', - MAXCDN = 'maxcdn', - CLOUDFRONT = 'cloudfront', - CHINACACHE = 'chinacache', - BITGRAVITY = 'bitgravity', +export interface AnalyticsConfig { + /** + * The analytics license key + */ + licenseKey: string; + /** + * Flag to enable Ad tracking (default: false). + */ + adTrackingDisabled?: boolean; + /** + * Flag to use randomised userId not depending on device specific values (default: false). + */ + randomizeUserId?: boolean; + /** + * Default metadata to be sent with events. + * Fields of the `SourceMetadata` are prioritized over the default metadata. + */ + defaultMetadata?: DefaultMetadata; } /** - * Object used to configure a new `AnalyticsCollector` instance. + * DefaultMetadata that can be used to enrich the analytics data. + * DefaultMetadata is not bound to a specific source and can be used to set fields for the lifecycle of the collector. + * If fields are specified in `SourceMetadata` and `DefaultMetadata`, `SourceMetadata` takes precedence. */ -export interface AnalyticsConfig - extends NativeInstanceConfig, - CustomDataConfig { +export interface DefaultMetadata extends CustomDataConfig { /** * CDN Provide that the video playback session is using. */ - cdnProvider?: CdnProvider; + cdnProvider?: string; /** * User ID of the customer. */ customUserId?: string; +} + +/** + * `SourceMetadata` that can be used to enrich the analytics data. + */ +export interface SourceMetadata extends CustomDataConfig { /** - * The analytics license key + * ID of the video in the CMS system */ - licenseKey: string; + videoId?: String; + /** - * Flag to enable Ad tracking (default: false). + * Human readable title of the video asset currently playing */ - adTrackingDisabled?: boolean; + title?: String; + /** - * Flag to use randomised userId not depending on device specific values (default: false). + * Breadcrumb path to show where in the app the user is */ - randomizeUserId?: boolean; + path?: String; + + /** + * Flag to see if stream is live before stream metadata is available + */ + isLive?: boolean; + + /** + * CDN Provider that the video playback session is using + */ + cdnProvider?: String; } +/** + * Free-form data that can be used to enrich the analytics data + * If customData is specified in `SourceMetadata` and `DefaultMetadata` + * data is merged on a field basis with `SourceMetadata` taking precedence. + */ export interface CustomDataConfig { /** * Optional free-form custom data @@ -197,30 +228,3 @@ export interface CustomDataConfig { */ experimentName?: string; } - -export interface SourceMetadata extends CustomDataConfig { - /** - * ID of the video in the CMS system - */ - videoId?: String; - - /** - * Human readable title of the video asset currently playing - */ - title?: String; - - /** - * Breadcrumb path to show where in the app the user is - */ - path?: String; - - /** - * Flag to see if stream is live before stream metadata is available - */ - isLive?: boolean; - - /** - * CDN Provider that the video playback session is using - */ - cdnProvider?: String; -} diff --git a/src/analytics/index.ts b/src/analytics/index.ts index de864ce2..08b0a11b 100644 --- a/src/analytics/index.ts +++ b/src/analytics/index.ts @@ -1,2 +1,2 @@ export * from './config'; -export * from './collector'; +export * from './player'; diff --git a/src/analytics/player.ts b/src/analytics/player.ts new file mode 100644 index 00000000..d20b7152 --- /dev/null +++ b/src/analytics/player.ts @@ -0,0 +1,36 @@ +import { NativeModules } from 'react-native'; +import { CustomDataConfig } from './config'; + +const AnalyticsModule = NativeModules.AnalyticsModule; + +/** + * Provides the means to control the analytics collected by a `Player`. + * Use the `Player.analytics` property to access a `Player`'s `AnalyticsApi`. + */ +export class AnalyticsApi { + /** + * The native player id that this analytics api is attached to. + */ + playerId: string; + + constructor(playerId: string) { + this.playerId = playerId; + } + + /** + * Sends a sample with the provided custom data. + * Does not change the configured custom data of the collector or source. + */ + sendCustomDataEvent = (customData: CustomDataConfig) => { + AnalyticsModule.sendCustomDataEvent(this.playerId, customData); + }; + + /** + * Gets the current user id used by the bundled analytics instance. + * + * @returns The current user id. + */ + getUserId = async (): Promise => { + return AnalyticsModule.getUserId(this.playerId); + }; +} diff --git a/src/player.ts b/src/player.ts index d865f1d3..7b9223fe 100644 --- a/src/player.ts +++ b/src/player.ts @@ -1,6 +1,6 @@ import { NativeModules, Platform } from 'react-native'; import { AdItem, AdvertisingConfig } from './advertising'; -import { AnalyticsCollector, AnalyticsConfig } from './analytics'; +import { AnalyticsConfig } from './analytics'; import NativeInstance, { NativeInstanceConfig } from './nativeInstance'; import { Source, SourceConfig } from './source'; import { AudioTrack } from './audioTrack'; @@ -10,6 +10,7 @@ import { TweaksConfig } from './tweaksConfig'; import { AdaptationConfig } from './adaptationConfig'; import { OfflineContentManager, OfflineSourceOptions } from './offline'; import { Thumbnail } from './thumbnail'; +import { AnalyticsApi } from './analytics/player'; const PlayerModule = NativeModules.PlayerModule; @@ -153,10 +154,6 @@ export class Player extends NativeInstance { * Currently active source, or `null` if none is active. */ source?: Source; - /** - * Analytics collector currently attached to this player instance. - */ - analyticsCollector?: AnalyticsCollector; /** * Whether the native `Player` object has been created. */ @@ -165,6 +162,12 @@ export class Player extends NativeInstance { * Whether the native `Player` object has been disposed. */ isDestroyed = false; + /** + * The `AnalyticsApi` for interactions regarding the `Player`'s analytics. + * + * `undefined` if the player was created without analytics support. + */ + analytics?: AnalyticsApi = undefined; /** * Allocates the native `Player` instance and its resources natively. @@ -173,11 +176,12 @@ export class Player extends NativeInstance { if (!this.isInitialized) { const analyticsConfig = this.config?.analyticsConfig; if (analyticsConfig) { - PlayerModule.initWithConfig( + PlayerModule.initWithAnalyticsConfig( this.nativeId, this.config, analyticsConfig ); + this.analytics = new AnalyticsApi(this.nativeId); } else { PlayerModule.initWithConfig(this.nativeId, this.config); } @@ -192,7 +196,6 @@ export class Player extends NativeInstance { if (!this.isDestroyed) { PlayerModule.destroy(this.nativeId); this.source?.destroy(); - this.analyticsCollector?.destroy(); this.isDestroyed = true; } }; diff --git a/src/source.ts b/src/source.ts index 81dd86dc..156ade8f 100644 --- a/src/source.ts +++ b/src/source.ts @@ -3,6 +3,7 @@ import { Drm, DrmConfig } from './drm'; import NativeInstance, { NativeInstanceConfig } from './nativeInstance'; import { SideLoadedSubtitleTrack } from './subtitleTrack'; import { Thumbnail } from './thumbnail'; +import { SourceMetadata } from './analytics'; const SourceModule = NativeModules.SourceModule; @@ -129,6 +130,10 @@ export interface SourceConfig extends NativeInstanceConfig { * The `SourceOptions` for this configuration. */ options?: SourceOptions; + /** + * The `SourceMetadata` for the `Source` to setup custom analytics tracking + */ + analyticsSourceMetadata?: SourceMetadata; } /** @@ -153,16 +158,24 @@ export class Source extends NativeInstance { */ initialize = () => { if (!this.isInitialized) { + const sourceMetadata = this.config?.analyticsSourceMetadata; if (this.config?.drmConfig) { this.drm = new Drm(this.config.drmConfig); this.drm.initialize(); - SourceModule.initWithDrmConfig( + } + if (sourceMetadata) { + SourceModule.initWithAnalyticsConfig( this.nativeId, - this.drm.nativeId, - this.config + this.drm?.nativeId, + this.config, + sourceMetadata ); } else { - SourceModule.initWithConfig(this.nativeId, this.config); + SourceModule.initWithConfig( + this.nativeId, + this.drm?.nativeId, + this.config + ); } this.isInitialized = true; }