diff --git a/CHANGELOG.md b/CHANGELOG.md index dcf0bd20..ec0a1aa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ # Changelog -## [Unreleased] +## [0.14.0] (2023-11-14) ### Added -- `LiveConfig.minTimeshiftBufferDepth` to control the minimum buffer depth of a stream needed to enable time shifting. +- `LiveConfig.minTimeshiftBufferDepth` to control the minimum buffer depth of a stream needed to enable time shifting - `Player.buffer` to control buffer preferences and to query the current buffer state - `DownloadFinishedEvent` to signal when the download of specific content has finished - `Player.videoQuality`, `Player.availableVideoQualities`, and `VideoDownloadQualityChangedEvent` to query current video qualities and listen to related changes diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt new file mode 100644 index 00000000..a5157b73 --- /dev/null +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -0,0 +1,106 @@ +package com.bitmovin.player.reactnative + +import android.util.Log +import com.bitmovin.player.api.Player +import com.bitmovin.player.api.source.Source +import com.bitmovin.player.reactnative.extensions.drmModule +import com.bitmovin.player.reactnative.extensions.offlineModule +import com.bitmovin.player.reactnative.extensions.playerModule +import com.bitmovin.player.reactnative.extensions.sourceModule +import com.bitmovin.player.reactnative.extensions.uiManagerModule +import com.facebook.react.bridge.* +import com.facebook.react.uimanager.UIManagerModule + +private const val MODULE_NAME = "BitmovinBaseModule" + +/** + * Base for Bitmovin React modules. + * + * Provides many helper methods that are promise exception safe. + * + * In general, code should not throw while resolving a [Promise]. Instead, [Promise.reject] should be used. + * This doesn't match Kotlin's error style, which uses exception. The helper methods in this class, provide such + * convenience, they can only be called in a context that will catch any Exception and reject the [Promise]. + * + */ +abstract class BitmovinBaseModule( + protected val context: ReactApplicationContext, +) : ReactContextBaseJavaModule(context) { + /** + * Runs [block] on the UI thread with [UIManagerModule.addUIBlock] and [TPromise.resolve] [this] with + * its return value. If [block] throws, [Promise.reject] [this] with the [Throwable]. + */ + protected inline fun TPromise.resolveOnUiThread( + crossinline block: RejectPromiseOnExceptionBlock.() -> R, + ) { + val uiManager = runAndRejectOnException { uiManager } ?: return + uiManager.addUIBlock { + resolveOnCurrentThread { block() } + } + } + + protected val RejectPromiseOnExceptionBlock.playerModule: PlayerModule get() = context.playerModule + ?: throw IllegalArgumentException("PlayerModule not found") + + protected val RejectPromiseOnExceptionBlock.uiManager: UIManagerModule get() = context.uiManagerModule + ?: throw IllegalStateException("UIManager not found") + + protected val RejectPromiseOnExceptionBlock.sourceModule: SourceModule get() = context.sourceModule + ?: throw IllegalStateException("SourceModule not found") + + protected val RejectPromiseOnExceptionBlock.offlineModule: OfflineModule get() = context.offlineModule + ?: throw IllegalStateException("OfflineModule not found") + + protected val RejectPromiseOnExceptionBlock.drmModule: DrmModule get() = context.drmModule + ?: throw IllegalStateException("DrmModule not found") + + fun RejectPromiseOnExceptionBlock.getPlayer( + nativeId: NativeId, + playerModule: PlayerModule = this.playerModule, + ): Player = playerModule.getPlayerOrNull(nativeId) ?: throw IllegalArgumentException("Invalid PlayerId $nativeId") + + fun RejectPromiseOnExceptionBlock.getSource( + nativeId: NativeId, + sourceModule: SourceModule = this.sourceModule, + ): Source = sourceModule.getSourceOrNull(nativeId) ?: throw IllegalArgumentException("Invalid SourceId $nativeId") +} + +/** Run [block], returning it's return value. If [block] throws, [Promise.reject] [this] and return null. */ +inline fun TPromise.runAndRejectOnException(block: RejectPromiseOnExceptionBlock.() -> R): R? = try { + RejectPromiseOnExceptionBlock.block() +} catch (e: Exception) { + reject(e) + null +} + +/** + * [TPromise.resolve] [this] with [block] return value. + * If [block] throws, [Promise.reject] [this] with the [Throwable]. + */ +inline fun TPromise.resolveOnCurrentThread( + crossinline block: RejectPromiseOnExceptionBlock.() -> T, +): Unit = runAndRejectOnException { this@resolveOnCurrentThread.resolve(block()) } ?: Unit + +/** Receiver of code that can safely throw when resolving a [Promise]. */ +object RejectPromiseOnExceptionBlock + +/** Compile time wrapper for Promises to type check the resolved type [T]. */ +@JvmInline +value class TPromise(val promise: Promise) { + // Promise only support built-in types. Functions that return [Unit] must resolve to `null`. + fun resolve(value: T): Unit = promise.resolve(value.takeUnless { it is Unit }) + fun reject(throwable: Throwable) { + Log.e(MODULE_NAME, "Failed to execute Bitmovin method", throwable) + promise.reject(throwable) + } +} + +inline val Promise.int get() = TPromise(this) +inline val Promise.unit get() = TPromise(this) +inline val Promise.string get() = TPromise(this) +inline val Promise.double get() = TPromise(this) +inline val Promise.float get() = TPromise(this) +inline val Promise.bool get() = TPromise(this) +inline val Promise.map get() = TPromise(this) +inline val Promise.array get() = TPromise(this) +inline val TPromise.nullable get() = TPromise(promise) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt index 3ccc9e1d..6d9cfbd4 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt @@ -1,70 +1,54 @@ package com.bitmovin.player.reactnative import com.bitmovin.player.casting.BitmovinCastManager -import com.bitmovin.player.reactnative.converter.JsonConverter +import com.bitmovin.player.reactnative.converter.toCastOptions 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 private const val MODULE_NAME = "BitmovinCastManagerModule" @ReactModule(name = MODULE_NAME) -class BitmovinCastManagerModule( - private val context: ReactApplicationContext, -) : ReactContextBaseJavaModule(context) { +class BitmovinCastManagerModule(context: ReactApplicationContext) : BitmovinBaseModule(context) { override fun getName() = MODULE_NAME /** * Returns whether the [BitmovinCastManager] is initialized. */ @ReactMethod - fun isInitialized(promise: Promise) = uiManager?.addUIBlock { - promise.resolve(BitmovinCastManager.isInitialized()) + fun isInitialized(promise: Promise) = promise.unit.resolveOnUiThread { + BitmovinCastManager.isInitialized() } /** * Initializes the [BitmovinCastManager] with the given options. */ @ReactMethod - fun initializeCastManager(options: ReadableMap?, promise: Promise) { - val castOptions = JsonConverter.toCastOptions(options) - uiManager?.addUIBlock { - BitmovinCastManager.initialize( - castOptions?.applicationId, - castOptions?.messageNamespace, - ) - promise.resolve(null) - } + fun initializeCastManager(options: ReadableMap?, promise: Promise) = promise.unit.resolveOnUiThread { + val castOptions = options?.toCastOptions() + BitmovinCastManager.initialize( + castOptions?.applicationId, + castOptions?.messageNamespace, + ) } /** * Sends a message to the receiver. */ @ReactMethod - fun sendMessage(message: String, messageNamespace: String?, promise: Promise) { - uiManager?.addUIBlock { - BitmovinCastManager.getInstance().sendMessage(message, messageNamespace) - promise.resolve(null) - } + fun sendMessage(message: String, messageNamespace: String?, promise: Promise) = promise.unit.resolveOnUiThread { + BitmovinCastManager.getInstance().sendMessage(message, messageNamespace) } /** * Updates the context of the [BitmovinCastManager] to the current activity. */ @ReactMethod - fun updateContext(promise: Promise) { - uiManager?.addUIBlock { - BitmovinCastManager.getInstance().updateContext(currentActivity) - promise.resolve(null) - } + fun updateContext(promise: Promise) = promise.unit.resolveOnUiThread { + BitmovinCastManager.getInstance().updateContext(currentActivity) } - - private val uiManager: UIManagerModule? - get() = context.getNativeModule(UIManagerModule::class.java) } /** diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt index 88b48d0b..a106ef10 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt @@ -2,66 +2,50 @@ package com.bitmovin.player.reactnative import com.bitmovin.player.api.buffer.BufferLevel import com.bitmovin.player.api.media.MediaType -import com.bitmovin.player.reactnative.converter.JsonConverter +import com.bitmovin.player.reactnative.converter.toBufferType +import com.bitmovin.player.reactnative.converter.toJson import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.uimanager.UIManagerModule private const val MODULE_NAME = "BufferModule" +private const val INVALID_BUFFER_TYPE = "Invalid buffer type" @ReactModule(name = MODULE_NAME) -class BufferModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { +class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(context) { override fun getName() = MODULE_NAME /** * Gets the [BufferLevel] from the Player * @param nativeId Target player id. - * @param type The [type of buffer][JsonConverter.toBufferType] to return the level for. + * @param type The [type of buffer][toBufferType] to return the level for. * @param promise JS promise object. */ @ReactMethod fun getLevel(nativeId: NativeId, type: String, promise: Promise) { - uiManager()?.addUIBlock { _ -> - val player = playerModule()?.getPlayer(nativeId) ?: return@addUIBlock - val bufferType = JsonConverter.toBufferType(type) - if (bufferType == null) { - promise.reject("Error: ", "Invalid buffer type") - return@addUIBlock - } - val bufferLevels = RNBufferLevels( - player.buffer.getLevel(bufferType, MediaType.Audio), - player.buffer.getLevel(bufferType, MediaType.Video), - ) - JsonConverter.fromRNBufferLevels(bufferLevels).let { - promise.resolve(it) - } + promise.map.resolveOnUiThread { + val player = getPlayer(nativeId) + val bufferType = type.toBufferTypeOrThrow() + RNBufferLevels( + audio = player.buffer.getLevel(bufferType, MediaType.Audio), + video = player.buffer.getLevel(bufferType, MediaType.Video), + ).toJson() } } /** * Sets the target buffer level for the chosen buffer type across all media types. * @param nativeId Target player id. - * @param type The [type of buffer][JsonConverter.toBufferType] to set the target level for. + * @param type The [type of buffer][toBufferType] to set the target level for. * @param value The value to set. */ @ReactMethod - fun setTargetLevel(nativeId: NativeId, type: String, value: Double) { - uiManager()?.addUIBlock { _ -> - val player = playerModule()?.getPlayer(nativeId) ?: return@addUIBlock - val bufferType = JsonConverter.toBufferType(type) ?: return@addUIBlock - player.buffer.setTargetLevel(bufferType, value) + fun setTargetLevel(nativeId: NativeId, type: String, value: Double, promise: Promise) { + promise.unit.resolveOnUiThread { + getPlayer(nativeId).buffer.setTargetLevel(type.toBufferTypeOrThrow(), value) } } - /** - * Helper function that gets the instantiated `UIManagerModule` from modules registry. - */ - private fun uiManager(): UIManagerModule? = context.getNativeModule(UIManagerModule::class.java) - - /** - * Helper function that gets the instantiated `PlayerModule` from modules registry. - */ - private fun playerModule(): PlayerModule? = context.getNativeModule(PlayerModule::class.java) + private fun String.toBufferTypeOrThrow() = toBufferType() ?: throw IllegalArgumentException(INVALID_BUFFER_TYPE) } /** diff --git a/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt index 2391807a..650df817 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt @@ -4,10 +4,10 @@ import android.util.Base64 import com.bitmovin.player.api.drm.PrepareLicenseCallback import com.bitmovin.player.api.drm.PrepareMessageCallback import com.bitmovin.player.api.drm.WidevineConfig -import com.bitmovin.player.reactnative.converter.JsonConverter +import com.bitmovin.player.reactnative.converter.toWidevineConfig import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.uimanager.UIManagerModule +import java.security.InvalidParameterException import java.util.concurrent.locks.Condition import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -20,7 +20,7 @@ typealias PrepareCallback = (ByteArray) -> ByteArray private const val MODULE_NAME = "DrmModule" @ReactModule(name = MODULE_NAME) -class DrmModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { +class DrmModule(context: ReactApplicationContext) : BitmovinBaseModule(context) { /** * In-memory mapping from `nativeId`s to `WidevineConfig` instances. */ @@ -74,15 +74,15 @@ class DrmModule(private val context: ReactApplicationContext) : ReactContextBase * @param config `DrmConfig` object received from JS. */ @ReactMethod - fun initWithConfig(nativeId: NativeId, config: ReadableMap?) { - uiManager()?.addUIBlock { - if (!drmConfigs.containsKey(nativeId) && config != null) { - JsonConverter.toWidevineConfig(config)?.let { - drmConfigs[nativeId] = it - initPrepareMessage(nativeId, config) - initPrepareLicense(nativeId, config) - } + fun initWithConfig(nativeId: NativeId, config: ReadableMap, promise: Promise) { + promise.unit.resolveOnUiThread { + if (drmConfigs.containsKey(nativeId)) { + throw InvalidParameterException("NativeId already exists $nativeId") } + val widevineConfig = config.toWidevineConfig() ?: throw InvalidParameterException("Invalid widevine config") + widevineConfig.prepareMessageCallback = buildPrepareMessageCallback(nativeId, config) + widevineConfig.prepareLicenseCallback = buildPrepareLicense(nativeId, config) + drmConfigs[nativeId] = widevineConfig } } @@ -118,24 +118,21 @@ class DrmModule(private val context: ReactApplicationContext) : ReactContextBase } /** - * Initialize the `prepareMessage` block in the `WidevineConfig` associated with `nativeId`. - * @param nativeId Instance ID. + * Initialize the `prepareMessage` block in the [widevineConfig] + * @param widevineConfig Instance ID. * @param config `DrmConfig` config object sent from JS. */ - private fun initPrepareMessage(nativeId: NativeId, config: ReadableMap) { - val widevineConfig = drmConfigs[nativeId] - val widevineJson = config.getMap("widevine") - if (widevineConfig != null && widevineJson != null && widevineJson.hasKey("prepareMessage")) { - val prepareMessage = createPrepareCallback( - nativeId, - "onPrepareMessage", - preparedMessages, - preparedMessagesCondition, - ) - widevineConfig.prepareMessageCallback = PrepareMessageCallback { - prepareMessage(it) - } + private fun buildPrepareMessageCallback(nativeId: NativeId, config: ReadableMap): PrepareMessageCallback? { + if (config.getMap("widevine")?.hasKey("prepareMessage") != true) { + return null } + val prepareMessageCallback = createPrepareCallback( + nativeId, + "onPrepareMessage", + preparedMessages, + preparedMessagesCondition, + ) + return PrepareMessageCallback(prepareMessageCallback) } /** @@ -143,20 +140,17 @@ class DrmModule(private val context: ReactApplicationContext) : ReactContextBase * @param nativeId Instance ID. * @param config `DrmConfig` config object sent from JS. */ - private fun initPrepareLicense(nativeId: NativeId, config: ReadableMap) { - val widevineConfig = drmConfigs[nativeId] - val widevineJson = config.getMap("widevine") - if (widevineConfig != null && widevineJson != null && widevineJson.hasKey("prepareLicense")) { - val prepareLicense = createPrepareCallback( - nativeId, - "onPrepareLicense", - preparedLicenses, - preparedLicensesCondition, - ) - widevineConfig.prepareLicenseCallback = PrepareLicenseCallback { - prepareLicense(it) - } + private fun buildPrepareLicense(nativeId: NativeId, config: ReadableMap): PrepareLicenseCallback? { + if (config.getMap("widevine")?.hasKey("prepareLicense") != true) { + return null } + val prepareLicense = createPrepareCallback( + nativeId, + "onPrepareLicense", + preparedLicenses, + preparedLicensesCondition, + ) + return PrepareLicenseCallback(prepareLicense) } /** @@ -181,10 +175,4 @@ class DrmModule(private val context: ReactApplicationContext) : ReactContextBase Base64.decode(result, Base64.NO_WRAP) } } - - /** - * Helper function that returns the initialized `UIManager` instance. - */ - private fun uiManager(): UIManagerModule? = - context.getNativeModule(UIManagerModule::class.java) } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt index 0a5bd44d..956e46be 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt @@ -1,18 +1,20 @@ package com.bitmovin.player.reactnative import com.bitmovin.player.api.offline.options.OfflineOptionEntryState -import com.bitmovin.player.reactnative.converter.JsonConverter -import com.bitmovin.player.reactnative.extensions.toList +import com.bitmovin.player.reactnative.converter.toSourceConfig +import com.bitmovin.player.reactnative.extensions.drmModule +import com.bitmovin.player.reactnative.extensions.getIntOrNull +import com.bitmovin.player.reactnative.extensions.getStringArray import com.bitmovin.player.reactnative.offline.OfflineContentManagerBridge import com.bitmovin.player.reactnative.offline.OfflineDownloadRequest import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.uimanager.UIManagerModule +import java.security.InvalidParameterException private const val OFFLINE_MODULE = "BitmovinOfflineModule" @ReactModule(name = OFFLINE_MODULE) -class OfflineModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { +class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(context) { /** * In-memory mapping from `nativeId`s to `OfflineManager` instances. @@ -26,15 +28,15 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext /** * Fetches the `OfflineManager` instance associated with `nativeId` from the internal offline managers. - * @param nativeId `OfflineManager` instance ID. - * @return The associated `OfflineManager` instance or `null`. */ - fun getOfflineContentManagerBridge(nativeId: NativeId?): OfflineContentManagerBridge? { - if (nativeId == null) { - return null - } - return offlineContentManagerBridges[nativeId] - } + fun getOfflineContentManagerBridgeOrNull( + nativeId: NativeId, + ): OfflineContentManagerBridge? = offlineContentManagerBridges[nativeId] + + private fun RejectPromiseOnExceptionBlock.getOfflineContentManagerBridge( + nativeId: NativeId, + ): OfflineContentManagerBridge = offlineContentManagerBridges[nativeId] + ?: throw IllegalArgumentException("No offline content manager bridge for id $nativeId") /** * Callback when a new NativeEventEmitter is created from the Typescript layer. @@ -58,33 +60,32 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun initWithConfig(nativeId: NativeId, config: ReadableMap?, drmNativeId: NativeId?, promise: Promise) { - uiManager()?.addUIBlock { - if (!offlineContentManagerBridges.containsKey(nativeId)) { - val identifier = config?.getString("identifier") - val sourceConfig = JsonConverter.toSourceConfig(config?.getMap("sourceConfig")) - sourceConfig?.drmConfig = drmModule()?.getConfig(drmNativeId) + promise.unit.resolveOnUiThread { + if (offlineContentManagerBridges.containsKey(nativeId)) { + throw InvalidParameterException("content manager bridge id already exists: $nativeId") + } + val identifier = config?.getString("identifier") + ?.takeIf { it.isNotEmpty() } ?: throw IllegalArgumentException("invalid identifier") - if (identifier.isNullOrEmpty() || sourceConfig == null) { - promise.reject(IllegalArgumentException("Identifier and SourceConfig may not be null")) - return@addUIBlock - } + val sourceConfig = config.getMap("sourceConfig")?.toSourceConfig() + ?: throw IllegalArgumentException("Invalid source config") - offlineContentManagerBridges[nativeId] = OfflineContentManagerBridge( - nativeId, - context, - identifier, - sourceConfig, - context.cacheDir.path, - ) - } - promise.resolve(null) + sourceConfig.drmConfig = context.drmModule?.getConfig(drmNativeId) + + offlineContentManagerBridges[nativeId] = OfflineContentManagerBridge( + nativeId, + context, + identifier, + sourceConfig, + context.cacheDir.path, + ) } } @ReactMethod fun getState(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { - promise.resolve(state.name) + promise.string.resolveWithBridge(nativeId) { + state.name } } @@ -95,9 +96,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun getOptions(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.unit.resolveWithBridge(nativeId) { getOptions() - promise.resolve(null) } } @@ -109,47 +109,23 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext * @param request `ReadableMap` that contains the `OfflineManager.OfflineOptionType`, id, and `OfflineOptionEntryAction` necessary to set the new action. */ @ReactMethod - fun download(nativeId: NativeId, request: ReadableMap?, promise: Promise) { - if (request == null) { - promise.reject(IllegalArgumentException("Request may not be null")) - return - } - - safeOfflineContentManager(nativeId, promise) { - try { - when (state) { - OfflineOptionEntryState.Downloaded -> { - promise.reject(IllegalStateException("Download already completed")) - return@safeOfflineContentManager - } - OfflineOptionEntryState.Downloading, - OfflineOptionEntryState.Failed, - -> { - promise.reject(IllegalStateException("Download already in progress")) - return@safeOfflineContentManager - } - OfflineOptionEntryState.Suspended -> { - promise.reject(IllegalStateException("Download is suspended")) - return@safeOfflineContentManager - } - else -> {} - } - val minimumBitRate = if (request.hasKey("minimumBitrate")) request.getInt("minimumBitrate") else null - if (minimumBitRate != null && minimumBitRate < 0) { - promise.reject(IllegalArgumentException("Invalid download request")) - return@safeOfflineContentManager - } - - val audioOptionIds = request.getArray("audioOptionIds")?.toList()?.filterNotNull() - val textOptionIds = request.getArray("textOptionIds")?.toList()?.filterNotNull() - - getOfflineContentManagerBridge(nativeId)?.process( - OfflineDownloadRequest(minimumBitRate, audioOptionIds, textOptionIds), + fun download(nativeId: NativeId, request: ReadableMap, promise: Promise) { + promise.unit.resolveWithBridge(nativeId) { + when (state) { + OfflineOptionEntryState.Downloaded -> throw IllegalStateException("Download already completed") + OfflineOptionEntryState.Downloading, OfflineOptionEntryState.Failed -> throw IllegalStateException( + "Download already in progress", ) - promise.resolve(null) - } catch (e: Exception) { - promise.reject(e) + OfflineOptionEntryState.Suspended -> throw IllegalStateException("Download is suspended") + else -> {} + } + val minimumBitRate = request.getIntOrNull("minimumBitrate")?.also { + if (it < 0) throw IllegalArgumentException("Invalid download request") } + val audioOptionIds = request.getStringArray("audioOptionIds")?.filterNotNull() + val textOptionIds = request.getStringArray("textOptionIds")?.filterNotNull() + + process(OfflineDownloadRequest(minimumBitRate, audioOptionIds, textOptionIds)) } } @@ -159,9 +135,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun resume(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.unit.resolveWithBridge(nativeId) { resume() - promise.resolve(null) } } @@ -171,9 +146,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun suspend(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.unit.resolveWithBridge(nativeId) { suspend() - promise.resolve(null) } } @@ -183,9 +157,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun cancelDownload(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.unit.resolveWithBridge(nativeId) { cancelDownload() - promise.resolve(null) } } @@ -195,8 +168,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun usedStorage(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { - promise.resolve(offlineContentManager.usedStorage.toDouble()) + promise.double.resolveWithBridge(nativeId) { + offlineContentManager.usedStorage.toDouble() } } @@ -206,9 +179,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun deleteAll(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.unit.resolveWithBridge(nativeId) { deleteAll() - promise.resolve(null) } } @@ -220,9 +192,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun downloadLicense(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.unit.resolveWithBridge(nativeId) { downloadLicense() - promise.resolve(null) } } @@ -234,9 +205,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun releaseLicense(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.unit.resolveWithBridge(nativeId) { releaseLicense() - promise.resolve(null) } } @@ -248,9 +218,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun renewOfflineLicense(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.unit.resolveWithBridge(nativeId) { renewOfflineLicense() - promise.resolve(null) } } @@ -262,29 +231,18 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun release(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.unit.resolveWithBridge(nativeId) { release() offlineContentManagerBridges.remove(nativeId) - promise.resolve(null) } } - private fun safeOfflineContentManager( + private inline fun TPromise.resolveWithBridge( nativeId: NativeId, - promise: Promise, - runBlock: OfflineContentManagerBridge.() -> Unit, + crossinline block: OfflineContentManagerBridge.() -> T, ) { - getOfflineContentManagerBridge(nativeId)?.let(runBlock) - ?: promise.reject(IllegalArgumentException("Could not find the offline module instance")) + resolveOnCurrentThread { + getOfflineContentManagerBridge(nativeId).block() + } } - - /** - * Helper function that returns the initialized `DrmModule` instance. - */ - private fun drmModule(): DrmModule? = context.getNativeModule(DrmModule::class.java) - - /** - * Helper function that returns the initialized `UIManager` instance. - */ - private fun uiManager(): UIManagerModule? = context.getNativeModule(UIManagerModule::class.java) } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt index f7e5f384..5d6de800 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt @@ -1,15 +1,15 @@ package com.bitmovin.player.reactnative +import com.bitmovin.player.api.analytics.AnalyticsApi import com.bitmovin.player.api.analytics.AnalyticsApi.Companion.analytics -import com.bitmovin.player.reactnative.converter.JsonConverter +import com.bitmovin.player.reactnative.converter.toAnalyticsCustomData import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.uimanager.UIManagerModule private const val MODULE_NAME = "PlayerAnalyticsModule" @ReactModule(name = MODULE_NAME) -class PlayerAnalyticsModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { +class PlayerAnalyticsModule(context: ReactApplicationContext) : BitmovinBaseModule(context) { /** * JS exported module name. */ @@ -18,41 +18,33 @@ class PlayerAnalyticsModule(private val context: ReactApplicationContext) : Reac /** * Sends a sample with the provided custom data. * Does not change the configured custom data of the collector or source. - * @param nativeId Native Id of the collector instance. + * @param playerId Native Id of the player instance. * @param json Custom data config json. */ @ReactMethod - fun sendCustomDataEvent(nativeId: NativeId, json: ReadableMap?) { - uiManager()?.addUIBlock { _ -> - JsonConverter.toAnalyticsCustomData(json)?.let { - playerModule()?.getPlayer(nativeId)?.analytics?.sendCustomDataEvent(it) - } + fun sendCustomDataEvent(playerId: NativeId, json: ReadableMap, promise: Promise) { + promise.unit.resolveOnUiThreadWithAnalytics(playerId) { + sendCustomDataEvent(json.toAnalyticsCustomData()) } } /** * Gets the current user Id for a player instance with analytics. - * @param nativeId Native Id of the the player instance. + * @param playerId Native Id of the the player instance. * @param promise JS promise object. */ @ReactMethod fun getUserId(playerId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { _ -> - playerModule()?.getPlayer(playerId)?.analytics?.let { - promise.resolve(it.userId) - } + promise.string.resolveOnUiThreadWithAnalytics(playerId) { + userId } } - /** - * Helper function that gets the instantiated `UIManagerModule` from modules registry. - */ - private fun uiManager(): UIManagerModule? = - context.getNativeModule(UIManagerModule::class.java) - - /** - * Helper function that gets the instantiated `PlayerModule` from modules registry. - */ - private fun playerModule(): PlayerModule? = - context.getNativeModule(PlayerModule::class.java) + private inline fun TPromise.resolveOnUiThreadWithAnalytics( + playerId: NativeId, + crossinline block: AnalyticsApi.() -> T, + ) = resolveOnUiThread { + val analytics = getPlayer(playerId).analytics ?: throw IllegalStateException("Analytics is disabled") + analytics.block() + } } 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 c55b75ae..5c5fa2bf 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -1,21 +1,26 @@ package com.bitmovin.player.reactnative -import android.util.Log import com.bitmovin.analytics.api.DefaultMetadata import com.bitmovin.player.api.Player +import com.bitmovin.player.api.PlayerConfig import com.bitmovin.player.api.analytics.create import com.bitmovin.player.api.event.PlayerEvent -import com.bitmovin.player.reactnative.converter.JsonConverter +import com.bitmovin.player.reactnative.converter.toAdItem +import com.bitmovin.player.reactnative.converter.toAnalyticsConfig +import com.bitmovin.player.reactnative.converter.toAnalyticsDefaultMetadata +import com.bitmovin.player.reactnative.converter.toJson +import com.bitmovin.player.reactnative.converter.toPlayerConfig +import com.bitmovin.player.reactnative.extensions.mapToReactArray import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.uimanager.UIManagerModule +import java.security.InvalidParameterException private const val MODULE_NAME = "PlayerModule" @ReactModule(name = MODULE_NAME) -class PlayerModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { +class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(context) { /** - * In-memory mapping from `nativeId`s to `Player` instances. + * In-memory mapping from [NativeId]s to [Player] instances. */ private val players: Registry = mutableMapOf() @@ -25,30 +30,17 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB override fun getName() = MODULE_NAME /** - * Fetches the `Player` instance associated with `nativeId` from the internal players. - * @param nativeId `Player` instance ID. - * @return The associated `Player` instance or `null`. + * Fetches the `Player` instance associated with [nativeId] from the internal players. */ - fun getPlayer(nativeId: NativeId?): Player? { - if (nativeId == null) { - return null - } - return players[nativeId] - } + fun getPlayerOrNull(nativeId: NativeId): Player? = players[nativeId] /** * Creates a new `Player` instance inside the internal players using the provided `config` object. * @param config `PlayerConfig` object received from JS. */ @ReactMethod - fun initWithConfig(nativeId: NativeId, config: ReadableMap?) { - uiManager()?.addUIBlock { - if (!players.containsKey(nativeId)) { - JsonConverter.toPlayerConfig(config).let { - players[nativeId] = Player.create(context, it) - } - } - } + fun initWithConfig(nativeId: NativeId, config: ReadableMap?, promise: Promise) { + init(nativeId, config, analyticsConfigJson = null, promise) } /** @@ -57,386 +49,370 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param analyticsConfigJson `AnalyticsConfig` object received from JS. */ @ReactMethod - fun initWithAnalyticsConfig(nativeId: NativeId, playerConfigJson: ReadableMap?, analyticsConfigJson: ReadableMap?) { - uiManager()?.addUIBlock { - if (players.containsKey(nativeId)) { - Log.d("[PlayerModule]", "Duplicate player creation for id $nativeId") - return@addUIBlock - } - val playerConfig = JsonConverter.toPlayerConfig(playerConfigJson) - val analyticsConfig = JsonConverter.toAnalyticsConfig(analyticsConfigJson) - val defaultMetadata = JsonConverter.toAnalyticsDefaultMetadata( - analyticsConfigJson?.getMap("defaultMetadata"), + fun initWithAnalyticsConfig( + nativeId: NativeId, + playerConfigJson: ReadableMap?, + analyticsConfigJson: ReadableMap, + promise: Promise, + ) = init(nativeId, playerConfigJson, analyticsConfigJson, promise) + + private fun init( + nativeId: NativeId, + playerConfigJson: ReadableMap?, + analyticsConfigJson: ReadableMap?, + promise: Promise, + ) = promise.unit.resolveOnUiThread { + if (players.containsKey(nativeId)) { + throw IllegalArgumentException("Duplicate player creation for id $nativeId") + } + val playerConfig = playerConfigJson?.toPlayerConfig() ?: PlayerConfig() + val analyticsConfig = analyticsConfigJson?.toAnalyticsConfig() + val defaultMetadata = analyticsConfigJson?.getMap("defaultMetadata")?.toAnalyticsDefaultMetadata() + + players[nativeId] = if (analyticsConfig == null) { + Player.create(context, playerConfig) + } else { + Player.create( + context = context, + playerConfig = playerConfig, + analyticsConfig = analyticsConfig, + defaultMetadata = defaultMetadata ?: DefaultMetadata(), ) - - players[nativeId] = if (analyticsConfig == null) { - Player.create(context, playerConfig) - } else { - Player.create( - context = context, - playerConfig = playerConfig, - analyticsConfig = analyticsConfig, - defaultMetadata = defaultMetadata ?: DefaultMetadata(), - ) - } } } /** - * Load the source of the given `nativeId` with `config` options from JS. + * Load the source of the given [nativeId] with `config` options from JS. * @param nativeId Target player. * @param sourceNativeId Target source. */ @ReactMethod - fun loadSource(nativeId: NativeId, sourceNativeId: String) { - uiManager()?.addUIBlock { - sourceModule()?.getSource(sourceNativeId)?.let { - players[nativeId]?.load(it) - } + fun loadSource(nativeId: NativeId, sourceNativeId: String, promise: Promise) { + promise.unit.resolveOnUiThread { + getPlayer(nativeId, this@PlayerModule).load(getSource(sourceNativeId)) } } /** - * Load the `offlineSourceConfig` for the player with `nativeId` and offline source module with `offlineModuleNativeId`. + * Load the `offlineSourceConfig` for the player with [nativeId] and offline source module with `offlineModuleNativeId`. * @param nativeId Target player. * @param offlineContentManagerBridgeId Target offline module. * @param options Source configuration options from JS. */ @ReactMethod - fun loadOfflineContent(nativeId: NativeId, offlineContentManagerBridgeId: String, options: ReadableMap?) { - uiManager()?.addUIBlock { - val offlineSourceConfig = offlineModule()?.getOfflineContentManagerBridge(offlineContentManagerBridgeId) - ?.offlineContentManager?.offlineSourceConfig - - if (offlineSourceConfig != null) { - players[nativeId]?.load(offlineSourceConfig) - } + fun loadOfflineContent( + nativeId: NativeId, + offlineContentManagerBridgeId: String, + options: ReadableMap?, + promise: Promise, + ) { + promise.unit.resolveOnUiThread { + offlineModule + .getOfflineContentManagerBridgeOrNull(offlineContentManagerBridgeId) + ?.offlineContentManager + ?.offlineSourceConfig + ?.let { getPlayer(nativeId).load(it) } } } /** - * Call `.unload()` on `nativeId`'s player. + * Call `.unload()` on [nativeId]'s player. * @param nativeId Target player Id. */ @ReactMethod - fun unload(nativeId: NativeId) { - uiManager()?.addUIBlock { - players[nativeId]?.unload() + fun unload(nativeId: NativeId, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + unload() } } /** - * Call `.play()` on `nativeId`'s player. + * Call `.play()` on [nativeId]'s player. * @param nativeId Target player Id. */ @ReactMethod - fun play(nativeId: NativeId) { - uiManager()?.addUIBlock { - players[nativeId]?.play() + fun play(nativeId: NativeId, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + play() } } /** - * Call `.pause()` on `nativeId`'s player. + * Call `.pause()` on [nativeId]'s player. * @param nativeId Target player Id. */ @ReactMethod - fun pause(nativeId: NativeId) { - uiManager()?.addUIBlock { - players[nativeId]?.pause() + fun pause(nativeId: NativeId, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + pause() } } /** - * Call `.seek(time:)` on `nativeId`'s player. + * Call `.seek(time:)` on [nativeId]'s player. * @param nativeId Target player Id. * @param time Seek time in seconds. */ @ReactMethod - fun seek(nativeId: NativeId, time: Double) { - uiManager()?.addUIBlock { - players[nativeId]?.seek(time) + fun seek(nativeId: NativeId, time: Double, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + seek(time) } } /** - * Call `.timeShift(offset:)` on `nativeId`'s player. + * Call `.timeShift(offset:)` on [nativeId]'s player. * @param nativeId Target player Id. * @param offset Offset time in seconds. */ @ReactMethod - fun timeShift(nativeId: NativeId, offset: Double) { - uiManager()?.addUIBlock { - players[nativeId]?.timeShift(offset) + fun timeShift(nativeId: NativeId, offset: Double, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + timeShift(offset) } } /** - * Call `.mute()` on `nativeId`'s player. + * Call `.mute()` on [nativeId]'s player. * @param nativeId Target player Id. */ @ReactMethod - fun mute(nativeId: NativeId) { - uiManager()?.addUIBlock { - players[nativeId]?.mute() + fun mute(nativeId: NativeId, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + mute() } } /** - * Call `.unmute()` on `nativeId`'s player. + * Call `.unmute()` on [nativeId]'s player. * @param nativeId Target player Id. */ @ReactMethod - fun unmute(nativeId: NativeId) { - uiManager()?.addUIBlock { - players[nativeId]?.unmute() + fun unmute(nativeId: NativeId, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + unmute() } } /** - * Call `.destroy()` on `nativeId`'s player. + * Call `.destroy()` on [nativeId]'s player. * @param nativeId Target player Id. */ @ReactMethod - fun destroy(nativeId: NativeId) { - uiManager()?.addUIBlock { - players[nativeId]?.let { - it.destroy() - players.remove(nativeId) - } + fun destroy(nativeId: NativeId, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + destroy() + players.remove(nativeId) } } /** - * Call `.setVolume(volume:)` on `nativeId`'s player. + * Call `.setVolume(volume:)` on [nativeId]'s player. * @param nativeId Target player Id. * @param volume Volume level integer between 0 to 100. */ @ReactMethod - fun setVolume(nativeId: NativeId, volume: Int) { - uiManager()?.addUIBlock { - players[nativeId]?.volume = volume + fun setVolume(nativeId: NativeId, volume: Int, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + this.volume = volume } } /** - * Resolve `nativeId`'s current volume. + * Resolve [nativeId]'s current volume. * @param nativeId Target player Id. * @param promise JS promise object. */ @ReactMethod fun getVolume(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(players[nativeId]?.volume) + promise.int.resolveOnUiThreadWithPlayer(nativeId) { + volume } } /** - * Resolve the source of `nativeId`'s player. + * Resolve the source of [nativeId]'s player. * @param nativeId Target player Id. * @param promise JS promise object. */ @ReactMethod fun source(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(JsonConverter.fromSource(players[nativeId]?.source)) + promise.map.nullable.resolveOnUiThreadWithPlayer(nativeId) { + source?.toJson() } } /** - * Resolve `nativeId`'s current playback time. + * Resolve [nativeId]'s current playback time. * @param nativeId Target player Id. * @param promise JS promise object. */ @ReactMethod fun currentTime(nativeId: NativeId, mode: String?, promise: Promise) { - uiManager()?.addUIBlock { - var timeOffset: Double = 0.0 - if (mode != null) { - timeOffset = if (mode == "relative") { - players[nativeId]?.playbackTimeOffsetToRelativeTime ?: 0.0 - } else { - players[nativeId]?.playbackTimeOffsetToAbsoluteTime ?: 0.0 - } - } - val currentTime = players[nativeId]?.currentTime - if (currentTime != null) { - promise.resolve(currentTime + timeOffset) + promise.double.resolveOnUiThreadWithPlayer(nativeId) { + currentTime + when (mode) { + "relative" -> playbackTimeOffsetToRelativeTime + "absolute" -> playbackTimeOffsetToAbsoluteTime + else -> throw InvalidParameterException("Unknown mode $mode") } } } /** - * Resolve `nativeId`'s current source duration. + * Resolve [nativeId]'s current source duration. * @param nativeId Target player Id. * @param promise JS promise object. */ @ReactMethod fun duration(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(players[nativeId]?.duration) + promise.double.resolveOnUiThreadWithPlayer(nativeId) { + duration } } /** - * Resolve `nativeId`'s current muted state. + * Resolve [nativeId]'s current muted state. * @param nativeId Target player Id. * @param promise JS promise object. */ @ReactMethod fun isMuted(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(players[nativeId]?.isMuted) + promise.bool.resolveOnUiThreadWithPlayer(nativeId) { + isMuted } } /** - * Resolve `nativeId`'s current playing state. + * Resolve [nativeId]'s current playing state. * @param nativeId Target player Id. * @param promise JS promise object. */ @ReactMethod fun isPlaying(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(players[nativeId]?.isPlaying) + promise.bool.resolveOnUiThreadWithPlayer(nativeId) { + isPlaying } } /** - * Resolve `nativeId`'s current paused state. + * Resolve [nativeId]'s current paused state. * @param nativeId Target player Id. * @param promise JS promise object. */ @ReactMethod fun isPaused(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(players[nativeId]?.isPaused) + promise.bool.resolveOnUiThreadWithPlayer(nativeId) { + isPaused } } /** - * Resolve `nativeId`'s current live state. + * Resolve [nativeId]'s current live state. * @param nativeId Target player Id. * @param promise JS promise object. */ @ReactMethod fun isLive(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(players[nativeId]?.isLive) + promise.bool.resolveOnUiThreadWithPlayer(nativeId) { + isLive } } /** - * Resolve `nativeId`'s currently selected audio track. + * Resolve [nativeId]'s currently selected audio track. * @param nativeId Target player Id. * @param promise JS promise object. */ @ReactMethod fun getAudioTrack(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(JsonConverter.fromAudioTrack(players[nativeId]?.source?.selectedAudioTrack)) + promise.map.nullable.resolveOnUiThreadWithPlayer(nativeId) { + source?.selectedAudioTrack?.toJson() } } /** - * Resolve `nativeId`'s player available audio tracks. + * Resolve [nativeId]'s player available audio tracks. * @param nativeId Target player Id. * @param promise JS promise object. */ @ReactMethod fun getAvailableAudioTracks(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - val audioTracks = Arguments.createArray() - players[nativeId]?.source?.availableAudioTracks?.let { tracks -> - tracks.forEach { - audioTracks.pushMap(JsonConverter.fromAudioTrack(it)) - } - } - promise.resolve(audioTracks) + promise.array.resolveOnUiThreadWithPlayer(nativeId) { + source?.availableAudioTracks?.mapToReactArray { it.toJson() } ?: Arguments.createArray() } } /** - * Set `nativeId`'s player audio track. + * Set [nativeId]'s player audio track. * @param nativeId Target player Id. * @param trackIdentifier The audio track identifier. * @param promise JS promise object. */ @ReactMethod fun setAudioTrack(nativeId: NativeId, trackIdentifier: String, promise: Promise) { - uiManager()?.addUIBlock { - players[nativeId]?.source?.setAudioTrack(trackIdentifier) - promise.resolve(null) + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + source?.setAudioTrack(trackIdentifier) } } /** - * Resolve `nativeId`'s currently selected subtitle track. + * Resolve [nativeId]'s currently selected subtitle track. * @param nativeId Target player Id. * @param promise JS promise object. */ @ReactMethod fun getSubtitleTrack(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(JsonConverter.fromSubtitleTrack(players[nativeId]?.source?.selectedSubtitleTrack)) + promise.map.nullable.resolveOnUiThreadWithPlayer(nativeId) { + source?.selectedSubtitleTrack?.toJson() } } /** - * Resolve `nativeId`'s player available subtitle tracks. + * Resolve [nativeId]'s player available subtitle tracks. * @param nativeId Target player Id. * @param promise JS promise object. */ @ReactMethod fun getAvailableSubtitles(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - val subtitleTracks = Arguments.createArray() - players[nativeId]?.source?.availableSubtitleTracks?.let { tracks -> - tracks.forEach { - subtitleTracks.pushMap(JsonConverter.fromSubtitleTrack(it)) - } - } - promise.resolve(subtitleTracks) + promise.array.resolveOnUiThreadWithPlayer(nativeId) { + source?.availableSubtitleTracks?.mapToReactArray { it.toJson() } ?: Arguments.createArray() } } /** - * Set `nativeId`'s player subtitle track. + * Set [nativeId]'s player subtitle track. * @param nativeId Target player Id. * @param trackIdentifier The subtitle track identifier. * @param promise JS promise object. */ @ReactMethod fun setSubtitleTrack(nativeId: NativeId, trackIdentifier: String?, promise: Promise) { - uiManager()?.addUIBlock { - players[nativeId]?.source?.setSubtitleTrack(trackIdentifier) - promise.resolve(null) + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + source?.setSubtitleTrack(trackIdentifier) } } /** - * Schedules an `AdItem` in the `nativeId`'s associated player. + * Schedules an `AdItem` in the [nativeId]'s associated player. * @param nativeId Target player id. * @param adItemJson Json representation of the `AdItem` to be scheduled. */ @ReactMethod - fun scheduleAd(nativeId: NativeId, adItemJson: ReadableMap?) { - JsonConverter.toAdItem(adItemJson)?.let { adItem -> - uiManager()?.addUIBlock { - players[nativeId]?.scheduleAd(adItem) - } + fun scheduleAd(nativeId: NativeId, adItemJson: ReadableMap, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + scheduleAd(adItemJson.toAdItem() ?: throw IllegalArgumentException("invalid adItem")) } } /** - * Skips the current ad in `nativeId`'s associated player. + * Skips the current ad in [nativeId]'s associated player. * Has no effect if the current ad is not skippable or if no ad is being played back. * @param nativeId Target player id. */ @ReactMethod - fun skipAd(nativeId: NativeId) { - uiManager()?.addUIBlock { - players[nativeId]?.skipAd() + fun skipAd(nativeId: NativeId, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + skipAd() } } @@ -446,8 +422,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isAd(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(players[nativeId]?.isAd) + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + isAd } } @@ -458,8 +434,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getTimeShift(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(players[nativeId]?.timeShift) + promise.double.resolveOnUiThreadWithPlayer(nativeId) { + timeShift } } @@ -470,8 +446,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getMaxTimeShift(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(players[nativeId]?.maxTimeShift) + promise.double.resolveOnUiThreadWithPlayer(nativeId) { + maxTimeShift } } @@ -481,9 +457,9 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param maxSelectableBitrate The desired max bitrate limit. */ @ReactMethod - fun setMaxSelectableBitrate(nativeId: NativeId, maxSelectableBitrate: Int) { - uiManager()?.addUIBlock { - players[nativeId]?.setMaxSelectableVideoBitrate( + fun setMaxSelectableBitrate(nativeId: NativeId, maxSelectableBitrate: Int, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + setMaxSelectableVideoBitrate( maxSelectableBitrate.takeUnless { it == -1 } ?: Integer.MAX_VALUE, ) } @@ -496,8 +472,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getThumbnail(nativeId: NativeId, time: Double, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(JsonConverter.fromThumbnail(players[nativeId]?.source?.getThumbnail(time))) + promise.map.nullable.resolveOnUiThreadWithPlayer(nativeId) { + source?.getThumbnail(time)?.toJson() } } @@ -506,9 +482,9 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * should be sent. */ @ReactMethod - fun castVideo(nativeId: NativeId) { - uiManager()?.addUIBlock { - players[nativeId]?.castVideo() + fun castVideo(nativeId: NativeId, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + castVideo() } } @@ -516,9 +492,9 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * Stops casting the current video. Has no effect if [isCasting] is false. */ @ReactMethod - fun castStop(nativeId: NativeId) { - uiManager()?.addUIBlock { - players[nativeId]?.castStop() + fun castStop(nativeId: NativeId, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + castStop() } } @@ -528,8 +504,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isCastAvailable(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(players[nativeId]?.isCastAvailable) + promise.bool.resolveOnUiThreadWithPlayer(nativeId) { + isCastAvailable } } @@ -538,80 +514,53 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isCasting(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(players[nativeId]?.isCasting) + promise.bool.resolveOnUiThreadWithPlayer(nativeId) { + isCasting } } /** - * Resolve `nativeId`'s current video quality. - * @param nativeId Target player Id. - * @param promise JS promise object. + * Resolve [nativeId]'s current video quality. */ @ReactMethod fun getVideoQuality(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(JsonConverter.fromVideoQuality(players[nativeId]?.source?.selectedVideoQuality)) + promise.map.nullable.resolveOnUiThreadWithPlayer(nativeId) { + source?.selectedVideoQuality?.toJson() } } /** - * Resolve `nativeId`'s current available video qualities. - * @param nativeId Target player Id. - * @param promise JS promise object. + * Resolve [nativeId]'s current available video qualities. */ @ReactMethod fun getAvailableVideoQualities(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - val videoQualities = Arguments.createArray() - players[nativeId]?.source?.availableVideoQualities?.let { qualities -> - qualities.forEach { - videoQualities.pushMap(JsonConverter.fromVideoQuality(it)) - } - } - promise.resolve(videoQualities) + promise.array.resolveOnUiThreadWithPlayer(nativeId) { + source?.availableVideoQualities?.mapToReactArray { it.toJson() } ?: Arguments.createArray() } } /** - * Resolve `nativeId`'s current playback speed. - * @param nativeId Target player Id. - * @param promise JS promise object. + * Resolve [nativeId]'s current playback speed. */ @ReactMethod fun getPlaybackSpeed(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(players[nativeId]?.playbackSpeed) + promise.float.resolveOnUiThreadWithPlayer(nativeId) { + playbackSpeed } } /** * Sets playback speed for the player. - * @param nativeId Target player Id. - * @param playbackSpeed Float representing the playback speed level. */ @ReactMethod - fun setPlaybackSpeed(nativeId: NativeId, playbackSpeed: Float) { - uiManager()?.addUIBlock { - players[nativeId]?.playbackSpeed = playbackSpeed + fun setPlaybackSpeed(nativeId: NativeId, playbackSpeed: Float, promise: Promise) { + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + this.playbackSpeed = playbackSpeed } } - /** - * Helper function that returns the initialized `UIManager` instance. - */ - private fun uiManager(): UIManagerModule? = - context.getNativeModule(UIManagerModule::class.java) - - /** - * Helper function that returns the initialized `SourceModule` instance. - */ - private fun sourceModule(): SourceModule? = - context.getNativeModule(SourceModule::class.java) - - /** - * Helper function that returns the initialized `OfflineModule` instance. - */ - private fun offlineModule(): OfflineModule? = - context.getNativeModule(OfflineModule::class.java) + private inline fun TPromise.resolveOnUiThreadWithPlayer( + nativeId: NativeId, + crossinline block: Player.() -> T, + ) = resolveOnUiThread { getPlayer(nativeId, this@PlayerModule).block() } } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt index d3c18ca0..1d5c78f2 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt @@ -16,7 +16,7 @@ import com.bitmovin.player.api.event.PlayerEvent import com.bitmovin.player.api.event.SourceEvent import com.bitmovin.player.api.ui.PlayerViewConfig import com.bitmovin.player.api.ui.StyleConfig -import com.bitmovin.player.reactnative.converter.JsonConverter +import com.bitmovin.player.reactnative.converter.toJson import com.bitmovin.player.reactnative.ui.RNPictureInPictureDelegate import com.bitmovin.player.reactnative.ui.RNPictureInPictureHandler import com.facebook.react.ReactActivity @@ -304,10 +304,10 @@ class RNPlayerView( * @param event Optional js object to be sent as payload. */ private inline fun emitEvent(name: String, event: E) { - val payload = if (event is PlayerEvent) { - JsonConverter.fromPlayerEvent(event) - } else { - JsonConverter.fromSourceEvent(event as SourceEvent) + val payload = when (event) { + is PlayerEvent -> event.toJson() + is SourceEvent -> event.toJson() + else -> throw IllegalArgumentException() } val reactContext = context as ReactContext reactContext diff --git a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt index 4f58b55d..e798524b 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt @@ -9,10 +9,12 @@ import com.bitmovin.player.SubtitleView import com.bitmovin.player.api.ui.PlayerViewConfig import com.bitmovin.player.api.ui.ScalingMode import com.bitmovin.player.api.ui.UiConfig -import com.bitmovin.player.reactnative.converter.JsonConverter +import com.bitmovin.player.reactnative.converter.toRNPlayerViewConfigWrapper +import com.bitmovin.player.reactnative.converter.toRNStyleConfigWrapperFromPlayerConfig +import com.bitmovin.player.reactnative.extensions.customMessageHandlerModule import com.bitmovin.player.reactnative.extensions.getBooleanOrNull import com.bitmovin.player.reactnative.extensions.getModule -import com.bitmovin.player.reactnative.ui.CustomMessageHandlerModule +import com.bitmovin.player.reactnative.extensions.playerModule import com.bitmovin.player.reactnative.ui.FullscreenHandlerModule import com.bitmovin.player.reactnative.ui.RNPictureInPictureHandler import com.facebook.react.bridge.* @@ -20,6 +22,7 @@ import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.annotations.ReactProp +import java.security.InvalidParameterException private const val MODULE_NAME = "NativePlayerView" @@ -43,6 +46,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple override fun getName() = MODULE_NAME private var customMessageHandlerBridgeId: NativeId? = null + private val handler = Handler(Looper.getMainLooper()) /** * The component's native view factory. RN may call this method multiple times @@ -154,48 +158,31 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple * @param args Arguments list sent from the js side. */ override fun receiveCommand(view: RNPlayerView, commandId: String?, args: ReadableArray?) { + fun Int.toCommand(): Commands? = Commands.values().getOrNull(this) val command = commandId?.toInt()?.toCommand() ?: throw IllegalArgumentException( "The received command is not supported by the Bitmovin Player View", ) + fun T?.require(): T = this ?: throw InvalidParameterException("Missing parameter") when (command) { - Commands.ATTACH_PLAYER -> attachPlayer(view, args?.getString(1), args?.getMap(2)) - Commands.ATTACH_FULLSCREEN_BRIDGE -> args?.getString(1)?.let { fullscreenBridgeId -> - attachFullscreenBridge(view, fullscreenBridgeId) - } - - Commands.SET_CUSTOM_MESSAGE_HANDLER_BRIDGE_ID -> { - args?.getString(1)?.let { customMessageHandlerBridgeId -> - setCustomMessageHandlerBridgeId(view, customMessageHandlerBridgeId) - } - } - - Commands.SET_FULLSCREEN -> { - args?.getBoolean(1)?.let { isFullscreen -> - setFullscreen(view, isFullscreen) - } - } - - Commands.SET_SCALING_MODE -> { - args?.getString(1)?.let { scalingMode -> - setScalingMode(view, scalingMode) - } - } - - Commands.SET_PICTURE_IN_PICTURE -> { - args?.getBoolean(1)?.let { isPictureInPicture -> - setPictureInPicture(view, isPictureInPicture) - } - } + Commands.ATTACH_PLAYER -> attachPlayer(view, args?.getString(1).require(), args?.getMap(2)) + Commands.ATTACH_FULLSCREEN_BRIDGE -> attachFullscreenBridge(view, args?.getString(1).require()) + Commands.SET_CUSTOM_MESSAGE_HANDLER_BRIDGE_ID -> setCustomMessageHandlerBridgeId( + view, + args?.getString(1).require(), + ) + Commands.SET_FULLSCREEN -> setFullscreen(view, args?.getBoolean(1).require()) + Commands.SET_SCALING_MODE -> setScalingMode(view, args?.getString(1).require()) + Commands.SET_PICTURE_IN_PICTURE -> setPictureInPicture(view, args?.getBoolean(1).require()) } } @ReactProp(name = "config") fun setConfig(view: RNPlayerView, config: ReadableMap?) { - view.config = if (config != null) JsonConverter.toRNPlayerViewConfigWrapper(config) else null + view.config = config?.toRNPlayerViewConfigWrapper() } private fun attachFullscreenBridge(view: RNPlayerView, fullscreenBridgeId: NativeId) { - Handler(Looper.getMainLooper()).post { + handler.postAndLogException { view.playerView?.setFullscreenHandler( context.getModule()?.getInstance(fullscreenBridgeId), ) @@ -203,9 +190,9 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } private fun setFullscreen(view: RNPlayerView, isFullscreenRequested: Boolean) { - Handler(Looper.getMainLooper()).post { - val playerView = view.playerView ?: return@post - if (playerView.isFullscreen == isFullscreenRequested) return@post + handler.postAndLogException { + val playerView = view.playerView ?: return@postAndLogException + if (playerView.isFullscreen == isFullscreenRequested) return@postAndLogException if (isFullscreenRequested) { playerView.enterFullscreen() } else { @@ -215,9 +202,9 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } private fun setPictureInPicture(view: RNPlayerView, isPictureInPictureRequested: Boolean) { - Handler(Looper.getMainLooper()).post { - val playerView = view.playerView ?: return@post - if (playerView.isPictureInPicture == isPictureInPictureRequested) return@post + handler.postAndLogException { + val playerView = view.playerView ?: throw IllegalStateException("The player view is not yet created") + if (playerView.isPictureInPicture == isPictureInPictureRequested) return@postAndLogException if (isPictureInPictureRequested) { playerView.enterPictureInPicture() } else { @@ -227,7 +214,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } private fun setScalingMode(view: RNPlayerView, scalingMode: String) { - Handler(Looper.getMainLooper()).post { + handler.postAndLogException { view.playerView?.scalingMode = ScalingMode.valueOf(scalingMode) } } @@ -239,7 +226,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple private fun attachCustomMessageHandlerBridge(view: RNPlayerView) { view.playerView?.setCustomMessageHandler( - context.getModule() + context.customMessageHandlerModule ?.getInstance(customMessageHandlerBridgeId) ?.customMessageHandler, ) @@ -250,9 +237,10 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple * @param view Target `RNPlayerView`. * @param playerId `Player` instance id inside `PlayerModule`'s registry. */ - private fun attachPlayer(view: RNPlayerView, playerId: NativeId?, playerConfig: ReadableMap?) { - Handler(Looper.getMainLooper()).post { - val player = getPlayerModule()?.getPlayer(playerId) + private fun attachPlayer(view: RNPlayerView, playerId: NativeId, playerConfig: ReadableMap?) { + handler.postAndLogException { + val player = playerId.let { context.playerModule?.getPlayerOrNull(it) } + ?: throw InvalidParameterException("Cannot create a PlayerView, invalid playerId was passed: $playerId") val playbackConfig = playerConfig?.getMap("playbackConfig") val isPictureInPictureEnabled = view.config?.pictureInPictureConfig?.isEnabled == true || playbackConfig?.getBooleanOrNull("isPictureInPictureEnabled") == true @@ -260,7 +248,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple view.pictureInPictureHandler = pictureInPictureHandler view.pictureInPictureHandler?.isPictureInPictureEnabled = isPictureInPictureEnabled - val rnStyleConfigWrapper = playerConfig?.let { JsonConverter.toRNStyleConfigWrapperFromPlayerConfig(it) } + val rnStyleConfigWrapper = playerConfig?.toRNStyleConfigWrapperFromPlayerConfig() val configuredPlayerViewConfig = view.config?.playerViewConfig ?: PlayerViewConfig() if (view.playerView != null) { @@ -268,10 +256,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } else { // PlayerView has to be initialized with Activity context val currentActivity = context.currentActivity - if (currentActivity == null) { - Log.e(MODULE_NAME, "Cannot create a PlayerView, because no activity is attached.") - return@post - } + ?: throw IllegalStateException("Cannot create a PlayerView, because no activity is attached.") val userInterfaceType = rnStyleConfigWrapper?.userInterfaceType ?: UserInterfaceType.Bitmovin val playerViewConfig: PlayerViewConfig = if (userInterfaceType != UserInterfaceType.Bitmovin) { configuredPlayerViewConfig.copy(uiConfig = UiConfig.Disabled) @@ -301,10 +286,12 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } } - /** - * Helper function that gets the instantiated `PlayerModule` from modules registry. - */ - private fun getPlayerModule(): PlayerModule? = context.getModule() + /** Post and log any exceptions instead of crashing the app. */ + private inline fun Handler.postAndLogException(crossinline block: () -> Unit) = post { + try { + block() + } catch (e: Exception) { + Log.e(MODULE_NAME, "Error while executing command", e) + } + } } - -private fun Int.toCommand(): RNPlayerViewManager.Commands? = RNPlayerViewManager.Commands.values().getOrNull(this) 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 07feb792..dda547d2 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt @@ -1,23 +1,23 @@ 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.bitmovin.player.reactnative.converter.toAnalyticsSourceMetadata +import com.bitmovin.player.reactnative.converter.toJson +import com.bitmovin.player.reactnative.converter.toSourceConfig +import com.bitmovin.player.reactnative.extensions.toMap +import com.bitmovin.player.reactnative.extensions.toReadableMap 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 +import java.security.InvalidParameterException private const val MODULE_NAME = "SourceModule" @ReactModule(name = MODULE_NAME) -class SourceModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { +class SourceModule(context: ReactApplicationContext) : BitmovinBaseModule(context) { /** * In-memory mapping from `nativeId`s to `Source` instances. */ @@ -29,16 +29,9 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB override fun getName() = MODULE_NAME /** - * Fetches the `Source` instance associated with `nativeId` from internal sources. - * @param nativeId `Source` instance ID. - * @return The associated `Source` instance or `null`. + * Fetches the [Source] instance associated with [nativeId] from internal sources or null. */ - fun getSource(nativeId: NativeId?): Source? { - if (nativeId == null) { - return null - } - return sources[nativeId] - } + fun getSourceOrNull(nativeId: NativeId): Source? = sources[nativeId] /** * Creates a new `Source` instance inside the internal sources using the provided @@ -55,15 +48,9 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB drmNativeId: NativeId?, config: ReadableMap?, sourceRemoteControlConfig: ReadableMap?, - analyticsSourceMetadata: ReadableMap?, - ) { - uiManager()?.addUIBlock { - val sourceMetadata = JsonConverter.toAnalyticsSourceMetadata(analyticsSourceMetadata) ?: SourceMetadata() - initializeSource(nativeId, drmNativeId, config) { sourceConfig -> - Source.create(sourceConfig, sourceMetadata) - } - } - } + analyticsSourceMetadata: ReadableMap, + promise: Promise, + ) = initializeSource(nativeId, drmNativeId, config, analyticsSourceMetadata, promise) /** * Creates a new `Source` instance inside the internal sources using the provided @@ -79,32 +66,27 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB drmNativeId: NativeId?, config: ReadableMap?, sourceRemoteControlConfig: ReadableMap?, - ) { - uiManager()?.addUIBlock { - initializeSource(nativeId, drmNativeId, config) { sourceConfig -> - Source.create(sourceConfig) - } - } - } + promise: Promise, + ) = initializeSource(nativeId, drmNativeId, config, analyticsSourceMetadata = null, promise) 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) - } + analyticsSourceMetadata: ReadableMap?, + promise: Promise, + ) = promise.unit.resolveOnUiThread { + val drmConfig = drmNativeId?.let { drmModule.getConfig(it) } + val sourceConfig = config?.toSourceConfig() ?: throw InvalidParameterException("Invalid SourceConfig") + val sourceMetadata = analyticsSourceMetadata?.toAnalyticsSourceMetadata() + if (sources.containsKey(nativeId)) { + throw IllegalStateException("NativeId $NativeId already exists") + } + sourceConfig.drmConfig = drmConfig + sources[nativeId] = if (sourceMetadata == null) { + Source.create(sourceConfig) + } else { + Source.create(sourceConfig, sourceMetadata) } } @@ -113,8 +95,10 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB * @param nativeId `Source` to be disposed. */ @ReactMethod - fun destroy(nativeId: NativeId) { - sources.remove(nativeId) + fun destroy(nativeId: NativeId, promise: Promise) { + promise.unit.resolveOnUiThreadWithSource(nativeId) { + sources.remove(nativeId) + } } /** @@ -124,8 +108,8 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isAttachedToPlayer(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(sources[nativeId]?.isAttachedToPlayer) + promise.bool.resolveOnUiThreadWithSource(nativeId) { + isAttachedToPlayer } } @@ -136,55 +120,48 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isActive(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(sources[nativeId]?.isActive) + promise.bool.resolveOnUiThreadWithSource(nativeId) { + isActive } } /** * The duration of `nativeId` source in seconds. - * @param nativeId Source `nativeId`. - * @param promise: JS promise object. */ @ReactMethod fun duration(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(sources[nativeId]?.duration) + promise.double.resolveOnUiThreadWithSource(nativeId) { + duration } } /** * The current loading state of `nativeId` source. - * @param nativeId Source `nativeId`. - * @param promise: JS promise object. */ @ReactMethod fun loadingState(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(sources[nativeId]?.loadingState?.ordinal) + promise.int.resolveOnUiThreadWithSource(nativeId) { + loadingState.ordinal } } /** * Metadata for the currently loaded `nativeId` source. - * @param nativeId Source `nativeId`. - * @param promise: JS promise object. */ @ReactMethod fun getMetadata(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(sources[nativeId]?.config?.metadata) + promise.map.nullable.resolveOnUiThreadWithSource(nativeId) { + config.metadata?.toReadableMap() } } /** * Set the metadata for a loaded `nativeId` source. - * @param nativeId Source `nativeId`. */ @ReactMethod - fun setMetadata(nativeId: NativeId, metadata: ReadableMap?) { - uiManager()?.addUIBlock { - sources[nativeId]?.config?.metadata = asStringMap(metadata) + fun setMetadata(nativeId: NativeId, metadata: ReadableMap?, promise: Promise) { + promise.unit.resolveOnUiThreadWithSource(nativeId) { + config.metadata = metadata?.toMap() } } @@ -195,32 +172,13 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getThumbnail(nativeId: NativeId, time: Double, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(JsonConverter.fromThumbnail(sources[nativeId]?.getThumbnail(time))) - } - } - - /** - * Helper method that converts a React `ReadableMap` into a kotlin String -> String map. - */ - private fun asStringMap(readableMap: ReadableMap?): Map? { - if (readableMap == null) { - return null + promise.map.nullable.resolveOnUiThreadWithSource(nativeId) { + getThumbnail(time)?.toJson() } - val map = mutableMapOf() - for (entry in readableMap.entryIterator) { - map[entry.key] = entry.value.toString() - } - return map } - /** - * Helper function that returns the initialized `UIManager` instance. - */ - private fun uiManager(): UIManagerModule? = context.getNativeModule(UIManagerModule::class.java) - - /** - * Helper function that returns the initialized `DrmModule` instance. - */ - private fun drmModule(): DrmModule? = context.getNativeModule(DrmModule::class.java) + private inline fun TPromise.resolveOnUiThreadWithSource( + nativeId: NativeId, + crossinline block: Source.() -> T, + ) = resolveOnUiThread { getSource(nativeId, this@SourceModule).block() } } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/UuidModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/UuidModule.kt index dabc15c8..5d9a5dbf 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/UuidModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/UuidModule.kt @@ -1,13 +1,12 @@ package com.bitmovin.player.reactnative import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import java.util.UUID private const val MODULE_NAME = "UuidModule" -class UuidModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { +class UuidModule(context: ReactApplicationContext) : BitmovinBaseModule(context) { /** * Exported JS module name. */ 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 4f77637d..ae4237a3 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 @@ -5,7 +5,6 @@ import com.bitmovin.analytics.api.CustomData import com.bitmovin.analytics.api.DefaultMetadata import com.bitmovin.analytics.api.SourceMetadata import com.bitmovin.player.api.DeviceDescription.DeviceName -import com.bitmovin.player.api.DeviceDescription.ModelName import com.bitmovin.player.api.PlaybackConfig import com.bitmovin.player.api.PlayerConfig import com.bitmovin.player.api.TweaksConfig @@ -52,1254 +51,773 @@ import com.bitmovin.player.reactnative.RNBufferLevels import com.bitmovin.player.reactnative.RNPlayerViewConfigWrapper import com.bitmovin.player.reactnative.RNStyleConfigWrapper import com.bitmovin.player.reactnative.UserInterfaceType +import com.bitmovin.player.reactnative.extensions.get import com.bitmovin.player.reactnative.extensions.getBooleanOrNull +import com.bitmovin.player.reactnative.extensions.getDoubleOrNull import com.bitmovin.player.reactnative.extensions.getName -import com.bitmovin.player.reactnative.extensions.getOrDefault -import com.bitmovin.player.reactnative.extensions.getProperty +import com.bitmovin.player.reactnative.extensions.mapToReactArray 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 -import com.bitmovin.player.reactnative.extensions.toList -import com.bitmovin.player.reactnative.extensions.toReadableArray +import com.bitmovin.player.reactnative.extensions.set +import com.bitmovin.player.reactnative.extensions.toMap +import com.bitmovin.player.reactnative.extensions.toMapList import com.bitmovin.player.reactnative.extensions.toReadableMap -import com.bitmovin.player.reactnative.ui.RNPictureInPictureHandler +import com.bitmovin.player.reactnative.extensions.withArray +import com.bitmovin.player.reactnative.extensions.withBoolean +import com.bitmovin.player.reactnative.extensions.withDouble +import com.bitmovin.player.reactnative.extensions.withInt +import com.bitmovin.player.reactnative.extensions.withMap +import com.bitmovin.player.reactnative.extensions.withString +import com.bitmovin.player.reactnative.extensions.withStringArray +import com.bitmovin.player.reactnative.ui.RNPictureInPictureHandler.PictureInPictureConfig import com.facebook.react.bridge.* import java.util.UUID /** - * Helper class to gather all conversion methods between JS -> Native objects. + * Converts an arbitrary `json` to `PlayerConfig`. */ -class JsonConverter { - companion object { - /** - * Converts an arbitrary `json` to `PlayerConfig`. - * @param json JS object representing the `PlayerConfig`. - * @return The generated `PlayerConfig` if successful, `null` otherwise. - */ - @JvmStatic - fun toPlayerConfig(json: ReadableMap?): PlayerConfig { - if (json == null) return PlayerConfig() - val playerConfig = if (json.hasKey("licenseKey")) { - PlayerConfig(key = json.getString("licenseKey")) - } else { - PlayerConfig() - } - if (json.hasKey("playbackConfig")) { - toPlaybackConfig(json.getMap("playbackConfig"))?.let { - playerConfig.playbackConfig = it - } - } - if (json.hasKey("styleConfig")) { - toStyleConfig(json.getMap("styleConfig"))?.let { - playerConfig.styleConfig = it - } - } - if (json.hasKey("tweaksConfig")) { - toTweaksConfig(json.getMap("tweaksConfig"))?.let { - playerConfig.tweaksConfig = it - } - } - if (json.hasKey("advertisingConfig")) { - toAdvertisingConfig(json.getMap("advertisingConfig"))?.let { - playerConfig.advertisingConfig = it - } - } - if (json.hasKey("adaptationConfig")) { - toAdaptationConfig(json.getMap("adaptationConfig"))?.let { - playerConfig.adaptationConfig = it - } - } - if (json.hasKey("remoteControlConfig")) { - toRemoteControlConfig(json.getMap("remoteControlConfig"))?.let { - playerConfig.remoteControlConfig = it - } - } - if (json.hasKey("bufferConfig")) { - toBufferConfig(json.getMap("bufferConfig"))?.let { - playerConfig.bufferConfig = it - } - } - if (json.hasKey("liveConfig")) { - toLiveConfig(json.getMap("liveConfig"))?.let { - playerConfig.liveConfig = it - } - } - return playerConfig - } - - /** - * Converts any JS object into a `BufferMediaTypeConfig` object. - * @param json JS object representing the `BufferMediaTypeConfig`. - * @return The generated `BufferMediaTypeConfig` if successful, `null` otherwise. - */ - @JvmStatic - fun toBufferMediaTypeConfig(json: ReadableMap?): BufferMediaTypeConfig? { - if (json == null) { - return null - } - val bufferMediaTypeConfig = BufferMediaTypeConfig() - if (json.hasKey("forwardDuration")) { - bufferMediaTypeConfig.forwardDuration = json.getDouble("forwardDuration") - } - return bufferMediaTypeConfig - } - - /** - * Converts any JS object into a `BufferConfig` object. - * @param json JS object representing the `BufferConfig`. - * @return The generated `BufferConfig` if successful, `null` otherwise. - */ - @JvmStatic - fun toBufferConfig(json: ReadableMap?): BufferConfig? { - if (json == null) { - return null - } - val bufferConfig = BufferConfig() - if (json.hasKey("audioAndVideo")) { - toBufferMediaTypeConfig(json.getMap("audioAndVideo"))?.let { - bufferConfig.audioAndVideo = it - } - } - if (json.hasKey("restartThreshold")) { - bufferConfig.restartThreshold = json.getDouble("restartThreshold") - } - if (json.hasKey("startupThreshold")) { - bufferConfig.startupThreshold = json.getDouble("startupThreshold") - } - return bufferConfig - } +fun ReadableMap.toPlayerConfig(): PlayerConfig = PlayerConfig(key = getString("licenseKey")).apply { + withMap("playbackConfig") { playbackConfig = it.toPlaybackConfig() } + withMap("styleConfig") { styleConfig = it.toStyleConfig() } + withMap("tweaksConfig") { tweaksConfig = it.toTweaksConfig() } + getMap("advertisingConfig")?.toAdvertisingConfig()?.let { advertisingConfig = it } + withMap("adaptationConfig") { adaptationConfig = it.toAdaptationConfig() } + withMap("remoteControlConfig") { remoteControlConfig = it.toRemoteControlConfig() } + withMap("bufferConfig") { bufferConfig = it.toBufferConfig() } + withMap("liveConfig") { liveConfig = it.toLiveConfig() } +} - /** - * Converts an arbitrary [ReadableMap] to a [RemoteControlConfig]. - * - * @param json JS object representing the [RemoteControlConfig]. - * @return The generated [RemoteControlConfig]. - */ - private fun toRemoteControlConfig(json: ReadableMap?): RemoteControlConfig? { - if (json == null) return null - val defaultRemoteControlConfig = RemoteControlConfig() - - val receiverStylesheetUrl = json.getOrDefault( - "receiverStylesheetUrl", - defaultRemoteControlConfig.receiverStylesheetUrl, - ) - - var customReceiverConfig = defaultRemoteControlConfig.customReceiverConfig - if (json.hasKey("customReceiverConfig")) { - customReceiverConfig = json.getMap("customReceiverConfig") - ?.toHashMap() - ?.mapValues { entry -> entry.value as String } ?: emptyMap() - } +/** + * Converts any JS object into a `BufferMediaTypeConfig` object. + */ +fun ReadableMap.toBufferMediaTypeConfig(): BufferMediaTypeConfig = BufferMediaTypeConfig().apply { + withDouble("forwardDuration") { forwardDuration = it } +} - val isCastEnabled = json.getOrDefault( - "isCastEnabled", - defaultRemoteControlConfig.isCastEnabled, - ) - - val sendManifestRequestsWithCredentials = json.getOrDefault( - "sendManifestRequestsWithCredentials", - defaultRemoteControlConfig.sendManifestRequestsWithCredentials, - ) - - val sendSegmentRequestsWithCredentials = json.getOrDefault( - "sendSegmentRequestsWithCredentials", - defaultRemoteControlConfig.sendSegmentRequestsWithCredentials, - ) - - val sendDrmLicenseRequestsWithCredentials = json.getOrDefault( - "sendDrmLicenseRequestsWithCredentials", - defaultRemoteControlConfig.sendDrmLicenseRequestsWithCredentials, - ) - - return RemoteControlConfig( - receiverStylesheetUrl = receiverStylesheetUrl, - customReceiverConfig = customReceiverConfig, - isCastEnabled = isCastEnabled, - sendManifestRequestsWithCredentials = sendManifestRequestsWithCredentials, - sendSegmentRequestsWithCredentials = sendSegmentRequestsWithCredentials, - sendDrmLicenseRequestsWithCredentials = sendDrmLicenseRequestsWithCredentials, - ) - } +/** + * Converts any JS object into a `BufferConfig` object. + */ +fun ReadableMap.toBufferConfig(): BufferConfig = BufferConfig().apply { + withMap("audioAndVideo") { audioAndVideo = it.toBufferMediaTypeConfig() } + withDouble("restartThreshold") { restartThreshold = it } + withDouble("startupThreshold") { startupThreshold = it } +} - /** - * Converts an arbitrary `json` to `SourceOptions`. - * @param json JS object representing the `SourceOptions`. - * @return The generated `SourceOptions`. - */ - @JvmStatic - fun toSourceOptions(json: ReadableMap?): SourceOptions { - if (json == null) return SourceOptions() - val startOffset = if (json.hasKey("startOffset")) json.getDouble("startOffset") else null - val timelineReferencePoint = toTimelineReferencePoint(json.getString("startOffsetTimelineReference")) - return SourceOptions(startOffset = startOffset, startOffsetTimelineReference = timelineReferencePoint) - } +/** + * Converts an arbitrary [ReadableMap] to a [RemoteControlConfig]. + */ +private fun ReadableMap.toRemoteControlConfig(): RemoteControlConfig = RemoteControlConfig().apply { + withString("receiverStylesheetUrl") { receiverStylesheetUrl = it } + withMap("customReceiverConfig") { customReceiverConfig = it.toMap() } + withBoolean("isCastEnabled") { isCastEnabled = it } + withBoolean("sendManifestRequestsWithCredentials") { sendManifestRequestsWithCredentials = it } + withBoolean("sendSegmentRequestsWithCredentials") { sendSegmentRequestsWithCredentials = it } + withBoolean("sendDrmLicenseRequestsWithCredentials") { sendDrmLicenseRequestsWithCredentials = it } +} - /** - * Converts an arbitrary `json` to `TimelineReferencePoint`. - * @param json JS string representing the `TimelineReferencePoint`. - * @return The generated `TimelineReferencePoint`. - */ - @JvmStatic - private fun toTimelineReferencePoint(json: String?): TimelineReferencePoint? = when (json) { - "start" -> TimelineReferencePoint.Start - "end" -> TimelineReferencePoint.End - else -> null - } +/** + * Converts an arbitrary `json` to `SourceOptions`. + */ +fun ReadableMap.toSourceOptions(): SourceOptions = SourceOptions( + startOffset = getDoubleOrNull("startOffset"), + startOffsetTimelineReference = getString("startOffsetTimelineReference")?.toTimelineReferencePoint(), +) - /** - * Converts an arbitrary `json` to `AdaptationConfig`. - * @param json JS object representing the `AdaptationConfig`. - * @return The generated `AdaptationConfig` if successful, `null` otherwise. - */ - private fun toAdaptationConfig(json: ReadableMap?): AdaptationConfig? { - if (json == null) return null - val adaptationConfig = AdaptationConfig() - if (json.hasKey("maxSelectableBitrate")) { - adaptationConfig.maxSelectableVideoBitrate = json.getInt("maxSelectableBitrate") - } - return adaptationConfig - } +/** + * Converts an arbitrary `json` to `TimelineReferencePoint`. + */ +private fun String.toTimelineReferencePoint(): TimelineReferencePoint? = when (this) { + "start" -> TimelineReferencePoint.Start + "end" -> TimelineReferencePoint.End + else -> null +} - /** - * Converts any JS object into a `PlaybackConfig` object. - * @param json JS object representing the `PlaybackConfig`. - * @return The generated `PlaybackConfig` if successful, `null` otherwise. - */ - @JvmStatic - fun toPlaybackConfig(json: ReadableMap?): PlaybackConfig? { - if (json == null) { - return null - } - val playbackConfig = PlaybackConfig() - if (json.hasKey("isAutoplayEnabled")) { - playbackConfig.isAutoplayEnabled = json.getBoolean("isAutoplayEnabled") - } - if (json.hasKey("isMuted")) { - playbackConfig.isMuted = json.getBoolean("isMuted") - } - if (json.hasKey("isTimeShiftEnabled")) { - playbackConfig.isTimeShiftEnabled = json.getBoolean("isTimeShiftEnabled") - } - return playbackConfig - } +/** + * Converts an arbitrary `json` to `AdaptationConfig`. + */ +private fun ReadableMap.toAdaptationConfig(): AdaptationConfig = AdaptationConfig().apply { + withInt("maxSelectableBitrate") { maxSelectableVideoBitrate = it } +} - /** - * Converts any JS object into a `StyleConfig` object. - * @param json JS object representing the `StyleConfig`. - * @return The generated `StyleConfig` if successful, `null` otherwise. - */ - @JvmStatic - fun toStyleConfig(json: ReadableMap?): StyleConfig? { - if (json == null) { - return null - } - val styleConfig = StyleConfig() - if (json.hasKey("isUiEnabled")) { - styleConfig.isUiEnabled = json.getBoolean("isUiEnabled") - } - if (json.hasKey("playerUiCss")) { - val playerUiCss = json.getString("playerUiCss") - if (!playerUiCss.isNullOrEmpty()) { - styleConfig.playerUiCss = playerUiCss - } - } - if (json.hasKey("supplementalPlayerUiCss")) { - val supplementalPlayerUiCss = json.getString("supplementalPlayerUiCss") - if (!supplementalPlayerUiCss.isNullOrEmpty()) { - styleConfig.supplementalPlayerUiCss = supplementalPlayerUiCss - } - } - if (json.hasKey("playerUiJs")) { - val playerUiJs = json.getString("playerUiJs") - if (!playerUiJs.isNullOrEmpty()) { - styleConfig.playerUiJs = playerUiJs - } - } - if (json.hasKey("scalingMode")) { - val scalingMode = json.getString("scalingMode") - if (!scalingMode.isNullOrEmpty()) { - styleConfig.scalingMode = ScalingMode.valueOf(scalingMode) - } - } - return styleConfig - } +/** + * Converts any JS object into a `PlaybackConfig` object. + */ +fun ReadableMap.toPlaybackConfig(): PlaybackConfig = PlaybackConfig().apply { + withBoolean("isAutoplayEnabled") { isAutoplayEnabled = it } + withBoolean("isMuted") { isMuted = it } + withBoolean("isTimeShiftEnabled") { isTimeShiftEnabled = it } +} - /** - * Converts any JS object into a `TweaksConfig` object. - * @param json JS object representing the `TweaksConfig`. - * @return The generated `TweaksConfig` if successful, `null` otherwise. - */ - @JvmStatic - fun toTweaksConfig(json: ReadableMap?): TweaksConfig? { - if (json == null) { - return null - } - val tweaksConfig = TweaksConfig() - if (json.hasKey("timeChangedInterval")) { - tweaksConfig.timeChangedInterval = json.getDouble("timeChangedInterval") - } - if (json.hasKey("bandwidthEstimateWeightLimit")) { - tweaksConfig.bandwidthEstimateWeightLimit = json.getInt("bandwidthEstimateWeightLimit") - } - if (json.hasKey("devicesThatRequireSurfaceWorkaround")) { - val devices = json.getMap("devicesThatRequireSurfaceWorkaround") - val deviceNames = devices?.getArray("deviceNames") - ?.toList() - ?.mapNotNull { it } - ?.map { DeviceName(it) } - ?: emptyList() - val modelNames = devices?.getArray("modelNames") - ?.toList() - ?.mapNotNull { it } - ?.map { ModelName(it) } - ?: emptyList() - tweaksConfig.devicesThatRequireSurfaceWorkaround = deviceNames + modelNames - } - if (json.hasKey("languagePropertyNormalization")) { - tweaksConfig.languagePropertyNormalization = json.getBoolean("languagePropertyNormalization") - } - if (json.hasKey("localDynamicDashWindowUpdateInterval")) { - tweaksConfig.localDynamicDashWindowUpdateInterval = json.getDouble( - "localDynamicDashWindowUpdateInterval", - ) - } - if (json.hasKey("shouldApplyTtmlRegionWorkaround")) { - tweaksConfig.shouldApplyTtmlRegionWorkaround = json.getBoolean("shouldApplyTtmlRegionWorkaround") - } - if (json.hasKey("useDrmSessionForClearPeriods")) { - tweaksConfig.useDrmSessionForClearPeriods = json.getBoolean("useDrmSessionForClearPeriods") - } - if (json.hasKey("useDrmSessionForClearSources")) { - tweaksConfig.useDrmSessionForClearSources = json.getBoolean("useDrmSessionForClearSources") - } - if (json.hasKey("useFiletypeExtractorFallbackForHls")) { - tweaksConfig.useFiletypeExtractorFallbackForHls = json.getBoolean("useFiletypeExtractorFallbackForHls") - } - return tweaksConfig - } +/** + * Converts any JS object into a `StyleConfig` object. + */ +fun ReadableMap.toStyleConfig(): StyleConfig = StyleConfig().apply { + withBoolean("isUiEnabled") { isUiEnabled = it } + getString("playerUiCss")?.takeIf { it.isNotEmpty() }?.let { playerUiCss = it } + getString("supplementalPlayerUiCss")?.takeIf { it.isNotEmpty() }?.let { supplementalPlayerUiCss = it } + getString("playerUiJs")?.takeIf { it.isNotEmpty() }?.let { playerUiJs = it } + withString("scalingMode") { scalingMode = ScalingMode.valueOf(it) } +} - /** - * Converts any JS object into an `AdvertisingConfig` object. - * @param json JS object representing the `AdvertisingConfig`. - * @return The generated `AdvertisingConfig` if successful, `null` otherwise. - */ - @JvmStatic - fun toAdvertisingConfig(json: ReadableMap?): AdvertisingConfig? = json?.getArray("schedule") - ?.toList() - ?.mapNotNull(::toAdItem) - ?.let { AdvertisingConfig(it) } - - /** - * Converts any JS object into an `AdItem` object. - * @param json JS object representing the `AdItem`. - * @return The generated `AdItem` if successful, `null` otherwise. - */ - @JvmStatic - fun toAdItem(json: ReadableMap?): AdItem? { - val sources = json?.getArray("sources") - ?.toList() - ?.mapNotNull(::toAdSource) - ?.toTypedArray() - ?: return null - return AdItem(sources, json.getString("position") ?: "pre") - } +/** + * Converts any JS object into a `TweaksConfig` object. + */ +fun ReadableMap.toTweaksConfig(): TweaksConfig = TweaksConfig().apply { + withDouble("timeChangedInterval") { timeChangedInterval = it } + withInt("bandwidthEstimateWeightLimit") { bandwidthEstimateWeightLimit = it } + withMap("devicesThatRequireSurfaceWorkaround") { devices -> + val deviceNames = devices.withStringArray("deviceNames") { + it.filterNotNull().map(::DeviceName) + } ?: emptyList() + val modelNames = devices.withStringArray("modelNames") { + it.filterNotNull().map(::DeviceName) + } ?: emptyList() + devicesThatRequireSurfaceWorkaround = deviceNames + modelNames + } + withBoolean("languagePropertyNormalization") { languagePropertyNormalization = it } + withDouble("localDynamicDashWindowUpdateInterval") { localDynamicDashWindowUpdateInterval = it } + withBoolean("shouldApplyTtmlRegionWorkaround") { shouldApplyTtmlRegionWorkaround = it } + withBoolean("useDrmSessionForClearPeriods") { useDrmSessionForClearPeriods = it } + withBoolean("useDrmSessionForClearSources") { useDrmSessionForClearSources = it } + withBoolean("useFiletypeExtractorFallbackForHls") { useFiletypeExtractorFallbackForHls = it } +} - /** - * Converts any JS object into an `AdSource` object. - * @param json JS object representing the `AdSource`. - * @return The generated `AdSource` if successful, `null` otherwise. - */ - @JvmStatic - fun toAdSource(json: ReadableMap?): AdSource? = json?.getString("tag")?.let { - AdSource(toAdSourceType(json.getString("type")), it) - } +/** + * Converts any JS object into an `AdvertisingConfig` object. + */ +fun ReadableMap.toAdvertisingConfig(): AdvertisingConfig? { + return AdvertisingConfig( + getArray("schedule")?.toMapList()?.mapNotNull { it?.toAdItem() } ?: return null, + ) +} - /** - * Converts any JS string into an `AdSourceType` enum value. - * @param json JS string representing the `AdSourceType`. - * @return The generated `AdSourceType`. - */ - @JvmStatic - fun toAdSourceType(json: String?): AdSourceType = when (json) { - "ima" -> AdSourceType.Ima - "progressive" -> AdSourceType.Progressive - else -> AdSourceType.Unknown - } +/** + * Converts any JS object into an `AdItem` object. + */ +fun ReadableMap.toAdItem(): AdItem? { + return AdItem( + sources = getArray("sources") ?.toMapList()?.mapNotNull { it?.toAdSource() }?.toTypedArray() ?: return null, + position = getString("position") ?: "pre", + ) +} - /** - * Converts an arbitrary `json` to `SourceConfig`. - * @param json JS object representing the `SourceConfig`. - * @return The generated `SourceConfig` if successful, `null` otherwise. - */ - @JvmStatic - fun toSourceConfig(json: ReadableMap?): SourceConfig? { - val url = json?.getString("url") - val type = json?.getString("type") - if (json == null || url == null || type == null) { - return null - } - val config = SourceConfig(url, toSourceType(type)) - config.title = json.getString("title") - config.description = json.getString("description") - config.posterSource = json.getString("poster") - if (json.hasKey("isPosterPersistent")) { - config.isPosterPersistent = json.getBoolean("isPosterPersistent") - } - if (json.hasKey("subtitleTracks")) { - val subtitleTracks = json.getArray("subtitleTracks") as ReadableArray - for (i in 0 until subtitleTracks.size()) { - toSubtitleTrack(subtitleTracks.getMap(i))?.let { - config.addSubtitleTrack(it) - } - } - } - if (json.hasKey("thumbnailTrack")) { - config.thumbnailTrack = toThumbnailTrack(json.getString("thumbnailTrack")) - } - if (json.hasKey("metadata")) { - config.metadata = json.getMap("metadata") - ?.toHashMap() - ?.mapValues { entry -> entry.value as String } - } - if (json.hasKey("options")) { - config.options = toSourceOptions(json.getMap("options")) - } - return config - } +/** + * Converts any JS object into an `AdSource` object. + */ +fun ReadableMap.toAdSource(): AdSource? { + return AdSource( + type = getString("type")?.toAdSourceType() ?: return null, + tag = getString("tag") ?: return null, + ) +} - /** - * Converts an arbitrary `json` to `SourceType`. - * @param json JS string representing the `SourceType`. - * @return The generated `SourceType` if successful or `SourceType.Dash` otherwise. - */ - @JvmStatic - fun toSourceType(json: String?): SourceType = when (json) { - "dash" -> SourceType.Dash - "hls" -> SourceType.Hls - "smooth" -> SourceType.Smooth - "progressive" -> SourceType.Progressive - else -> SourceType.Dash - } +/** + * Converts any JS string into an `AdSourceType` enum value. + */ +private fun String.toAdSourceType(): AdSourceType? = when (this) { + "ima" -> AdSourceType.Ima + "progressive" -> AdSourceType.Progressive + "unknown" -> AdSourceType.Unknown + else -> null +} - /** - * Converts any given `Source` object into its `json` representation. - * @param source `Source` object to be converted. - * @return The `json` representation of the given `Source`. - */ - @JvmStatic - fun fromSource(source: Source?): WritableMap? { - if (source == null) { - return null - } - val json = Arguments.createMap() - json.putDouble("duration", source.duration) - json.putBoolean("isActive", source.isActive) - json.putBoolean("isAttachedToPlayer", source.isAttachedToPlayer) - json.putInt("loadingState", source.loadingState.ordinal) - json.putMap("metadata", source.config.metadata?.toReadableMap()) - return json - } +/** + * Converts an arbitrary `json` to `SourceConfig`. + */ +fun ReadableMap.toSourceConfig(): SourceConfig? { + val url = getString("url") ?: return null + val type = getString("type")?.toSourceType() ?: return null + return SourceConfig(url, type).apply { + withString("title") { title = it } + withString("description") { description = it } + withString("poster") { posterSource = it } + withBoolean("isPosterPersistent") { isPosterPersistent = it } + withArray("subtitleTracks") { subtitleTracks -> + for (i in 0 until subtitleTracks.size()) { + subtitleTracks.getMap(i).toSubtitleTrack()?.let { + addSubtitleTrack(it) + } + } + } + withString("thumbnailTrack") { thumbnailTrack = it.toThumbnailTrack() } + withMap("metadata") { metadata = it.toMap() } + withMap("options") { options = it.toSourceOptions() } + } +} - /** - * Converts any given `SeekPosition` object into its `json` representation. - * @param seekPosition `SeekPosition` object to be converted. - * @return The `json` representation of the given `SeekPosition`. - */ - @JvmStatic - fun fromSeekPosition(seekPosition: SeekPosition): WritableMap? { - val json = Arguments.createMap() - json.putDouble("time", seekPosition.time) - json.putMap("source", fromSource(seekPosition.source)) - return json - } +/** + * Converts an arbitrary `json` to `SourceType`. + */ +fun String.toSourceType(): SourceType? = when (this) { + "dash" -> SourceType.Dash + "hls" -> SourceType.Hls + "smooth" -> SourceType.Smooth + "progressive" -> SourceType.Progressive + else -> null +} - /** - * Converts any given `SourceEvent` object into its `json` representation. - * @param event `SourceEvent` object to be converted. - * @return The `json` representation of the given `SourceEvent`. - */ - @JvmStatic - fun fromSourceEvent(event: SourceEvent): WritableMap? { - val json = Arguments.createMap() - json.putString("name", event.getName()) - json.putDouble("timestamp", event.timestamp.toDouble()) - when (event) { - is SourceEvent.Load -> { - json.putMap("source", fromSource(event.source)) - } +/** + * Converts any given `Source` object into its `json` representation. + */ +fun Source.toJson(): WritableMap = Arguments.createMap().apply { + putDouble("duration", duration) + putBoolean("isActive", isActive) + putBoolean("isAttachedToPlayer", isAttachedToPlayer) + putInt("loadingState", loadingState.ordinal) + putMap("metadata", config.metadata?.toReadableMap()) +} - is SourceEvent.Loaded -> { - json.putMap("source", fromSource(event.source)) - } +/** + * Converts any given `SeekPosition` object into its `json` representation. + */ +fun SeekPosition.toJson(): WritableMap = Arguments.createMap().apply { + putDouble("time", time) + putMap("source", source.toJson()) +} - is SourceEvent.Error -> { - json.putInt("code", event.code.value) - json.putString("message", event.message) - } +/** + * Converts any given `SourceEvent` object into its `json` representation. + */ +fun SourceEvent.toJson(): WritableMap { + val json = Arguments.createMap() + json.putString("name", getName()) + json.putDouble("timestamp", timestamp.toDouble()) + when (this) { + is SourceEvent.Load -> { + json.putMap("source", source.toJson()) + } - is SourceEvent.Warning -> { - json.putInt("code", event.code.value) - json.putString("message", event.message) - } + is SourceEvent.Loaded -> { + json.putMap("source", source.toJson()) + } - is SourceEvent.AudioTrackAdded -> { - json.putMap("audioTrack", fromAudioTrack(event.audioTrack)) - } + is SourceEvent.Error -> { + json.putInt("code", code.value) + json.putString("message", message) + } - is SourceEvent.AudioTrackChanged -> { - json.putMap("oldAudioTrack", fromAudioTrack(event.oldAudioTrack)) - json.putMap("newAudioTrack", fromAudioTrack(event.newAudioTrack)) - } + is SourceEvent.Warning -> { + json.putInt("code", code.value) + json.putString("message", message) + } - is SourceEvent.AudioTrackRemoved -> { - json.putMap("audioTrack", fromAudioTrack(event.audioTrack)) - } + is SourceEvent.AudioTrackAdded -> { + json.putMap("audioTrack", audioTrack.toJson()) + } - is SourceEvent.SubtitleTrackAdded -> { - json.putMap("subtitleTrack", fromSubtitleTrack(event.subtitleTrack)) - } + is SourceEvent.AudioTrackChanged -> { + json.putMap("oldAudioTrack", oldAudioTrack?.toJson()) + json.putMap("newAudioTrack", newAudioTrack?.toJson()) + } - is SourceEvent.SubtitleTrackRemoved -> { - json.putMap("subtitleTrack", fromSubtitleTrack(event.subtitleTrack)) - } + is SourceEvent.AudioTrackRemoved -> { + json.putMap("audioTrack", audioTrack.toJson()) + } - is SourceEvent.SubtitleTrackChanged -> { - json.putMap("oldSubtitleTrack", fromSubtitleTrack(event.oldSubtitleTrack)) - json.putMap("newSubtitleTrack", fromSubtitleTrack(event.newSubtitleTrack)) - } + is SourceEvent.SubtitleTrackAdded -> { + json.putMap("subtitleTrack", subtitleTrack.toJson()) + } - is SourceEvent.DownloadFinished -> { - json.putDouble("downloadTime", event.downloadTime) - json.putString("requestType", event.downloadType.toString()) - json.putInt("httpStatus", event.httpStatus) - json.putBoolean("isSuccess", event.isSuccess) - event.lastRedirectLocation?.let { - json.putString("lastRedirectLocation", it) - } - json.putDouble("size", event.size.toDouble()) - json.putString("url", event.url) - } + is SourceEvent.SubtitleTrackRemoved -> { + json.putMap("subtitleTrack", subtitleTrack.toJson()) + } - is SourceEvent.VideoDownloadQualityChanged -> { - json.putMap("newVideoQuality", fromVideoQuality(event.newVideoQuality)) - json.putMap("oldVideoQuality", fromVideoQuality(event.oldVideoQuality)) - } + is SourceEvent.SubtitleTrackChanged -> { + json.putMap("oldSubtitleTrack", oldSubtitleTrack?.toJson()) + json.putMap("newSubtitleTrack", newSubtitleTrack?.toJson()) + } - else -> { - // Event is not supported yet or does not have any additional data - } + is SourceEvent.DownloadFinished -> { + json.putDouble("downloadTime", downloadTime) + json.putString("requestType", downloadType.toString()) + json.putInt("httpStatus", httpStatus) + json.putBoolean("isSuccess", isSuccess) + lastRedirectLocation?.let { + json.putString("lastRedirectLocation", it) } - return json + json.putDouble("size", size.toDouble()) + json.putString("url", url) } - /** - * Converts any given `PlayerEvent` object into its `json` representation. - * @param event `PlayerEvent` object to be converted. - * @return The `json` representation of given `PlayerEvent`. - */ - @JvmStatic - fun fromPlayerEvent(event: PlayerEvent): WritableMap? { - val json = Arguments.createMap() - json.putString("name", event.getName()) - json.putDouble("timestamp", event.timestamp.toDouble()) - when (event) { - is PlayerEvent.Error -> { - json.putInt("code", event.code.value) - json.putString("message", event.message) - } - - is PlayerEvent.Warning -> { - json.putInt("code", event.code.value) - json.putString("message", event.message) - } - - is PlayerEvent.Play -> { - json.putDouble("time", event.time) - } - - is PlayerEvent.Playing -> { - json.putDouble("time", event.time) - } - - is PlayerEvent.Paused -> { - json.putDouble("time", event.time) - } - - is PlayerEvent.TimeChanged -> { - json.putDouble("currentTime", event.time) - } - - is PlayerEvent.Seek -> { - json.putMap("from", fromSeekPosition(event.from)) - json.putMap("to", fromSeekPosition(event.to)) - } - - is PlayerEvent.TimeShift -> { - json.putDouble("position", event.position) - json.putDouble("targetPosition", event.target) - } - - is PlayerEvent.PictureInPictureAvailabilityChanged -> { - json.putBoolean("isPictureInPictureAvailable", event.isPictureInPictureAvailable) - } - - is PlayerEvent.AdBreakFinished -> { - json.putMap("adBreak", fromAdBreak(event.adBreak)) - } - - is PlayerEvent.AdBreakStarted -> { - json.putMap("adBreak", fromAdBreak(event.adBreak)) - } - - is PlayerEvent.AdClicked -> { - json.putString("clickThroughUrl", event.clickThroughUrl) - } - - is PlayerEvent.AdError -> { - json.putInt("code", event.code) - json.putString("message", event.message) - json.putMap("adConfig", fromAdConfig(event.adConfig)) - json.putMap("adItem", fromAdItem(event.adItem)) - } - - is PlayerEvent.AdFinished -> { - json.putMap("ad", fromAd(event.ad)) - } - - is PlayerEvent.AdManifestLoad -> { - json.putMap("adBreak", fromAdBreak(event.adBreak)) - json.putMap("adConfig", fromAdConfig(event.adConfig)) - } - - is PlayerEvent.AdManifestLoaded -> { - json.putMap("adBreak", fromAdBreak(event.adBreak)) - json.putMap("adConfig", fromAdConfig(event.adConfig)) - json.putDouble("downloadTime", event.downloadTime.toDouble()) - } - - is PlayerEvent.AdQuartile -> { - json.putString("quartile", fromAdQuartile(event.quartile)) - } - - is PlayerEvent.AdScheduled -> { - json.putInt("numberOfAds", event.numberOfAds) - } - - is PlayerEvent.AdSkipped -> { - json.putMap("ad", fromAd(event.ad)) - } - - is PlayerEvent.AdStarted -> { - json.putMap("ad", fromAd(event.ad)) - json.putString("clickThroughUrl", event.clickThroughUrl) - json.putString("clientType", fromAdSourceType(event.clientType)) - json.putDouble("duration", event.duration) - json.putInt("indexInQueue", event.indexInQueue) - json.putString("position", event.position) - json.putDouble("skipOffset", event.skipOffset) - json.putDouble("timeOffset", event.timeOffset) - } + is SourceEvent.VideoDownloadQualityChanged -> { + json.putMap("newVideoQuality", newVideoQuality?.toJson()) + json.putMap("oldVideoQuality", oldVideoQuality?.toJson()) + } - is PlayerEvent.VideoPlaybackQualityChanged -> { - json.putMap("newVideoQuality", fromVideoQuality(event.newVideoQuality)) - json.putMap("oldVideoQuality", fromVideoQuality(event.oldVideoQuality)) - } + else -> { + // Event is not supported yet or does not have any additional data + } + } + return json +} - is PlayerEvent.CastWaitingForDevice -> { - json.putMap("castPayload", fromCastPayload(event.castPayload)) - } +/** + * Converts any given `PlayerEvent` object into its `json` representation. + */ +fun PlayerEvent.toJson(): WritableMap { + val json = Arguments.createMap() + json.putString("name", getName()) + json.putDouble("timestamp", timestamp.toDouble()) + when (this) { + is PlayerEvent.Error -> { + json.putInt("code", code.value) + json.putString("message", message) + } - is PlayerEvent.CastStarted -> { - json.putString("deviceName", event.deviceName) - } + is PlayerEvent.Warning -> { + json.putInt("code", code.value) + json.putString("message", message) + } - else -> { - // Event is not supported yet or does not have any additional data - } - } - return json + is PlayerEvent.Play -> { + json.putDouble("time", time) } - /** - * Converts an arbitrary `json` into [BitmovinCastManagerOptions]. - * @param json JS object representing the [BitmovinCastManagerOptions]. - * @return The generated [BitmovinCastManagerOptions] if successful, `null` otherwise. - */ - fun toCastOptions(json: ReadableMap?): BitmovinCastManagerOptions? { - if (json == null) return null - return BitmovinCastManagerOptions( - json.getOrDefault("applicationId", null), - json.getOrDefault("messageNamespace", null), - ) + is PlayerEvent.Playing -> { + json.putDouble("time", time) } - /** - * Converts an arbitrary `json` to `WidevineConfig`. - * @param json JS object representing the `WidevineConfig`. - * @return The generated `WidevineConfig` if successful, `null` otherwise. - */ - @JvmStatic - fun toWidevineConfig(json: ReadableMap?): WidevineConfig? = json - ?.getMap("widevine") - ?.let { - WidevineConfig(it.getString("licenseUrl")) - .apply { - if (it.hasKey("preferredSecurityLevel")) { - preferredSecurityLevel = it.getString("preferredSecurityLevel") - } - if (it.hasKey("shouldKeepDrmSessionsAlive")) { - shouldKeepDrmSessionsAlive = it.getBoolean("shouldKeepDrmSessionsAlive") - } - if (it.hasKey("httpHeaders")) { - httpHeaders = it.getMap("httpHeaders") - ?.toHashMap() - ?.mapValues { entry -> entry.value as String } - ?.toMutableMap() - } - } - } + is PlayerEvent.Paused -> { + json.putDouble("time", time) + } - /** - * Converts an `url` string into a `ThumbnailsTrack`. - * @param url JS object representing the `ThumbnailsTrack`. - * @return The generated `ThumbnailsTrack` if successful, `null` otherwise. - */ - @JvmStatic - fun toThumbnailTrack(url: String?): ThumbnailTrack? { - if (url == null) { - return null - } - return ThumbnailTrack(url) + is PlayerEvent.TimeChanged -> { + json.putDouble("currentTime", time) } - /** - * Converts any `AudioTrack` into its json representation. - * @param audioTrack `AudioTrack` object to be converted. - * @return The generated json map. - */ - @JvmStatic - fun fromAudioTrack(audioTrack: AudioTrack?): WritableMap? { - if (audioTrack == null) { - return null - } - val json = Arguments.createMap() - json.putString("url", audioTrack.url) - json.putString("label", audioTrack.label) - json.putBoolean("isDefault", audioTrack.isDefault) - json.putString("identifier", audioTrack.id) - json.putString("language", audioTrack.language) - return json + is PlayerEvent.Seek -> { + json.putMap("from", from.toJson()) + json.putMap("to", to.toJson()) } - /** - * Converts an arbitrary `json` into a `SubtitleTrack`. - * @param json JS object representing the `SubtitleTrack`. - * @return The generated `SubtitleTrack` if successful, `null` otherwise. - */ - @JvmStatic - fun toSubtitleTrack(json: ReadableMap?): SubtitleTrack? { - val url = json?.getString("url") - val label = json?.getString("label") - if (json == null || url == null || label == null) { - return null - } - val identifier = json.getString("identifier") ?: UUID.randomUUID().toString() - val isDefault = if (json.hasKey("isDefault")) { - json.getBoolean("isDefault") - } else { - false - } - val isForced = if (json.hasKey("isForced")) { - json.getBoolean("isForced") - } else { - false - } - val format = json.getString("format") - if (!format.isNullOrBlank()) { - return SubtitleTrack( - url = url, - label = label, - id = identifier, - isDefault = isDefault, - language = json.getString("language"), - isForced = isForced, - mimeType = toSubtitleMimeType(format), - ) - } - return SubtitleTrack( - url = url, - label = label, - id = identifier, - isDefault = isDefault, - language = json.getString("language"), - isForced = isForced, - ) + is PlayerEvent.TimeShift -> { + json.putDouble("position", position) + json.putDouble("targetPosition", target) } - /** - * Converts any subtitle format name in its mime type representation. - * @param format The file format string received from JS. - * @return The subtitle file mime type. - */ - @JvmStatic - fun toSubtitleMimeType(format: String?): String? { - if (format == null) { - return null - } - return "text/$format" + is PlayerEvent.PictureInPictureAvailabilityChanged -> { + json.putBoolean("isPictureInPictureAvailable", isPictureInPictureAvailable) } - /** - * Converts any `SubtitleTrack` into its json representation. - * @param subtitleTrack `SubtitleTrack` object to be converted. - * @return The generated json map. - */ - @JvmStatic - fun fromSubtitleTrack(subtitleTrack: SubtitleTrack?): WritableMap? { - if (subtitleTrack == null) { - return null - } - val json = Arguments.createMap() - json.putString("url", subtitleTrack.url) - json.putString("label", subtitleTrack.label) - json.putBoolean("isDefault", subtitleTrack.isDefault) - json.putString("identifier", subtitleTrack.id) - json.putString("language", subtitleTrack.language) - json.putBoolean("isForced", subtitleTrack.isForced) - json.putString("format", fromSubtitleMimeType(subtitleTrack.mimeType)) - return json + is PlayerEvent.AdBreakFinished -> { + json.putMap("adBreak", adBreak?.toJson()) } - /** - * Converts any subtitle track mime type into its json representation (file format value). - * @param mimeType `SubtitleTrack` file mime type. - * @return The extracted file format. - */ - @JvmStatic - fun fromSubtitleMimeType(mimeType: String?): String? { - if (mimeType == null) { - return null - } - return mimeType.split("/").last() + is PlayerEvent.AdBreakStarted -> { + json.putMap("adBreak", adBreak?.toJson()) } - /** - * Converts any `AdBreak` object into its json representation. - * @param adBreak `AdBreak` object. - * @return The produced JS object. - */ - @JvmStatic - fun fromAdBreak(adBreak: AdBreak?): WritableMap? = adBreak?.let { - Arguments.createMap().apply { - putArray("ads", it.ads.mapNotNull(::fromAd).toReadableArray()) - putString("id", it.id) - putDouble("scheduleTime", it.scheduleTime) - } + is PlayerEvent.AdClicked -> { + json.putString("clickThroughUrl", clickThroughUrl) } - /** - * Converts any `Ad` object into its json representation. - * @param ad `Ad` object. - * @return The produced JS object. - */ - @JvmStatic - fun fromAd(ad: Ad?): WritableMap? = ad?.let { - Arguments.createMap().apply { - putString("clickThroughUrl", it.clickThroughUrl) - putMap("data", fromAdData(it.data)) - putInt("height", it.height) - putString("id", it.id) - putBoolean("isLinear", it.isLinear) - putString("mediaFileUrl", it.mediaFileUrl) - putInt("width", it.width) - } + is PlayerEvent.AdError -> { + json.putInt("code", code) + json.putString("message", message) + json.putMap("adConfig", adConfig?.toJson()) + json.putMap("adItem", adItem?.toJson()) } - /** - * Converts any `AdData` object into its json representation. - * @param adData `AdData` object. - * @return The produced JS object. - */ - @JvmStatic - fun fromAdData(adData: AdData?): WritableMap? = adData?.let { - Arguments.createMap().apply { - putInt("bitrate", it.bitrate) - putInt("maxBitrate", it.maxBitrate) - putString("mimeType", it.mimeType) - putInt("minBitrate", it.minBitrate) - } + is PlayerEvent.AdFinished -> { + json.putMap("ad", ad?.toJson()) } - /** - * Converts any `AdConfig` object into its json representation. - * @param adConfig `AdConfig` object. - * @return The produced JS object. - */ - @JvmStatic - fun fromAdConfig(adConfig: AdConfig?): WritableMap? = adConfig?.let { - Arguments.createMap().apply { - putDouble("replaceContentDuration", it.replaceContentDuration) - } + is PlayerEvent.AdManifestLoad -> { + json.putMap("adBreak", adBreak?.toJson()) + json.putMap("adConfig", adConfig.toJson()) } - /** - * Converts any `AdItem` object into its json representation. - * @param adItem `AdItem` object. - * @return The produced JS object. - */ - @JvmStatic - fun fromAdItem(adItem: AdItem?): WritableMap? = adItem?.let { - Arguments.createMap().apply { - putString("position", it.position) - putArray("sources", it.sources.mapNotNull(::fromAdSource).toReadableArray()) - } + is PlayerEvent.AdManifestLoaded -> { + json.putMap("adBreak", adBreak?.toJson()) + json.putMap("adConfig", adConfig.toJson()) + json.putDouble("downloadTime", downloadTime.toDouble()) } - /** - * Converts any `AdSource` object into its json representation. - * @param adSource `AdSource` object. - * @return The produced JS object. - */ - @JvmStatic - fun fromAdSource(adSource: AdSource?): WritableMap? = adSource?.let { - Arguments.createMap().apply { - putString("tag", it.tag) - putString("type", fromAdSourceType(it.type)) - } + is PlayerEvent.AdQuartile -> { + json.putString("quartile", quartile.toJson()) } - /** - * Converts any `AdSourceType` value into its json representation. - * @param adSourceType `AdSourceType` value. - * @return The produced JS string. - */ - @JvmStatic - fun fromAdSourceType(adSourceType: AdSourceType?): String? = when (adSourceType) { - AdSourceType.Ima -> "ima" - AdSourceType.Unknown -> "unknown" - AdSourceType.Progressive -> "progressive" - else -> null + is PlayerEvent.AdScheduled -> { + json.putInt("numberOfAds", numberOfAds) } - /** - * Converts any `AdQuartile` value into its json representation. - * @param adQuartile `AdQuartile` value. - * @return The produced JS string. - */ - @JvmStatic - fun fromAdQuartile(adQuartile: AdQuartile?): String? = when (adQuartile) { - AdQuartile.FirstQuartile -> "first" - AdQuartile.MidPoint -> "mid_point" - AdQuartile.ThirdQuartile -> "third" - else -> null + is PlayerEvent.AdSkipped -> { + json.putMap("ad", ad?.toJson()) } - /** - * Converts an arbitrary json object into a `BitmovinAnalyticsConfig`. - * @param json JS object representing the `BitmovinAnalyticsConfig`. - * @return The produced `BitmovinAnalyticsConfig` or null. - */ - @JvmStatic - fun toAnalyticsConfig(json: ReadableMap?): AnalyticsConfig? = json?.let { - val licenseKey = it.getString("licenseKey") ?: return null - - return AnalyticsConfig.Builder(licenseKey).apply { - it.getBooleanOrNull("adTrackingDisabled")?.let { adTrackingDisabled -> - setAdTrackingDisabled(adTrackingDisabled) - } - it.getBooleanOrNull("randomizeUserId")?.let { randomizeUserId -> - setRandomizeUserId(randomizeUserId) - } - }.build() + is PlayerEvent.AdStarted -> { + json.putMap("ad", ad?.toJson()) + json.putString("clickThroughUrl", clickThroughUrl) + json.putString("clientType", clientType?.toJson()) + json.putDouble("duration", duration) + json.putInt("indexInQueue", indexInQueue) + json.putString("position", position) + json.putDouble("skipOffset", skipOffset) + json.putDouble("timeOffset", timeOffset) } - /** - * Converts an arbitrary json object into an analytics `DefaultMetadata`. - * @param json JS object representing the `CustomData`. - * @return The produced `CustomData` or null. - */ - @JvmStatic - fun toAnalyticsDefaultMetadata(json: ReadableMap?): DefaultMetadata? { - if (json == null) return null - - return DefaultMetadata.Builder().apply { - toAnalyticsCustomData(json)?.let { - setCustomData(it) - } - json.getString("cdnProvider")?.let { cdnProvider -> - setCdnProvider(cdnProvider) - } - json.getString("customUserId")?.let { customUserId -> - setCustomUserId(customUserId) - } - }.build() + is PlayerEvent.VideoPlaybackQualityChanged -> { + json.putMap("newVideoQuality", newVideoQuality?.toJson()) + json.putMap("oldVideoQuality", oldVideoQuality?.toJson()) } - /** - * Converts an arbitrary json object into an analytics `CustomData`. - * @param json JS object representing the `CustomData`. - * @return The produced `CustomData` or null. - */ - @JvmStatic - fun toAnalyticsCustomData(json: ReadableMap?): CustomData? { - if (json == null) return null - - return CustomData.Builder().apply { - for (n in 1..30) { - setProperty( - "customData$n", - json.getString("customData$n") ?: continue, - ) - } - json.getString("experimentName")?.let { - setExperimentName(it) - } - }.build() + is PlayerEvent.CastWaitingForDevice -> { + json.putMap("castPayload", castPayload.toJson()) } - /** - * Converts an arbitrary analytics `CustomData` object into a JS value. - * @param customData `CustomData` to be converted. - * @return The produced JS value or null. - */ - @JvmStatic - fun fromAnalyticsCustomData(customData: CustomData?): WritableMap? = customData?.let { - val json = Arguments.createMap() - for (n in 1..30) { - it.getProperty("customData$n")?.let { customDataN -> - json.putString("customData$n", customDataN) - } - } - it.experimentName?.let { experimentName -> - json.putString("experimentName", experimentName) - } - json + is PlayerEvent.CastStarted -> { + json.putString("deviceName", deviceName) } - @JvmStatic - fun toAnalyticsSourceMetadata(json: ReadableMap?): SourceMetadata? = json?.let { - val sourceCustomData = toAnalyticsCustomData(json) ?: CustomData() - SourceMetadata( - title = it.getString("title"), - videoId = it.getString("videoId"), - cdnProvider = it.getString("cdnProvider"), - path = it.getString("path"), - isLive = it.getBoolean("isLive"), - customData = sourceCustomData, - ) + else -> { + // Event is not supported yet or does not have any additional data } + } + return json +} - @JvmStatic - fun fromAnalyticsSourceMetadata(sourceMetadata: SourceMetadata?): ReadableMap? { - if (sourceMetadata == null) return null +/** + * Converts an arbitrary `json` into [BitmovinCastManagerOptions]. + */ +fun ReadableMap.toCastOptions(): BitmovinCastManagerOptions = BitmovinCastManagerOptions( + applicationId = getString("applicationId"), + messageNamespace = getString("messageNamespace"), +) - 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 an arbitrary `json` to `WidevineConfig`. + */ +fun ReadableMap.toWidevineConfig(): WidevineConfig? = getMap("widevine")?.run { + WidevineConfig(getString("licenseUrl")).apply { + withString("preferredSecurityLevel") { preferredSecurityLevel = it } + withBoolean("shouldKeepDrmSessionsAlive") { shouldKeepDrmSessionsAlive = it } + withMap("httpHeaders") { httpHeaders = it.toMap().toMutableMap() } + } +} - /** - * Converts any `VideoQuality` value into its json representation. - * @param videoQuality `VideoQuality` value. - * @return The produced JS string. - */ - @JvmStatic - fun fromVideoQuality(videoQuality: VideoQuality?): WritableMap? = videoQuality?.let { - Arguments.createMap().apply { - putString("id", videoQuality.id) - putString("label", videoQuality.label) - putInt("bitrate", videoQuality.bitrate) - putString("codec", videoQuality.codec) - putDouble("frameRate", videoQuality.frameRate.toDouble()) - putInt("height", videoQuality.height) - putInt("width", videoQuality.width) - } - } +/** + * Converts an `url` string into a `ThumbnailsTrack`. + */ +fun String.toThumbnailTrack(): ThumbnailTrack = ThumbnailTrack(this) - /** - * Converts any `OfflineOptionEntry` into its json representation. - * @param offlineEntry `OfflineOptionEntry` object to be converted. - * @return The generated json map. - */ - @JvmStatic - fun toJson(offlineEntry: OfflineOptionEntry): WritableMap { - return Arguments.createMap().apply { - putString("id", offlineEntry.id) - putString("language", offlineEntry.language) - } - } +/** + * Converts any `AudioTrack` into its json representation. + */ +fun AudioTrack.toJson(): WritableMap = Arguments.createMap().apply { + putString("url", url) + putString("label", label) + putBoolean("isDefault", isDefault) + putString("identifier", id) + putString("language", language) +} - /** - * Converts any `OfflineContentOptions` into its json representation. - * @param options `OfflineContentOptions` object to be converted. - * @return The generated json map. - */ - @JvmStatic - fun toJson(options: OfflineContentOptions?): WritableMap? { - if (options == null) { - return null - } +/** + * Converts an arbitrary `json` into a `SubtitleTrack`. + */ +fun ReadableMap.toSubtitleTrack(): SubtitleTrack? { + return SubtitleTrack( + url = getString("url") ?: return null, + label = getString("label") ?: return null, + id = getString("identifier") ?: UUID.randomUUID().toString(), + isDefault = getBooleanOrNull("isDefault") ?: false, + language = getString("language"), + isForced = getBooleanOrNull("isForced") ?: false, + mimeType = getString("format")?.takeIf { it.isNotEmpty() }?.toSubtitleMimeType(), + ) +} - return Arguments.createMap().apply { - putArray("audioOptions", options.audioOptions.map { toJson(it) }.toReadableArray()) - putArray("textOptions", options.textOptions.map { toJson(it) }.toReadableArray()) - } - } +/** + * Converts any subtitle format name in its mime type representation. + */ +private fun String.toSubtitleMimeType(): String = "text/$this" - @JvmStatic - fun fromThumbnail(thumbnail: Thumbnail?): WritableMap? { - if (thumbnail == null) { - return null - } +/** + * Converts any `SubtitleTrack` into its json representation. + */ +fun SubtitleTrack.toJson(): WritableMap = Arguments.createMap().apply { + putString("url", url) + putString("label", label) + putBoolean("isDefault", isDefault) + putString("identifier", id) + putString("language", language) + putBoolean("isForced", isForced) + putString("format", mimeType?.textMimeTypeToJson()) +} - return Arguments.createMap().apply { - putDouble("start", thumbnail.start) - putDouble("end", thumbnail.end) - putString("text", thumbnail.text) - putString("url", thumbnail.uri.toString()) - putInt("x", thumbnail.x) - putInt("y", thumbnail.y) - putInt("width", thumbnail.width) - putInt("height", thumbnail.height) - } - } +/** + * Converts any subtitle track mime type into its json representation (file format value). + */ +private fun String.textMimeTypeToJson(): String = split("/").last() - @JvmStatic - fun toPictureInPictureConfig(json: ReadableMap?): RNPictureInPictureHandler.PictureInPictureConfig? = - json?.let { - RNPictureInPictureHandler.PictureInPictureConfig( - isEnabled = it.getBoolean("isEnabled"), - ) - } +/** + * Converts any `AdBreak` object into its json representation. + */ +fun AdBreak.toJson(): WritableMap = Arguments.createMap().apply { + putArray("ads", ads.mapToReactArray { it.toJson() }) + putString("id", id) + putDouble("scheduleTime", scheduleTime) +} - /** - * Converts the [json] to a `RNUiConfig` object. - */ - fun toPlayerViewConfig(json: ReadableMap) = PlayerViewConfig( - uiConfig = UiConfig.WebUi( - playbackSpeedSelectionEnabled = json.getMap("uiConfig") - ?.getBooleanOrNull("playbackSpeedSelectionEnabled") - ?: true, - ), - ) - - private fun toUserInterfaceTypeFromPlayerConfig(json: ReadableMap): UserInterfaceType = - when (json.getMap("styleConfig")?.getString("userInterfaceType")) { - "Subtitle" -> UserInterfaceType.Subtitle - "Bitmovin" -> UserInterfaceType.Bitmovin - else -> UserInterfaceType.Bitmovin - } +/** + * Converts any `Ad` object into its json representation. + */ +fun Ad.toJson(): WritableMap = Arguments.createMap().apply { + putString("clickThroughUrl", clickThroughUrl) + putMap("data", data?.toJson()) + putInt("height", height) + putString("id", id) + putBoolean("isLinear", isLinear) + putString("mediaFileUrl", mediaFileUrl) + putInt("width", width) +} - /** - * Converts the [json] to a `RNPlayerViewConfig` object. - */ - fun toRNPlayerViewConfigWrapper(json: ReadableMap) = RNPlayerViewConfigWrapper( - playerViewConfig = toPlayerViewConfig(json), - pictureInPictureConfig = toPictureInPictureConfig(json.getMap("pictureInPictureConfig")), - ) - - fun toRNStyleConfigWrapperFromPlayerConfig(json: ReadableMap) = RNStyleConfigWrapper( - styleConfig = toStyleConfig(json), - userInterfaceType = toUserInterfaceTypeFromPlayerConfig(json), - ) - - /** - * Converts any JS object into a [LiveConfig] object. - * @param json JS object representing the [LiveConfig]. - * @return The generated [LiveConfig] if successful, `null` otherwise. - */ - @JvmStatic - fun toLiveConfig(json: ReadableMap?): LiveConfig? { - if (json == null) { - return null - } - val liveConfig = LiveConfig() - if (json.hasKey("minTimeshiftBufferDepth")) { - liveConfig.minTimeShiftBufferDepth = json.getDouble("minTimeshiftBufferDepth") - } - return liveConfig - } +/** + * Converts any `AdData` object into its json representation. + */ +fun AdData.toJson(): WritableMap = Arguments.createMap().apply { + putInt("bitrate", bitrate) + putInt("maxBitrate", maxBitrate) + putString("mimeType", mimeType) + putInt("minBitrate", minBitrate) +} - /** - * Converts any [MediaType] value into its json representation. - * @param mediaType [MediaType] value. - * @return The produced JS string. - */ - @JvmStatic - fun fromMediaType(mediaType: MediaType): String = when (mediaType) { - MediaType.Audio -> "audio" - MediaType.Video -> "video" - } +/** + * Converts any `AdConfig` object into its json representation. + */ +fun AdConfig.toJson(): WritableMap = Arguments.createMap().apply { + putDouble("replaceContentDuration", replaceContentDuration) +} - /** - * Converts any [BufferType] value into its json representation. - * @param bufferType [BufferType] value. - * @return The produced JS string. - */ - @JvmStatic - fun fromBufferType(bufferType: BufferType): String = when (bufferType) { - BufferType.ForwardDuration -> "forwardDuration" - BufferType.BackwardDuration -> "backwardDuration" - } +/** + * Converts any `AdItem` object into its json representation. + */ +fun AdItem.toJson(): WritableMap = Arguments.createMap().apply { + putString("position", position) + putArray("sources", sources.toList().mapToReactArray { it.toJson() }) +} - @JvmStatic - fun fromBufferLevel(bufferLevel: BufferLevel): WritableMap = - Arguments.createMap().apply { - putDouble("level", bufferLevel.level) - putDouble("targetLevel", bufferLevel.targetLevel) - putString( - "media", - fromMediaType(bufferLevel.media), - ) - putString( - "type", - fromBufferType(bufferLevel.type), - ) - } +/** + * Converts any `AdSource` object into its json representation. + */ +fun AdSource.toJson(): WritableMap = Arguments.createMap().apply { + putString("tag", tag) + putString("type", type.toJson()) +} - @JvmStatic - fun fromRNBufferLevels(bufferLevels: RNBufferLevels): WritableMap = - Arguments.createMap().apply { - putMap("audio", fromBufferLevel(bufferLevels.audio)) - putMap("video", fromBufferLevel(bufferLevels.video)) - } +/** + * Converts any `AdSourceType` value into its json representation. + */ +fun AdSourceType.toJson(): String = when (this) { + AdSourceType.Ima -> "ima" + AdSourceType.Unknown -> "unknown" + AdSourceType.Progressive -> "progressive" +} - /** - * Maps a JS string into the corresponding [BufferType] value. - * @param json JS string representing the [BufferType]. - * @return The [BufferType] corresponding to [json], or `null` if the conversion fails. - */ - @JvmStatic - fun toBufferType(json: String?): BufferType? = when (json) { - "forwardDuration" -> BufferType.ForwardDuration - "backwardDuration" -> BufferType.BackwardDuration - else -> null - } +/** + * Converts any `AdQuartile` value into its json representation. + */ +fun AdQuartile.toJson(): String = when (this) { + AdQuartile.FirstQuartile -> "first" + AdQuartile.MidPoint -> "mid_point" + AdQuartile.ThirdQuartile -> "third" +} - /** - * Maps a JS string into the corresponding [MediaType] value. - * @param json JS string representing the [MediaType]. - * @return The [MediaType] corresponding to [json], or `null` if the conversion fails. - */ - @JvmStatic - fun toMediaType(json: String?): MediaType? = when (json) { - "audio" -> MediaType.Audio - "video" -> MediaType.Video - else -> null - } +/** + * Converts an arbitrary json object into a `BitmovinAnalyticsConfig`. + */ +fun ReadableMap.toAnalyticsConfig(): AnalyticsConfig? = getString("licenseKey") + ?.let { AnalyticsConfig.Builder(it) } + ?.apply { + withBoolean("adTrackingDisabled") { setAdTrackingDisabled(it) } + withBoolean("randomizeUserId") { setRandomizeUserId(it) } + }?.build() + +/** + * Converts an arbitrary json object into an analytics `DefaultMetadata`. + */ +fun ReadableMap.toAnalyticsDefaultMetadata(): DefaultMetadata = DefaultMetadata.Builder().apply { + setCustomData(toAnalyticsCustomData()) + withString("cdnProvider") { setCdnProvider(it) } + withString("customUserId") { setCustomUserId(it) } +}.build() + +/** + * Converts an arbitrary json object into an analytics `CustomData`. + */ +fun ReadableMap.toAnalyticsCustomData(): CustomData = CustomData.Builder().apply { + for (n in 1..30) { + this[n] = getString("customData$n") + } + getString("experimentName")?.let { + setExperimentName(it) + } +}.build() + +/** + * Converts an arbitrary analytics `CustomData` object into a JS value. + */ +fun CustomData.toJson(): WritableMap = Arguments.createMap().also { json -> + for (n in 1..30) { + json.putStringIfNotNull("customData$n", this[n]) } + json.putStringIfNotNull("experimentName", experimentName) +} + +fun ReadableMap.toAnalyticsSourceMetadata(): SourceMetadata = SourceMetadata( + title = getString("title"), + videoId = getString("videoId"), + cdnProvider = getString("cdnProvider"), + path = getString("path"), + isLive = getBooleanOrNull("isLive"), + customData = toAnalyticsCustomData(), +) + +fun SourceMetadata.toJson(): ReadableMap = customData.toJson().also { + it.putString("title", title) + it.putString("videoId", videoId) + it.putString("cdnProvider", cdnProvider) + it.putString("path", path) + it.putBoolean("isLive", isLive) +} + +/** + * Converts any `VideoQuality` value into its json representation. + */ +fun VideoQuality.toJson(): WritableMap = Arguments.createMap().apply { + putString("id", id) + putString("label", label) + putInt("bitrate", bitrate) + putString("codec", codec) + putDouble("frameRate", frameRate.toDouble()) + putInt("height", height) + putInt("width", width) +} + +/** + * Converts any `OfflineOptionEntry` into its json representation. + */ +fun OfflineOptionEntry.toJson(): WritableMap = Arguments.createMap().apply { + putString("id", id) + putString("language", language) +} + +/** + * Converts any `OfflineContentOptions` into its json representation. + */ +fun OfflineContentOptions.toJson(): WritableMap = Arguments.createMap().apply { + putArray("audioOptions", audioOptions.mapToReactArray { it.toJson() }) + putArray("textOptions", textOptions.mapToReactArray { it.toJson() }) +} + +fun Thumbnail.toJson(): WritableMap = Arguments.createMap().apply { + putDouble("start", start) + putDouble("end", end) + putString("text", text) + putString("url", uri.toString()) + putInt("x", x) + putInt("y", y) + putInt("width", width) + putInt("height", height) +} + +fun ReadableMap.toPictureInPictureConfig(): PictureInPictureConfig = PictureInPictureConfig( + isEnabled = getBooleanOrNull("isEnabled") ?: false, +) + +/** + * Converts the [json] to a `RNUiConfig` object. + */ +fun toPlayerViewConfig(json: ReadableMap) = PlayerViewConfig( + uiConfig = UiConfig.WebUi( + playbackSpeedSelectionEnabled = json.getMap("uiConfig") + ?.getBooleanOrNull("playbackSpeedSelectionEnabled") + ?: true, + ), +) + +private fun ReadableMap.toUserInterfaceTypeFromPlayerConfig(): UserInterfaceType? = + when (getMap("styleConfig")?.getString("userInterfaceType")) { + "Subtitle" -> UserInterfaceType.Subtitle + "Bitmovin" -> UserInterfaceType.Bitmovin + else -> null + } + +/** + * Converts the [this@toRNPlayerViewConfigWrapper] to a `RNPlayerViewConfig` object. + */ +fun ReadableMap.toRNPlayerViewConfigWrapper() = RNPlayerViewConfigWrapper( + playerViewConfig = toPlayerViewConfig(this), + pictureInPictureConfig = getMap("pictureInPictureConfig")?.toPictureInPictureConfig(), +) + +fun ReadableMap.toRNStyleConfigWrapperFromPlayerConfig(): RNStyleConfigWrapper? { + return RNStyleConfigWrapper( + styleConfig = toStyleConfig(), + userInterfaceType = toUserInterfaceTypeFromPlayerConfig() ?: return null, + ) +} + +/** + * Converts any JS object into a [LiveConfig] object. + */ +fun ReadableMap.toLiveConfig(): LiveConfig = LiveConfig().apply { + withDouble("minTimeshiftBufferDepth") { minTimeShiftBufferDepth = it } +} + +/** + * Converts any [MediaType] value into its json representation. + */ +fun MediaType.toJson(): String = when (this) { + MediaType.Audio -> "audio" + MediaType.Video -> "video" +} + +/** + * Converts any [BufferType] value into its json representation. + */ +fun BufferType.toJson(): String = when (this) { + BufferType.ForwardDuration -> "forwardDuration" + BufferType.BackwardDuration -> "backwardDuration" +} + +fun BufferLevel.toJson(): WritableMap = Arguments.createMap().apply { + putDouble("level", level) + putDouble("targetLevel", targetLevel) + putString("media", media.toJson()) + putString("type", type.toJson()) +} + +fun RNBufferLevels.toJson(): WritableMap = Arguments.createMap().apply { + putMap("audio", audio.toJson()) + putMap("video", video.toJson()) +} + +/** + * Maps a JS string into the corresponding [BufferType] value. + */ +fun String.toBufferType(): BufferType? = when (this) { + "forwardDuration" -> BufferType.ForwardDuration + "backwardDuration" -> BufferType.BackwardDuration + else -> null +} + +/** + * Maps a JS string into the corresponding [MediaType] value. + */ +fun String.toMediaType(): MediaType? = when (this) { + "audio" -> MediaType.Audio + "video" -> MediaType.Video + else -> null } /** * Converts a [CastPayload] object into its JS representation. */ -private fun fromCastPayload(castPayload: CastPayload) = Arguments.createMap().apply { - putDouble("currentTime", castPayload.currentTime) - putString("deviceName", castPayload.deviceName) - putString("type", castPayload.type) +private fun CastPayload.toJson(): WritableMap = Arguments.createMap().apply { + putDouble("currentTime", currentTime) + putString("deviceName", deviceName) + putString("type", type) } + +private fun WritableMap.putStringIfNotNull(name: String, value: String?) = value?.let { putString(name, value) } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/extensions/Any.kt b/android/src/main/java/com/bitmovin/player/reactnative/extensions/Any.kt deleted file mode 100644 index 0ef5d0d5..00000000 --- a/android/src/main/java/com/bitmovin/player/reactnative/extensions/Any.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.bitmovin.player.reactnative.extensions - -/** - * Reflection helper for dynamically getting a property by name from a java object. - * @param propertyName Property name. - * @return A mutable property reference that can be used to get/set the prop's value. - */ -@Suppress("UNCHECKED_CAST") -inline fun Any?.getProperty(propertyName: String): T? = this?.let { - val getter = it::class.java.methods.firstOrNull { method -> - method.name == "get${propertyName.capitalized()}" - } - getter?.invoke(it) as? T -} - -/** - * Reflection helper for dynamically setting a property value by name to a java object. - * @param propertyName Property name. - * @param value Value that will be set for the specified `propertyName`. - */ -@Suppress("UNCHECKED_CAST") -inline fun Any?.setProperty(propertyName: String, value: T) = this?.let { - val setter = it::class.java.methods.firstOrNull { method -> - method.name == "set${propertyName.capitalized()}" - } - setter?.invoke(it, value) -} diff --git a/android/src/main/java/com/bitmovin/player/reactnative/extensions/CustomData.kt b/android/src/main/java/com/bitmovin/player/reactnative/extensions/CustomData.kt new file mode 100644 index 00000000..13b184b4 --- /dev/null +++ b/android/src/main/java/com/bitmovin/player/reactnative/extensions/CustomData.kt @@ -0,0 +1,71 @@ +package com.bitmovin.player.reactnative.extensions + +import com.bitmovin.analytics.api.CustomData + +operator fun CustomData.get(index: Int): String? = when (index) { + 1 -> customData1 + 2 -> customData2 + 3 -> customData3 + 4 -> customData4 + 5 -> customData5 + 6 -> customData6 + 7 -> customData7 + 8 -> customData8 + 9 -> customData9 + 10 -> customData10 + 11 -> customData11 + 12 -> customData12 + 13 -> customData13 + 14 -> customData14 + 15 -> customData15 + 16 -> customData16 + 17 -> customData17 + 18 -> customData18 + 19 -> customData19 + 20 -> customData20 + 21 -> customData21 + 22 -> customData22 + 23 -> customData23 + 24 -> customData24 + 25 -> customData25 + 26 -> customData26 + 27 -> customData27 + 28 -> customData28 + 29 -> customData29 + 30 -> customData30 + else -> throw IndexOutOfBoundsException() +} + +operator fun CustomData.Builder.set(index: Int, value: String?) = when (index) { + 1 -> setCustomData1(value) + 2 -> setCustomData2(value) + 3 -> setCustomData3(value) + 4 -> setCustomData4(value) + 5 -> setCustomData5(value) + 6 -> setCustomData6(value) + 7 -> setCustomData7(value) + 8 -> setCustomData8(value) + 9 -> setCustomData9(value) + 10 -> setCustomData10(value) + 11 -> setCustomData11(value) + 12 -> setCustomData12(value) + 13 -> setCustomData13(value) + 14 -> setCustomData14(value) + 15 -> setCustomData15(value) + 16 -> setCustomData16(value) + 17 -> setCustomData17(value) + 18 -> setCustomData18(value) + 19 -> setCustomData19(value) + 20 -> setCustomData20(value) + 21 -> setCustomData21(value) + 22 -> setCustomData22(value) + 23 -> setCustomData23(value) + 24 -> setCustomData24(value) + 25 -> setCustomData25(value) + 26 -> setCustomData26(value) + 27 -> setCustomData27(value) + 28 -> setCustomData28(value) + 29 -> setCustomData29(value) + 30 -> setCustomData30(value) + else -> throw IndexOutOfBoundsException() +} diff --git a/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReactContextExtension.kt b/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReactContextExtension.kt index 3ca11671..43e00bc9 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReactContextExtension.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReactContextExtension.kt @@ -1,8 +1,20 @@ package com.bitmovin.player.reactnative.extensions -import com.facebook.react.bridge.ReactContext -import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.bitmovin.player.reactnative.DrmModule +import com.bitmovin.player.reactnative.OfflineModule +import com.bitmovin.player.reactnative.PlayerModule +import com.bitmovin.player.reactnative.SourceModule +import com.bitmovin.player.reactnative.ui.CustomMessageHandlerModule +import com.facebook.react.bridge.* +import com.facebook.react.uimanager.UIManagerModule inline fun ReactContext.getModule(): T? { return getNativeModule(T::class.java) } + +val ReactApplicationContext.playerModule get() = getModule() +val ReactApplicationContext.sourceModule get() = getModule() +val ReactApplicationContext.offlineModule get() = getModule() +val ReactApplicationContext.uiManagerModule get() = getModule() +val ReactApplicationContext.drmModule get() = getModule() +val ReactApplicationContext.customMessageHandlerModule get() = getModule() diff --git a/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableArray.kt b/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableArray.kt index f27d2933..8b3ee30e 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableArray.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableArray.kt @@ -2,34 +2,19 @@ package com.bitmovin.player.reactnative.extensions import com.facebook.react.bridge.* -inline fun ReadableArray.toList(): List = (0 until size()).map { i -> - getDynamic(i).let { - when (T::class) { - Boolean::class -> it.asBoolean() as T - String::class -> it.asString() as T - Double::class -> it.asDouble() as T - Int::class -> it.asInt() as T - ReadableArray::class -> it.asArray() as T - ReadableMap::class -> it.asMap() as T - WritableArray::class -> it.asArray() as T - WritableMap::class -> it.asMap() as T - else -> null - } - } +inline fun ReadableArray.toList(convert: (Dynamic) -> T): List = (0 until size()).map { i -> + convert(getDynamic(i)) } -inline fun List.toReadableArray(): ReadableArray = Arguments.createArray().apply { - forEach { - when (T::class) { - Boolean::class -> pushBoolean(it as Boolean) - String::class -> pushString(it as String) - Double::class -> pushDouble(it as Double) - Int::class -> pushInt(it as Int) - ReadableArray::class -> pushArray(it as ReadableArray) - ReadableMap::class -> pushMap(it as ReadableMap) - WritableArray::class -> pushArray(it as ReadableArray) - WritableMap::class -> pushMap(it as ReadableMap) - else -> pushNull() - } - } +fun ReadableArray.toBooleanList() = toList { it.asBoolean() } +fun ReadableArray.toStringList() = toList { it.asString() } +fun ReadableArray.toDoubleList() = toList { it.asDouble() } +fun ReadableArray.toIntList() = toList { it.asInt() } +fun ReadableArray.toListOfArrays() = toList { it.asArray() } +fun ReadableArray.toMapList() = toList { it.asMap() } + +inline fun List.mapToReactArray( + transform: (T) -> WritableMap, +): WritableArray = Arguments.createArray().also { + forEach { element -> it.pushMap(transform(element)) } } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMap.kt b/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMap.kt index e0ac13c7..c248521f 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMap.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMap.kt @@ -2,18 +2,14 @@ package com.bitmovin.player.reactnative.extensions import com.facebook.react.bridge.* -inline fun Map.toReadableMap(): ReadableMap = Arguments.createMap().apply { +/** Convert a [Map] to [ReadableMap], adding each [T] value using [put]. */ +private inline fun Map.toReadableMap( + put: WritableMap.(String, T) -> Unit, +): ReadableMap = Arguments.createMap().apply { forEach { - when (T::class) { - Boolean::class -> putBoolean(it.key, it.value as Boolean) - String::class -> putString(it.key, it.value as String) - Double::class -> putDouble(it.key, it.value as Double) - Int::class -> putInt(it.key, it.value as Int) - ReadableArray::class -> putArray(it.key, it.value as ReadableArray) - ReadableMap::class -> putMap(it.key, it.value as ReadableMap) - WritableArray::class -> putArray(it.key, it.value as ReadableArray) - WritableMap::class -> putMap(it.key, it.value as ReadableMap) - else -> putNull(it.key) - } + put(it.key, it.value) } } + +@JvmName("toReadableStringMap") +fun Map.toReadableMap(): ReadableMap = toReadableMap(WritableMap::putString) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMapExtension.kt b/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMapExtension.kt index 3187f0d9..7668aeca 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMapExtension.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMapExtension.kt @@ -1,25 +1,57 @@ package com.bitmovin.player.reactnative.extensions -import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.* -fun ReadableMap.getBooleanOrNull( +fun ReadableMap.getBooleanOrNull(key: String): Boolean? = getValueOrNull(key, ReadableMap::getBoolean) +fun ReadableMap.getIntOrNull(key: String): Int? = getValueOrNull(key, ReadableMap::getInt) +fun ReadableMap.getDoubleOrNull(key: String): Double? = getValueOrNull(key, ReadableMap::getDouble) + +inline fun ReadableMap.getValueOrNull( + key: String, + get: ReadableMap.(String) -> T?, +) = takeIf { hasKey(key) }?.get(key) + +inline fun ReadableMap.withDouble( + key: String, + block: (Double) -> T, +): T? = mapValue(key, ReadableMap::getDouble, block) + +inline fun ReadableMap.withMap( + key: String, + block: (ReadableMap) -> T, +): T? = mapValue(key, ReadableMap::getMap, block) + +inline fun ReadableMap.withInt( key: String, -): Boolean? = takeIf { hasKey(key) }?.getBoolean(key) + block: (Int) -> T, +): T? = mapValue(key, ReadableMap::getInt, block) -/** - * Reads the [Boolean] value from the given [ReadableMap] if the [key] is present. - * Returns the [default] value otherwise. - */ -fun ReadableMap.getOrDefault( +inline fun ReadableMap.withBoolean( key: String, - default: Boolean, -) = if (hasKey(key)) getBoolean(key) else default + block: (Boolean) -> T, +): T? = mapValue(key, ReadableMap::getBoolean, block) -/** - * Reads the [String] value from the given [ReadableMap] if the [key] is present. - * Returns the [default] value otherwise. - */ -fun ReadableMap.getOrDefault( +inline fun ReadableMap.withString( key: String, - default: String?, -) = if (hasKey(key)) getString(key) else default + block: (String) -> T, +): T? = mapValue(key, ReadableMap::getString, block) + +inline fun ReadableMap.withArray( + key: String, + block: (ReadableArray) -> T, +): T? = mapValue(key, ReadableMap::getArray, block) + +inline fun ReadableMap.withStringArray( + key: String, + block: (List) -> T, +): T? = mapValue(key, ReadableMap::getStringArray, block) + +fun ReadableMap.getStringArray(it: String): List? = getArray(it)?.toStringList() + +inline fun ReadableMap.mapValue( + key: String, + get: ReadableMap.(String) -> T?, + block: (T) -> R, +) = getValueOrNull(key, get)?.let(block) + +inline fun ReadableMap.toMap(): Map = toHashMap().mapValues { it.value as T } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/offline/OfflineContentManagerBridge.kt b/android/src/main/java/com/bitmovin/player/reactnative/offline/OfflineContentManagerBridge.kt index 2d97429c..97ab272c 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/offline/OfflineContentManagerBridge.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/offline/OfflineContentManagerBridge.kt @@ -9,11 +9,11 @@ import com.bitmovin.player.api.offline.options.OfflineOptionEntryAction import com.bitmovin.player.api.offline.options.OfflineOptionEntryState import com.bitmovin.player.api.source.SourceConfig import com.bitmovin.player.reactnative.NativeId -import com.bitmovin.player.reactnative.converter.JsonConverter +import com.bitmovin.player.reactnative.converter.toJson import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableMap -import com.facebook.react.modules.core.DeviceEventManagerModule +import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter class OfflineContentManagerBridge( private val nativeId: NativeId, @@ -155,7 +155,7 @@ class OfflineContentManagerBridge( sendEvent( OfflineEventType.ON_COMPLETED, Arguments.createMap().apply { - putMap("options", JsonConverter.toJson(options)) + putMap("options", options?.toJson()) }, ) } @@ -193,7 +193,7 @@ class OfflineContentManagerBridge( sendEvent( OfflineEventType.ON_OPTIONS_AVAILABLE, Arguments.createMap().apply { - putMap("options", JsonConverter.toJson(options)) + putMap("options", options?.toJson()) }, ) } @@ -219,15 +219,14 @@ class OfflineContentManagerBridge( sendEvent(OfflineEventType.ON_RESUMED) } - private fun sendEvent(eventType: OfflineEventType, event: WritableMap? = null) { - val e = event ?: Arguments.createMap() - e.putString("nativeId", nativeId) - e.putString("identifier", identifier) - e.putString("eventType", eventType.eventName) - e.putString("state", aggregateState(contentOptions).name) - - context - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) - .emit("BitmovinOfflineEvent", e) + private fun sendEvent(eventType: OfflineEventType, event: WritableMap = Arguments.createMap()) { + event.putString("nativeId", nativeId) + event.putString("identifier", identifier) + event.putString("eventType", eventType.eventName) + event.putString("state", aggregateState(contentOptions).name) + context.rtcDeviceEventEmitter.emit("BitmovinOfflineEvent", event) } } + +val ReactApplicationContext.rtcDeviceEventEmitter: RCTDeviceEventEmitter + get() = getJSModule(RCTDeviceEventEmitter::class.java) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b72c748e..12d7437b 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -345,7 +345,7 @@ PODS: - React-jsi (= 0.69.6-2) - React-logger (= 0.69.6-2) - React-perflogger (= 0.69.6-2) - - RNBitmovinPlayer (0.13.0): + - RNBitmovinPlayer (0.14.0): - BitmovinPlayer (= 3.48.0) - GoogleAds-IMA-iOS-SDK (= 3.18.4) - GoogleAds-IMA-tvOS-SDK (= 4.8.2) @@ -574,7 +574,7 @@ SPEC CHECKSUMS: React-RCTText: f72442f7436fd8624494963af4906000a5465ce6 React-runtimeexecutor: f1383f6460ea3d66ed122b4defb0b5ba664ee441 ReactCommon: 7857ab475239c5ba044b7ed946ba564f2e7f1626 - RNBitmovinPlayer: 100318957ac564997fc4f898a7e2f7595fa9e2ca + RNBitmovinPlayer: 7b30750994923ebfe825e82f9182cd0e03e1b708 RNCPicker: 0250e95ad170569a96f5b0555cdd5e65b9084dca RNScreens: 4a1af06327774490d97342c00aee0c2bafb497b7 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 @@ -583,4 +583,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 253d54be44299ecb3c274fb9bf8415c9c99cd6c6 -COCOAPODS: 1.14.2 +COCOAPODS: 1.12.1 diff --git a/package.json b/package.json index 81895afc..dc49fb5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bitmovin-player-react-native", - "version": "0.13.0", + "version": "0.14.0", "description": "Official React Native bindings for Bitmovin's mobile Player SDKs.", "main": "lib/index.js", "module": "lib/index.mjs", diff --git a/src/components/PlayerView/index.tsx b/src/components/PlayerView/index.tsx index 20df3edd..86155c25 100644 --- a/src/components/PlayerView/index.tsx +++ b/src/components/PlayerView/index.tsx @@ -132,7 +132,7 @@ export function PlayerView({ useEffect(() => { const node = findNodeHandle(nativeView.current); - if (node) { + if (node && scalingMode) { dispatch('setScalingMode', node, scalingMode); } }, [scalingMode, nativeView]);