Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-buckets-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@theoplayer/react-native-analytics-bitmovin': minor
---

Added `programChange` method to allow passing new source metadata during a live stream.
5 changes: 5 additions & 0 deletions .changeset/vast-tires-wish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@theoplayer/react-native-analytics-bitmovin': minor
---

Added support for Web platforms.
9 changes: 8 additions & 1 deletion apps/e2e/src/components/menu/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 16 additions & 5 deletions bitmovin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -38,8 +47,8 @@ const defaultMetadata: DefaultMetadata = {
cdnProvider: 'akamai',
customUserId: 'custom-user-id-1234',
customData: {
customData0: 'value0',
customData1: 'value1'
customData1: 'value1',
customData2: 'value2'
}
};

Expand All @@ -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 (<THEOplayerView config={playerConfig} onPlayerReady={onPlayerReady}/>);
Expand Down
2 changes: 1 addition & 1 deletion bitmovin/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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
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"
Expand All @@ -25,6 +27,8 @@ private const val PROP_CUSTOM_USER_ID = "customUserId"

object BitmovinAdapter {

private const val TAG = "BitmovinAdapter"

/**
* Create an AnalyticsConfig object.
*
Expand Down Expand Up @@ -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)) {
Expand All @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SourceChangeEvent> { 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<String>("title") ?: sourceMetadata?.title)
setVideoId(metadata.get<String>("videoId") ?: sourceMetadata?.videoId)
setPath(metadata.get<String>("path") ?: sourceMetadata?.path)
setCdnProvider(metadata.get<String>("cdnProvider") ?: sourceMetadata?.cdnProvider)
setIsLive(metadata.get<Boolean>("isLive") ?: sourceMetadata?.isLive)
val customData = (metadata.get<Any>("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)
}
}
Original file line number Diff line number Diff line change
@@ -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<Int, ITHEOplayerCollector> = HashMap()
private var bitmovinConnectors: HashMap<Int, BitmovinHandler> = HashMap()

override fun getName(): String {
return TAG
Expand All @@ -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()
}
}
}
27 changes: 19 additions & 8 deletions bitmovin/ios/Connector/BitmovinAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
}

Expand Down
Loading
Loading