From 373b2360a965542ffd8572c6d14d74a9d69cde6e Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 8 Nov 2023 17:18:17 +0100 Subject: [PATCH 01/48] refactor: improve null safety Refactor json serialization to use null operators rather than operating on null values. No behaviour is changed. PRN-74 --- .../reactnative/BitmovinCastManagerModule.kt | 4 +- .../player/reactnative/BufferModule.kt | 15 +- .../bitmovin/player/reactnative/DrmModule.kt | 6 +- .../player/reactnative/OfflineModule.kt | 4 +- .../reactnative/PlayerAnalyticsModule.kt | 4 +- .../player/reactnative/PlayerModule.kt | 37 +- .../player/reactnative/RNPlayerView.kt | 11 +- .../player/reactnative/RNPlayerViewManager.kt | 4 +- .../player/reactnative/SourceModule.kt | 10 +- .../reactnative/converter/JsonConverter.kt | 1774 ++++++----------- .../player/reactnative/extensions/Any.kt | 27 - .../reactnative/extensions/CustomData.kt | 71 + .../extensions/ReadableMapExtension.kt | 29 + .../offline/OfflineContentManagerBridge.kt | 6 +- 14 files changed, 812 insertions(+), 1190 deletions(-) delete mode 100644 android/src/main/java/com/bitmovin/player/reactnative/extensions/Any.kt create mode 100644 android/src/main/java/com/bitmovin/player/reactnative/extensions/CustomData.kt 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..403b73c1 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt @@ -1,7 +1,7 @@ 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 @@ -31,7 +31,7 @@ class BitmovinCastManagerModule( */ @ReactMethod fun initializeCastManager(options: ReadableMap?, promise: Promise) { - val castOptions = JsonConverter.toCastOptions(options) + val castOptions = options?.toCastOptions() uiManager?.addUIBlock { BitmovinCastManager.initialize( castOptions?.applicationId, 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..8e2c837a 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt @@ -2,7 +2,8 @@ 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 @@ -16,14 +17,14 @@ class BufferModule(private val context: ReactApplicationContext) : ReactContextB /** * 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) + val bufferType = type.toBufferType() if (bufferType == null) { promise.reject("Error: ", "Invalid buffer type") return@addUIBlock @@ -32,23 +33,21 @@ class BufferModule(private val context: ReactApplicationContext) : ReactContextB player.buffer.getLevel(bufferType, MediaType.Audio), player.buffer.getLevel(bufferType, MediaType.Video), ) - JsonConverter.fromRNBufferLevels(bufferLevels).let { - promise.resolve(it) - } + promise.resolve(bufferLevels.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 + val bufferType = type.toBufferType() ?: return@addUIBlock player.buffer.setTargetLevel(bufferType, value) } } 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..ea5367b8 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt @@ -4,7 +4,7 @@ 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 @@ -76,8 +76,8 @@ class DrmModule(private val context: ReactApplicationContext) : ReactContextBase @ReactMethod fun initWithConfig(nativeId: NativeId, config: ReadableMap?) { uiManager()?.addUIBlock { - if (!drmConfigs.containsKey(nativeId) && config != null) { - JsonConverter.toWidevineConfig(config)?.let { + if (!drmConfigs.containsKey(nativeId)) { + config?.toWidevineConfig()?.let { drmConfigs[nativeId] = it initPrepareMessage(nativeId, config) initPrepareLicense(nativeId, config) 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..01e26f30 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt @@ -1,7 +1,7 @@ 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.converter.toSourceConfig import com.bitmovin.player.reactnative.extensions.toList import com.bitmovin.player.reactnative.offline.OfflineContentManagerBridge import com.bitmovin.player.reactnative.offline.OfflineDownloadRequest @@ -61,7 +61,7 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext uiManager()?.addUIBlock { if (!offlineContentManagerBridges.containsKey(nativeId)) { val identifier = config?.getString("identifier") - val sourceConfig = JsonConverter.toSourceConfig(config?.getMap("sourceConfig")) + val sourceConfig = config?.getMap("sourceConfig")?.toSourceConfig() sourceConfig?.drmConfig = drmModule()?.getConfig(drmNativeId) if (identifier.isNullOrEmpty() || sourceConfig == null) { 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..16b40385 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt @@ -1,7 +1,7 @@ package com.bitmovin.player.reactnative 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 @@ -24,7 +24,7 @@ class PlayerAnalyticsModule(private val context: ReactApplicationContext) : Reac @ReactMethod fun sendCustomDataEvent(nativeId: NativeId, json: ReadableMap?) { uiManager()?.addUIBlock { _ -> - JsonConverter.toAnalyticsCustomData(json)?.let { + json?.toAnalyticsCustomData()?.let { playerModule()?.getPlayer(nativeId)?.analytics?.sendCustomDataEvent(it) } } 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 0e72de49..fa02c523 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -3,9 +3,16 @@ 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.fromSource +import com.bitmovin.player.reactnative.converter.fromVideoQuality +import com.bitmovin.player.reactnative.converter.toJson +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.toPlayerConfig import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.UIManagerModule @@ -44,7 +51,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB fun initWithConfig(nativeId: NativeId, config: ReadableMap?) { uiManager()?.addUIBlock { if (!players.containsKey(nativeId)) { - JsonConverter.toPlayerConfig(config).let { + config?.toPlayerConfig()?.let { players[nativeId] = Player.create(context, it) } } @@ -63,11 +70,9 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB 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"), - ) + val playerConfig = playerConfigJson?.toPlayerConfig() ?: PlayerConfig() + val analyticsConfig = analyticsConfigJson?.toAnalyticsConfig() + val defaultMetadata = analyticsConfigJson?.getMap("defaultMetadata")?.toAnalyticsDefaultMetadata() players[nativeId] = if (analyticsConfig == null) { Player.create(context, playerConfig) @@ -239,7 +244,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB @ReactMethod fun source(nativeId: NativeId, promise: Promise) { uiManager()?.addUIBlock { - promise.resolve(JsonConverter.fromSource(players[nativeId]?.source)) + promise.resolve(players[nativeId]?.source?.fromSource()) } } @@ -334,7 +339,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB @ReactMethod fun getAudioTrack(nativeId: NativeId, promise: Promise) { uiManager()?.addUIBlock { - promise.resolve(JsonConverter.fromAudioTrack(players[nativeId]?.source?.selectedAudioTrack)) + promise.resolve(players[nativeId]?.source?.selectedAudioTrack?.toJson()) } } @@ -349,7 +354,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB val audioTracks = Arguments.createArray() players[nativeId]?.source?.availableAudioTracks?.let { tracks -> tracks.forEach { - audioTracks.pushMap(JsonConverter.fromAudioTrack(it)) + audioTracks.pushMap(it.toJson()) } } promise.resolve(audioTracks) @@ -378,7 +383,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB @ReactMethod fun getSubtitleTrack(nativeId: NativeId, promise: Promise) { uiManager()?.addUIBlock { - promise.resolve(JsonConverter.fromSubtitleTrack(players[nativeId]?.source?.selectedSubtitleTrack)) + promise.resolve(players[nativeId]?.source?.selectedSubtitleTrack?.toJson()) } } @@ -393,7 +398,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB val subtitleTracks = Arguments.createArray() players[nativeId]?.source?.availableSubtitleTracks?.let { tracks -> tracks.forEach { - subtitleTracks.pushMap(JsonConverter.fromSubtitleTrack(it)) + subtitleTracks.pushMap(it.toJson()) } } promise.resolve(subtitleTracks) @@ -421,7 +426,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun scheduleAd(nativeId: NativeId, adItemJson: ReadableMap?) { - JsonConverter.toAdItem(adItemJson)?.let { adItem -> + adItemJson?.toAdItem()?.let { adItem -> uiManager()?.addUIBlock { players[nativeId]?.scheduleAd(adItem) } @@ -497,7 +502,7 @@ 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.resolve(players[nativeId]?.source?.getThumbnail(time)?.toJson()) } } @@ -551,7 +556,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB @ReactMethod fun getVideoQuality(nativeId: NativeId, promise: Promise) { uiManager()?.addUIBlock { - promise.resolve(JsonConverter.fromVideoQuality(players[nativeId]?.source?.selectedVideoQuality)) + promise.resolve(players[nativeId]?.source?.selectedVideoQuality?.fromVideoQuality()) } } @@ -566,7 +571,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB val videoQualities = Arguments.createArray() players[nativeId]?.source?.availableVideoQualities?.let { qualities -> qualities.forEach { - videoQualities.pushMap(JsonConverter.fromVideoQuality(it)) + videoQualities.pushMap(it.fromVideoQuality()) } } promise.resolve(videoQualities) 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 513f4aff..30ffacd0 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt @@ -14,7 +14,8 @@ import com.bitmovin.player.api.event.Event 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.reactnative.converter.JsonConverter +import com.bitmovin.player.reactnative.converter.fromPlayerEvent +import com.bitmovin.player.reactnative.converter.fromSourceEvent import com.bitmovin.player.reactnative.ui.RNPictureInPictureDelegate import com.bitmovin.player.reactnative.ui.RNPictureInPictureHandler import com.facebook.react.ReactActivity @@ -283,10 +284,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.fromPlayerEvent() + is SourceEvent -> event.fromSourceEvent() + 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 5d622b00..3b94bd34 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt @@ -7,7 +7,7 @@ import android.view.ViewGroup.LayoutParams import com.bitmovin.player.PlayerView import com.bitmovin.player.api.ui.PlayerViewConfig import com.bitmovin.player.api.ui.ScalingMode -import com.bitmovin.player.reactnative.converter.JsonConverter +import com.bitmovin.player.reactnative.converter.toRNPlayerViewConfigWrapper import com.bitmovin.player.reactnative.extensions.getBooleanOrNull import com.bitmovin.player.reactnative.extensions.getModule import com.bitmovin.player.reactnative.ui.CustomMessageHandlerModule @@ -187,7 +187,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple @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) { 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..e1527159 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt @@ -5,7 +5,9 @@ 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.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule @@ -58,7 +60,7 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB analyticsSourceMetadata: ReadableMap?, ) { uiManager()?.addUIBlock { - val sourceMetadata = JsonConverter.toAnalyticsSourceMetadata(analyticsSourceMetadata) ?: SourceMetadata() + val sourceMetadata = analyticsSourceMetadata?.toAnalyticsSourceMetadata() ?: SourceMetadata() initializeSource(nativeId, drmNativeId, config) { sourceConfig -> Source.create(sourceConfig, sourceMetadata) } @@ -95,7 +97,7 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB ) { val drmConfig = drmNativeId?.let { drmModule()?.getConfig(it) } if (!sources.containsKey(nativeId)) { - val sourceConfig = JsonConverter.toSourceConfig(config)?.apply { + val sourceConfig = config?.toSourceConfig()?.apply { if (drmConfig != null) { this.drmConfig = drmConfig } @@ -196,7 +198,7 @@ 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))) + promise.resolve(sources[nativeId]?.getThumbnail(time)?.toJson()) } } 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 92ac49ea..7f34791c 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 @@ -50,1235 +50,773 @@ import com.bitmovin.player.api.ui.UiConfig import com.bitmovin.player.reactnative.BitmovinCastManagerOptions import com.bitmovin.player.reactnative.RNBufferLevels import com.bitmovin.player.reactnative.RNPlayerViewConfigWrapper +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.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.set import com.bitmovin.player.reactnative.extensions.toList import com.bitmovin.player.reactnative.extensions.toReadableArray import com.bitmovin.player.reactnative.extensions.toReadableMap -import com.bitmovin.player.reactnative.ui.RNPictureInPictureHandler +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.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 - } +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 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 `BufferMediaTypeConfig` object. + */ +fun ReadableMap.toBufferMediaTypeConfig(): BufferMediaTypeConfig = BufferMediaTypeConfig().apply { + withDouble("forwardDuration") { forwardDuration = it } +} - /** - * 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 - } +/** + * 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 [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 an arbitrary [ReadableMap] to a [RemoteControlConfig]. + */ +private fun ReadableMap.toRemoteControlConfig(): RemoteControlConfig = RemoteControlConfig().apply { + withString("receiverStylesheetUrl") { receiverStylesheetUrl = it } + getMap("customReceiverConfig") + ?.toHashMap() + ?.mapValues { entry -> entry.value as String } + ?.let { customReceiverConfig = it } + withBoolean("isCastEnabled") { isCastEnabled = it } + withBoolean("sendManifestRequestsWithCredentials") { sendManifestRequestsWithCredentials = it } + withBoolean("sendSegmentRequestsWithCredentials") { sendSegmentRequestsWithCredentials = it } + withBoolean("sendDrmLicenseRequestsWithCredentials") { sendDrmLicenseRequestsWithCredentials = 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 an arbitrary `json` to `SourceOptions`. + */ +fun ReadableMap.toSourceOptions(): SourceOptions = SourceOptions( + startOffset = getDoubleOrNull("startOffset"), + startOffsetTimelineReference = getString("startOffsetTimelineReference")?.toTimelineReferencePoint(), +) - /** - * 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 `json` to `TimelineReferencePoint`. + */ +private fun String.toTimelineReferencePoint(): TimelineReferencePoint? = when (this) { + "start" -> TimelineReferencePoint.Start + "end" -> TimelineReferencePoint.End + else -> null +} - /** - * 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 `AdaptationConfig`. + */ +private fun ReadableMap.toAdaptationConfig(): AdaptationConfig = AdaptationConfig().apply { + withInt("maxSelectableBitrate") { maxSelectableVideoBitrate = it } +} - /** - * 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 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 `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 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 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 `TweaksConfig` object. + */ +fun ReadableMap.toTweaksConfig(): TweaksConfig = TweaksConfig().apply { + withDouble("timeChangedInterval") { timeChangedInterval = it } + withInt("bandwidthEstimateWeightLimit") { bandwidthEstimateWeightLimit = it } + getMap("devicesThatRequireSurfaceWorkaround")?.let { devices -> + val deviceNames = devices.getArray("deviceNames") + ?.toList() + ?.filterNotNull() + ?.map { DeviceName(it) } + ?: emptyList() + val modelNames = devices.getArray("modelNames") + ?.toList() + ?.filterNotNull() + ?.map { ModelName(it) } + ?: 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 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 an `AdvertisingConfig` object. + */ +fun ReadableMap.toAdvertisingConfig(): AdvertisingConfig? = getArray("schedule") + ?.toList() + ?.mapNotNull { it?.toAdItem() } + ?.let { AdvertisingConfig(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 an `AdItem` object. + */ +fun ReadableMap.toAdItem(): AdItem? = getArray("sources") + ?.toList() + ?.mapNotNull { it?.toAdSource() } + ?.toTypedArray() + ?.let { AdItem(it, getString("position") ?: "pre") } - /** - * 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 `AdSource` object. + */ +fun ReadableMap.toAdSource(): AdSource? { + return AdSource( + type = getString("type")?.toAdSourceType() ?: return null, + tag = getString("tag") ?: 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 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 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) - } +/** + * Converts an arbitrary `json` to `SourceConfig`. + */ +fun ReadableMap.toSourceConfig(): SourceConfig? { + val url = getString("url") + val type = getString("type")?.toSourceType() + if (url == null || type == null) { + return null + } + return SourceConfig(url, type).apply { + title = getString("title") + description = getString("description") + posterSource = getString("poster") + if (hasKey("isPosterPersistent")) { + isPosterPersistent = getBoolean("isPosterPersistent") + } + if (hasKey("subtitleTracks")) { + val subtitleTracks = getArray("subtitleTracks") as ReadableArray + for (i in 0 until subtitleTracks.size()) { + subtitleTracks.getMap(i).toSubtitleTrack()?.let { + 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 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 + if (hasKey("thumbnailTrack")) { + thumbnailTrack = getString("thumbnailTrack")?.toThumbnailTrack() } - - /** - * 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 + if (hasKey("metadata")) { + metadata = getMap("metadata") + ?.toHashMap() + ?.mapValues { entry -> entry.value as String } } + getMap("options")?.let { 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.fromSource(): 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.fromSeekPosition(): WritableMap = Arguments.createMap().apply { + putDouble("time", time) + putMap("source", source.fromSource()) +} - 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.fromSourceEvent(): WritableMap { + val json = Arguments.createMap() + json.putString("name", getName()) + json.putDouble("timestamp", timestamp.toDouble()) + when (this) { + is SourceEvent.Load -> { + json.putMap("source", source.fromSource()) + } - is SourceEvent.Warning -> { - json.putInt("code", event.code.value) - json.putString("message", event.message) - } + is SourceEvent.Loaded -> { + json.putMap("source", source.fromSource()) + } - 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?.fromVideoQuality()) + json.putMap("oldVideoQuality", oldVideoQuality?.fromVideoQuality()) + } - 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.fromPlayerEvent(): 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.fromSeekPosition()) + json.putMap("to", to.fromSeekPosition()) } - /** - * 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?.fromVideoQuality()) + json.putMap("oldVideoQuality", oldVideoQuality?.fromVideoQuality()) } - /** - * 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", fromCastPayload(castPayload)) } - /** - * 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 = getOrDefault("applicationId", null), + messageNamespace = getOrDefault("messageNamespace", null), +) - return fromAnalyticsCustomData(sourceMetadata.customData)?.apply { - putString("title", sourceMetadata.title) - putString("videoId", sourceMetadata.videoId) - putString("cdnProvider", sourceMetadata.cdnProvider) - putString("path", sourceMetadata.path) - putBoolean("isLive", sourceMetadata.isLive) - } - } +/** + * Converts an arbitrary `json` to `WidevineConfig`. + */ +fun ReadableMap.toWidevineConfig(): WidevineConfig? = getMap("widevine")?.run { + WidevineConfig(getString("licenseUrl")).apply { + preferredSecurityLevel = getOrDefault("preferredSecurityLevel", null) + getBooleanOrNull("shouldKeepDrmSessionsAlive")?.let { shouldKeepDrmSessionsAlive = it } + httpHeaders = getMap("httpHeaders") + ?.toHashMap() + ?.mapValues { entry -> entry.value as String } + ?.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? { + val url = this.getString("url") + val label = this.getString("label") + if (url == null || label == null) { + return null + } + return SubtitleTrack( + url = url, + label = label, + id = getString("identifier") ?: UUID.randomUUID().toString(), + isDefault = getBoolean("isDefault"), + language = getString("language"), + isForced = getBoolean("isForced"), + 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.map { it.toJson() }.toReadableArray()) + 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, - ), - ) - - /** - * Converts the [json] to a `RNPlayerViewConfig` object. - */ - fun toRNPlayerViewConfigWrapper(json: ReadableMap) = RNPlayerViewConfigWrapper( - playerViewConfig = toPlayerViewConfig(json), - pictureInPictureConfig = toPictureInPictureConfig(json.getMap("pictureInPictureConfig")), - ) - - /** - * 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 `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 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 `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 [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 `AdConfig` object into its json representation. + */ +fun AdConfig.toJson(): WritableMap = Arguments.createMap().apply { + putDouble("replaceContentDuration", replaceContentDuration) +} - @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 `AdItem` object into its json representation. + */ +fun AdItem.toJson(): WritableMap = Arguments.createMap().apply { + putString("position", position) + putArray("sources", sources.map { it.toJson() }.toReadableArray()) +} - @JvmStatic - fun fromRNBufferLevels(bufferLevels: RNBufferLevels): WritableMap = - Arguments.createMap().apply { - putMap("audio", fromBufferLevel(bufferLevels.audio)) - putMap("video", fromBufferLevel(bufferLevels.video)) - } +/** + * Converts any `AdSource` object into its json representation. + */ +fun AdSource.toJson(): WritableMap = Arguments.createMap().apply { + putString("tag", tag) + putString("type", type.toJson()) +} - /** - * 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 `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 [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 any `AdQuartile` value into its json representation. + */ +fun AdQuartile.toJson(): String = when (this) { + AdQuartile.FirstQuartile -> "first" + AdQuartile.MidPoint -> "mid_point" + AdQuartile.ThirdQuartile -> "third" +} + +/** + * 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 = getBoolean("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.fromVideoQuality(): 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.map { it.toJson() }.toReadableArray()) + putArray("textOptions", textOptions.map { it.toJson() }.toReadableArray()) +} + +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 = getBoolean("isEnabled"), +) + +/** + * Converts the [json] to a `RNUiConfig` object. + */ +fun toPlayerViewConfig(json: ReadableMap) = PlayerViewConfig( + uiConfig = UiConfig.WebUi( + playbackSpeedSelectionEnabled = json.getMap("uiConfig") + ?.getBooleanOrNull("playbackSpeedSelectionEnabled") + ?: true, + ), +) + +/** + * Converts the [this@toRNPlayerViewConfigWrapper] to a `RNPlayerViewConfig` object. + */ +fun ReadableMap.toRNPlayerViewConfigWrapper() = RNPlayerViewConfigWrapper( + playerViewConfig = toPlayerViewConfig(this), + pictureInPictureConfig = getMap("pictureInPictureConfig")?.toPictureInPictureConfig(), +) + +/** + * 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 } /** @@ -1289,3 +827,7 @@ private fun fromCastPayload(castPayload: CastPayload) = Arguments.createMap().ap putString("deviceName", castPayload.deviceName) putString("type", castPayload.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..a11ced56 --- /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() +} \ No newline at end of file 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..95d54485 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 @@ -6,6 +6,35 @@ fun ReadableMap.getBooleanOrNull( key: String, ): Boolean? = takeIf { hasKey(key) }?.getBoolean(key) +fun ReadableMap.getDoubleOrNull( + key: String, +): Double? = takeIf { hasKey(key) }?.getDouble(key) + +inline fun ReadableMap.withDouble( + key: String, + block: (Double) -> T +) : T? = takeIf { hasKey(key) }?.getDouble(key)?.let(block) + +inline fun ReadableMap.withMap( + key: String, + block: (ReadableMap) -> T +) : T? = takeIf { hasKey(key) }?.getMap(key)?.let(block) + +inline fun ReadableMap.withInt( + key: String, + block: (Int) -> T +) : T? = takeIf { hasKey(key) }?.getInt(key)?.let(block) + +inline fun ReadableMap.withBoolean( + key: String, + block: (Boolean) -> T +) : T? = takeIf { hasKey(key) }?.getBoolean(key)?.let(block) + +inline fun ReadableMap.withString( + key: String, + block: (String) -> T +) : T? = getString(key)?.let(block) + /** * Reads the [Boolean] value from the given [ReadableMap] if the [key] is present. * Returns the [default] value otherwise. 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..56b00572 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,7 +9,7 @@ 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 @@ -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()) }, ) } From a2e29de3878ec80e60fe6597e714b055982c4ab7 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Thu, 9 Nov 2023 16:53:13 +0100 Subject: [PATCH 02/48] refactor: report errors instead of ignoring invalid states --- .../player/reactnative/PlayerModule.kt | 154 ++++++++++-------- 1 file changed, 85 insertions(+), 69 deletions(-) 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 fa02c523..405c429b 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -1,6 +1,5 @@ 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 @@ -8,10 +7,10 @@ import com.bitmovin.player.api.analytics.create import com.bitmovin.player.api.event.PlayerEvent import com.bitmovin.player.reactnative.converter.fromSource import com.bitmovin.player.reactnative.converter.fromVideoQuality -import com.bitmovin.player.reactnative.converter.toJson 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.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule @@ -48,8 +47,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param config `PlayerConfig` object received from JS. */ @ReactMethod - fun initWithConfig(nativeId: NativeId, config: ReadableMap?) { - uiManager()?.addUIBlock { + fun initWithConfig(nativeId: NativeId, config: ReadableMap?, promise: Promise) { + addUIBlock(promise) { if (!players.containsKey(nativeId)) { config?.toPlayerConfig()?.let { players[nativeId] = Player.create(context, it) @@ -58,17 +57,34 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB } } + /** Run [block], forwarding the return value. If it throws, sets [Promise.reject] and return null. */ + private inline fun runAndRejectOnException(promise: Promise, crossinline block: ()->T) : T? = try { + block() + } catch (e: Exception) { + promise.reject(e) + null + } + + /** Run [block] in [UIManagerModule.addUIBlock], forwarding the result to the [promise]. */ + private inline fun addUIBlock(promise: Promise, crossinline block: ()->T) { + val uiManager = runAndRejectOnException(promise) { uiManager() } ?: return + uiManager.addUIBlock { + runAndRejectOnException(promise) { + promise.resolve(block()) + } + } + } + /** * Creates a new `Player` instance inside the internal players using the provided `playerConfig` and `analyticsConfig`. * @param playerConfigJson `PlayerConfig` object received from JS. * @param analyticsConfigJson `AnalyticsConfig` object received from JS. */ @ReactMethod - fun initWithAnalyticsConfig(nativeId: NativeId, playerConfigJson: ReadableMap?, analyticsConfigJson: ReadableMap?) { - uiManager()?.addUIBlock { + fun initWithAnalyticsConfig(nativeId: NativeId, playerConfigJson: ReadableMap?, analyticsConfigJson: ReadableMap?, promise: Promise) { + addUIBlock(promise) { if (players.containsKey(nativeId)) { - Log.d("[PlayerModule]", "Duplicate player creation for id $nativeId") - return@addUIBlock + throw IllegalArgumentException("Duplicate player creation for id $nativeId") } val playerConfig = playerConfigJson?.toPlayerConfig() ?: PlayerConfig() val analyticsConfig = analyticsConfigJson?.toAnalyticsConfig() @@ -93,8 +109,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param sourceNativeId Target source. */ @ReactMethod - fun loadSource(nativeId: NativeId, sourceNativeId: String) { - uiManager()?.addUIBlock { + fun loadSource(nativeId: NativeId, sourceNativeId: String, promise: Promise) { + addUIBlock(promise) { sourceModule()?.getSource(sourceNativeId)?.let { players[nativeId]?.load(it) } @@ -108,9 +124,9 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param options Source configuration options from JS. */ @ReactMethod - fun loadOfflineContent(nativeId: NativeId, offlineContentManagerBridgeId: String, options: ReadableMap?) { - uiManager()?.addUIBlock { - val offlineSourceConfig = offlineModule()?.getOfflineContentManagerBridge(offlineContentManagerBridgeId) + fun loadOfflineContent(nativeId: NativeId, offlineContentManagerBridgeId: String, options: ReadableMap?, promise: Promise) { + addUIBlock(promise) { + val offlineSourceConfig = offlineModule().getOfflineContentManagerBridge(offlineContentManagerBridgeId) ?.offlineContentManager?.offlineSourceConfig if (offlineSourceConfig != null) { @@ -124,8 +140,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param nativeId Target player Id. */ @ReactMethod - fun unload(nativeId: NativeId) { - uiManager()?.addUIBlock { + fun unload(nativeId: NativeId, promise: Promise) { + addUIBlock(promise) { players[nativeId]?.unload() } } @@ -135,8 +151,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param nativeId Target player Id. */ @ReactMethod - fun play(nativeId: NativeId) { - uiManager()?.addUIBlock { + fun play(nativeId: NativeId, promise: Promise) { + addUIBlock(promise) { players[nativeId]?.play() } } @@ -146,8 +162,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param nativeId Target player Id. */ @ReactMethod - fun pause(nativeId: NativeId) { - uiManager()?.addUIBlock { + fun pause(nativeId: NativeId, promise: Promise) { + addUIBlock(promise) { players[nativeId]?.pause() } } @@ -158,8 +174,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param time Seek time in seconds. */ @ReactMethod - fun seek(nativeId: NativeId, time: Double) { - uiManager()?.addUIBlock { + fun seek(nativeId: NativeId, time: Double, promise: Promise) { + addUIBlock(promise) { players[nativeId]?.seek(time) } } @@ -170,8 +186,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param offset Offset time in seconds. */ @ReactMethod - fun timeShift(nativeId: NativeId, offset: Double) { - uiManager()?.addUIBlock { + fun timeShift(nativeId: NativeId, offset: Double, promise: Promise) { + addUIBlock(promise) { players[nativeId]?.timeShift(offset) } } @@ -181,8 +197,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param nativeId Target player Id. */ @ReactMethod - fun mute(nativeId: NativeId) { - uiManager()?.addUIBlock { + fun mute(nativeId: NativeId, promise: Promise) { + addUIBlock(promise) { players[nativeId]?.mute() } } @@ -192,8 +208,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param nativeId Target player Id. */ @ReactMethod - fun unmute(nativeId: NativeId) { - uiManager()?.addUIBlock { + fun unmute(nativeId: NativeId, promise: Promise) { + addUIBlock(promise) { players[nativeId]?.unmute() } } @@ -203,8 +219,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param nativeId Target player Id. */ @ReactMethod - fun destroy(nativeId: NativeId) { - uiManager()?.addUIBlock { + fun destroy(nativeId: NativeId, promise: Promise) { + addUIBlock(promise) { players[nativeId]?.let { it.destroy() players.remove(nativeId) @@ -218,8 +234,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param volume Volume level integer between 0 to 100. */ @ReactMethod - fun setVolume(nativeId: NativeId, volume: Int) { - uiManager()?.addUIBlock { + fun setVolume(nativeId: NativeId, volume: Int, promise: Promise) { + addUIBlock(promise) { players[nativeId]?.volume = volume } } @@ -231,7 +247,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getVolume(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.volume) } } @@ -243,7 +259,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun source(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.source?.fromSource()) } } @@ -255,7 +271,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun currentTime(nativeId: NativeId, mode: String?, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { var timeOffset: Double = 0.0 if (mode != null) { timeOffset = if (mode == "relative") { @@ -278,7 +294,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun duration(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.duration) } } @@ -290,7 +306,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isMuted(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.isMuted) } } @@ -302,7 +318,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isPlaying(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.isPlaying) } } @@ -314,7 +330,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isPaused(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.isPaused) } } @@ -326,7 +342,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isLive(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.isLive) } } @@ -338,7 +354,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getAudioTrack(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.source?.selectedAudioTrack?.toJson()) } } @@ -350,7 +366,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getAvailableAudioTracks(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { val audioTracks = Arguments.createArray() players[nativeId]?.source?.availableAudioTracks?.let { tracks -> tracks.forEach { @@ -369,7 +385,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun setAudioTrack(nativeId: NativeId, trackIdentifier: String, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { players[nativeId]?.source?.setAudioTrack(trackIdentifier) promise.resolve(null) } @@ -382,7 +398,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getSubtitleTrack(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.source?.selectedSubtitleTrack?.toJson()) } } @@ -394,7 +410,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getAvailableSubtitles(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { val subtitleTracks = Arguments.createArray() players[nativeId]?.source?.availableSubtitleTracks?.let { tracks -> tracks.forEach { @@ -413,7 +429,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun setSubtitleTrack(nativeId: NativeId, trackIdentifier: String?, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { players[nativeId]?.source?.setSubtitleTrack(trackIdentifier) promise.resolve(null) } @@ -425,9 +441,9 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param adItemJson Json representation of the `AdItem` to be scheduled. */ @ReactMethod - fun scheduleAd(nativeId: NativeId, adItemJson: ReadableMap?) { + fun scheduleAd(nativeId: NativeId, adItemJson: ReadableMap?, promise: Promise) { adItemJson?.toAdItem()?.let { adItem -> - uiManager()?.addUIBlock { + addUIBlock(promise) { players[nativeId]?.scheduleAd(adItem) } } @@ -439,8 +455,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param nativeId Target player id. */ @ReactMethod - fun skipAd(nativeId: NativeId) { - uiManager()?.addUIBlock { + fun skipAd(nativeId: NativeId, promise: Promise) { + addUIBlock(promise) { players[nativeId]?.skipAd() } } @@ -451,7 +467,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isAd(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.isAd) } } @@ -463,7 +479,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getTimeShift(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.timeShift) } } @@ -475,7 +491,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getMaxTimeShift(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.maxTimeShift) } } @@ -486,8 +502,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param maxSelectableBitrate The desired max bitrate limit. */ @ReactMethod - fun setMaxSelectableBitrate(nativeId: NativeId, maxSelectableBitrate: Int) { - uiManager()?.addUIBlock { + fun setMaxSelectableBitrate(nativeId: NativeId, maxSelectableBitrate: Int, promise: Promise) { + addUIBlock(promise) { players[nativeId]?.setMaxSelectableVideoBitrate( maxSelectableBitrate.takeUnless { it == -1 } ?: Integer.MAX_VALUE, ) @@ -501,7 +517,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getThumbnail(nativeId: NativeId, time: Double, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.source?.getThumbnail(time)?.toJson()) } } @@ -511,8 +527,8 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * should be sent. */ @ReactMethod - fun castVideo(nativeId: NativeId) { - uiManager()?.addUIBlock { + fun castVideo(nativeId: NativeId, promise: Promise) { + addUIBlock(promise) { players[nativeId]?.castVideo() } } @@ -521,8 +537,8 @@ 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 { + fun castStop(nativeId: NativeId, promise: Promise) { + addUIBlock(promise) { players[nativeId]?.castStop() } } @@ -533,7 +549,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isCastAvailable(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.isCastAvailable) } } @@ -543,7 +559,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isCasting(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.isCasting) } } @@ -555,7 +571,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getVideoQuality(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { promise.resolve(players[nativeId]?.source?.selectedVideoQuality?.fromVideoQuality()) } } @@ -567,7 +583,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getAvailableVideoQualities(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { + addUIBlock(promise) { val videoQualities = Arguments.createArray() players[nativeId]?.source?.availableVideoQualities?.let { qualities -> qualities.forEach { @@ -581,18 +597,18 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB /** * Helper function that returns the initialized `UIManager` instance. */ - private fun uiManager(): UIManagerModule? = - context.getNativeModule(UIManagerModule::class.java) + private fun uiManager(): UIManagerModule = + context.getNativeModule(UIManagerModule::class.java) ?: throw IllegalStateException("UIManager not found") /** * Helper function that returns the initialized `SourceModule` instance. */ - private fun sourceModule(): SourceModule? = - context.getNativeModule(SourceModule::class.java) + private fun sourceModule(): SourceModule = + context.getNativeModule(SourceModule::class.java) ?: throw IllegalStateException("SourceModule not found") /** * Helper function that returns the initialized `OfflineModule` instance. */ - private fun offlineModule(): OfflineModule? = - context.getNativeModule(OfflineModule::class.java) + private fun offlineModule(): OfflineModule = + context.getNativeModule(OfflineModule::class.java) ?: throw IllegalStateException("OfflineModule not found") } From e0f0244c2d6d412966758e834f66b93f31ca9252 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 10 Nov 2023 09:25:55 +0100 Subject: [PATCH 03/48] refactor: move helpers in base module --- .../player/reactnative/BitmovinBaseModule.kt | 49 ++++++++++++++++ .../player/reactnative/BufferModule.kt | 22 ++----- .../reactnative/PlayerAnalyticsModule.kt | 24 ++------ .../player/reactnative/PlayerModule.kt | 58 +++++-------------- .../player/reactnative/SourceModule.kt | 12 +--- 5 files changed, 79 insertions(+), 86 deletions(-) create mode 100644 android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt 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..8afba57a --- /dev/null +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -0,0 +1,49 @@ +package com.bitmovin.player.reactnative + +import com.facebook.react.bridge.* +import com.facebook.react.uimanager.UIManagerModule + +abstract class BitmovinBaseModule(protected val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { + /** Run [block] in [UIManagerModule.addUIBlock], forwarding the result to the [promise]. */ + protected inline fun addUIBlock(promise: Promise, crossinline block: ()->T) { + val uiManager = runAndRejectOnException(promise) { uiManager() } ?: return + uiManager.addUIBlock { + runAndRejectOnException(promise) { + promise.resolve(block()) + } + } + } + + /** + * Helper function that returns the initialized [UIManagerModule] instance or throw. + */ + protected fun uiManager(): UIManagerModule = + context.getNativeModule(UIManagerModule::class.java) ?: throw IllegalStateException("UIManager not found") + + /** + * Helper function that returns the initialized [SourceModule] instance or throw. + */ + protected fun sourceModule(): SourceModule = + context.getNativeModule(SourceModule::class.java) ?: throw IllegalStateException("SourceModule not found") + + /** + * Helper function that returns the initialized [OfflineModule] instance or throw. + */ + protected fun offlineModule(): OfflineModule = + context.getNativeModule(OfflineModule::class.java) ?: throw IllegalStateException("OfflineModule not found") + + /** + * Helper function that gets the instantiated [PlayerModule] from modules registry. + */ + protected fun playerModule(): PlayerModule = + context.getNativeModule(PlayerModule::class.java) ?: throw IllegalArgumentException("PlayerModule not found") +} + +/** Run [block], forwarding the return value. If it throws, sets [Promise.reject] and return null. */ +inline fun runAndRejectOnException(promise: Promise, crossinline block: ()->T) : T? = try { + block() +} catch (e: Exception) { + promise.reject(e) + null +} + 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 8e2c837a..f4d10511 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt @@ -11,7 +11,7 @@ import com.facebook.react.uimanager.UIManagerModule private const val MODULE_NAME = "BufferModule" @ReactModule(name = MODULE_NAME) -class BufferModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { +class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(context) { override fun getName() = MODULE_NAME /** @@ -22,8 +22,8 @@ class BufferModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getLevel(nativeId: NativeId, type: String, promise: Promise) { - uiManager()?.addUIBlock { _ -> - val player = playerModule()?.getPlayer(nativeId) ?: return@addUIBlock + addUIBlock(promise) { + val player = playerModule().getPlayer(nativeId) ?: return@addUIBlock val bufferType = type.toBufferType() if (bufferType == null) { promise.reject("Error: ", "Invalid buffer type") @@ -44,23 +44,13 @@ class BufferModule(private val context: ReactApplicationContext) : ReactContextB * @param value The value to set. */ @ReactMethod - fun setTargetLevel(nativeId: NativeId, type: String, value: Double) { - uiManager()?.addUIBlock { _ -> - val player = playerModule()?.getPlayer(nativeId) ?: return@addUIBlock + fun setTargetLevel(nativeId: NativeId, type: String, value: Double, promise: Promise) { + addUIBlock(promise) { + val player = playerModule().getPlayer(nativeId) ?: return@addUIBlock val bufferType = type.toBufferType() ?: return@addUIBlock player.buffer.setTargetLevel(bufferType, 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) } /** 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 16b40385..b2bf530a 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt @@ -9,7 +9,7 @@ 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. */ @@ -22,10 +22,10 @@ class PlayerAnalyticsModule(private val context: ReactApplicationContext) : Reac * @param json Custom data config json. */ @ReactMethod - fun sendCustomDataEvent(nativeId: NativeId, json: ReadableMap?) { - uiManager()?.addUIBlock { _ -> + fun sendCustomDataEvent(nativeId: NativeId, json: ReadableMap?, promise: Promise) { + addUIBlock(promise) { json?.toAnalyticsCustomData()?.let { - playerModule()?.getPlayer(nativeId)?.analytics?.sendCustomDataEvent(it) + playerModule().getPlayer(nativeId)?.analytics?.sendCustomDataEvent(it) } } } @@ -37,22 +37,10 @@ class PlayerAnalyticsModule(private val context: ReactApplicationContext) : Reac */ @ReactMethod fun getUserId(playerId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { _ -> - playerModule()?.getPlayer(playerId)?.analytics?.let { + addUIBlock(promise) { + playerModule().getPlayer(playerId)?.analytics?.let { promise.resolve(it.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) } 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 405c429b..786310fb 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -19,7 +19,7 @@ import com.facebook.react.uimanager.UIManagerModule 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. */ @@ -57,31 +57,18 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB } } - /** Run [block], forwarding the return value. If it throws, sets [Promise.reject] and return null. */ - private inline fun runAndRejectOnException(promise: Promise, crossinline block: ()->T) : T? = try { - block() - } catch (e: Exception) { - promise.reject(e) - null - } - - /** Run [block] in [UIManagerModule.addUIBlock], forwarding the result to the [promise]. */ - private inline fun addUIBlock(promise: Promise, crossinline block: ()->T) { - val uiManager = runAndRejectOnException(promise) { uiManager() } ?: return - uiManager.addUIBlock { - runAndRejectOnException(promise) { - promise.resolve(block()) - } - } - } - /** * Creates a new `Player` instance inside the internal players using the provided `playerConfig` and `analyticsConfig`. * @param playerConfigJson `PlayerConfig` object received from JS. * @param analyticsConfigJson `AnalyticsConfig` object received from JS. */ @ReactMethod - fun initWithAnalyticsConfig(nativeId: NativeId, playerConfigJson: ReadableMap?, analyticsConfigJson: ReadableMap?, promise: Promise) { + fun initWithAnalyticsConfig( + nativeId: NativeId, + playerConfigJson: ReadableMap?, + analyticsConfigJson: ReadableMap?, + promise: Promise + ) { addUIBlock(promise) { if (players.containsKey(nativeId)) { throw IllegalArgumentException("Duplicate player creation for id $nativeId") @@ -111,9 +98,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB @ReactMethod fun loadSource(nativeId: NativeId, sourceNativeId: String, promise: Promise) { addUIBlock(promise) { - sourceModule()?.getSource(sourceNativeId)?.let { - players[nativeId]?.load(it) - } + players[nativeId]?.load(sourceModule().getSource(sourceNativeId)) } } @@ -124,7 +109,12 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param options Source configuration options from JS. */ @ReactMethod - fun loadOfflineContent(nativeId: NativeId, offlineContentManagerBridgeId: String, options: ReadableMap?, promise: Promise) { + fun loadOfflineContent( + nativeId: NativeId, + offlineContentManagerBridgeId: String, + options: ReadableMap?, + promise: Promise + ) { addUIBlock(promise) { val offlineSourceConfig = offlineModule().getOfflineContentManagerBridge(offlineContentManagerBridgeId) ?.offlineContentManager?.offlineSourceConfig @@ -593,22 +583,4 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB promise.resolve(videoQualities) } } - - /** - * Helper function that returns the initialized `UIManager` instance. - */ - private fun uiManager(): UIManagerModule = - context.getNativeModule(UIManagerModule::class.java) ?: throw IllegalStateException("UIManager not found") - - /** - * Helper function that returns the initialized `SourceModule` instance. - */ - private fun sourceModule(): SourceModule = - context.getNativeModule(SourceModule::class.java) ?: throw IllegalStateException("SourceModule not found") - - /** - * Helper function that returns the initialized `OfflineModule` instance. - */ - private fun offlineModule(): OfflineModule = - context.getNativeModule(OfflineModule::class.java) ?: throw IllegalStateException("OfflineModule not found") -} +} \ No newline at end of file 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 e1527159..686b3a81 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt @@ -31,16 +31,10 @@ 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 throws. */ - fun getSource(nativeId: NativeId?): Source? { - if (nativeId == null) { - return null - } - return sources[nativeId] - } + fun getSource(nativeId: NativeId): Source = sources[nativeId] + ?: throw IllegalArgumentException("No source matching provided id") /** * Creates a new `Source` instance inside the internal sources using the provided From 5fc530a023c21a2e5919b670239b5567a095829b Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 10 Nov 2023 09:45:50 +0100 Subject: [PATCH 04/48] refactor: reformat --- .../player/reactnative/BitmovinBaseModule.kt | 9 ++++---- .../player/reactnative/BufferModule.kt | 1 - .../reactnative/PlayerAnalyticsModule.kt | 1 - .../player/reactnative/PlayerModule.kt | 7 +++--- .../reactnative/converter/JsonConverter.kt | 22 +++++++++---------- .../reactnative/extensions/CustomData.kt | 4 ++-- .../extensions/ReadableMapExtension.kt | 20 ++++++++--------- 7 files changed, 31 insertions(+), 33 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index 8afba57a..b9dcc56f 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -3,9 +3,11 @@ package com.bitmovin.player.reactnative import com.facebook.react.bridge.* import com.facebook.react.uimanager.UIManagerModule -abstract class BitmovinBaseModule(protected val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { +abstract class BitmovinBaseModule( + protected val context: ReactApplicationContext, +) : ReactContextBaseJavaModule(context) { /** Run [block] in [UIManagerModule.addUIBlock], forwarding the result to the [promise]. */ - protected inline fun addUIBlock(promise: Promise, crossinline block: ()->T) { + protected inline fun addUIBlock(promise: Promise, crossinline block: () -> T) { val uiManager = runAndRejectOnException(promise) { uiManager() } ?: return uiManager.addUIBlock { runAndRejectOnException(promise) { @@ -40,10 +42,9 @@ abstract class BitmovinBaseModule(protected val context: ReactApplicationContext } /** Run [block], forwarding the return value. If it throws, sets [Promise.reject] and return null. */ -inline fun runAndRejectOnException(promise: Promise, crossinline block: ()->T) : T? = try { +inline fun runAndRejectOnException(promise: Promise, crossinline block: () -> T): T? = try { block() } catch (e: Exception) { promise.reject(e) null } - 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 f4d10511..d1ebb6b6 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt @@ -6,7 +6,6 @@ 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" 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 b2bf530a..9fcad3cc 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt @@ -4,7 +4,6 @@ import com.bitmovin.player.api.analytics.AnalyticsApi.Companion.analytics 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" 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 786310fb..11dafdb0 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -14,7 +14,6 @@ import com.bitmovin.player.reactnative.converter.toJson import com.bitmovin.player.reactnative.converter.toPlayerConfig import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.uimanager.UIManagerModule private const val MODULE_NAME = "PlayerModule" @@ -67,7 +66,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex nativeId: NativeId, playerConfigJson: ReadableMap?, analyticsConfigJson: ReadableMap?, - promise: Promise + promise: Promise, ) { addUIBlock(promise) { if (players.containsKey(nativeId)) { @@ -113,7 +112,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex nativeId: NativeId, offlineContentManagerBridgeId: String, options: ReadableMap?, - promise: Promise + promise: Promise, ) { addUIBlock(promise) { val offlineSourceConfig = offlineModule().getOfflineContentManagerBridge(offlineContentManagerBridgeId) @@ -583,4 +582,4 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex promise.resolve(videoQualities) } } -} \ No newline at end of file +} 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 7f34791c..c7f3475b 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 @@ -552,7 +552,7 @@ fun ReadableMap.toSubtitleTrack(): SubtitleTrack? { /** * Converts any subtitle format name in its mime type representation. */ -private fun String.toSubtitleMimeType(): String = "text/${this}" +private fun String.toSubtitleMimeType(): String = "text/$this" /** * Converts any `SubtitleTrack` into its json representation. @@ -669,7 +669,7 @@ fun ReadableMap.toAnalyticsDefaultMetadata(): DefaultMetadata = DefaultMetadata. */ fun ReadableMap.toAnalyticsCustomData(): CustomData = CustomData.Builder().apply { for (n in 1..30) { - this[n] = getString("customData${n}") + this[n] = getString("customData$n") } getString("experimentName")?.let { setExperimentName(it) @@ -681,7 +681,7 @@ fun ReadableMap.toAnalyticsCustomData(): CustomData = CustomData.Builder().apply */ fun CustomData.toJson(): WritableMap = Arguments.createMap().also { json -> for (n in 1..30) { - json.putStringIfNotNull("customData${n}", this[n]) + json.putStringIfNotNull("customData$n", this[n]) } json.putStringIfNotNull("experimentName", experimentName) } @@ -790,16 +790,16 @@ fun BufferType.toJson(): String = when (this) { } fun BufferLevel.toJson(): WritableMap = Arguments.createMap().apply { - putDouble("level", level) - putDouble("targetLevel", targetLevel) - putString("media", media.toJson()) - putString("type", type.toJson()) - } + 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()) - } + putMap("audio", audio.toJson()) + putMap("video", video.toJson()) +} /** * Maps a JS string into the corresponding [BufferType] 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 index a11ced56..13b184b4 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/extensions/CustomData.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/extensions/CustomData.kt @@ -2,7 +2,7 @@ package com.bitmovin.player.reactnative.extensions import com.bitmovin.analytics.api.CustomData -operator fun CustomData.get(index: Int) : String? = when (index) { +operator fun CustomData.get(index: Int): String? = when (index) { 1 -> customData1 2 -> customData2 3 -> customData3 @@ -68,4 +68,4 @@ operator fun CustomData.Builder.set(index: Int, value: String?) = when (index) { 29 -> setCustomData29(value) 30 -> setCustomData30(value) else -> throw IndexOutOfBoundsException() -} \ No newline at end of file +} 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 95d54485..699dc860 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 @@ -12,28 +12,28 @@ fun ReadableMap.getDoubleOrNull( inline fun ReadableMap.withDouble( key: String, - block: (Double) -> T -) : T? = takeIf { hasKey(key) }?.getDouble(key)?.let(block) + block: (Double) -> T, +): T? = takeIf { hasKey(key) }?.getDouble(key)?.let(block) inline fun ReadableMap.withMap( key: String, - block: (ReadableMap) -> T -) : T? = takeIf { hasKey(key) }?.getMap(key)?.let(block) + block: (ReadableMap) -> T, +): T? = takeIf { hasKey(key) }?.getMap(key)?.let(block) inline fun ReadableMap.withInt( key: String, - block: (Int) -> T -) : T? = takeIf { hasKey(key) }?.getInt(key)?.let(block) + block: (Int) -> T, +): T? = takeIf { hasKey(key) }?.getInt(key)?.let(block) inline fun ReadableMap.withBoolean( key: String, - block: (Boolean) -> T -) : T? = takeIf { hasKey(key) }?.getBoolean(key)?.let(block) + block: (Boolean) -> T, +): T? = takeIf { hasKey(key) }?.getBoolean(key)?.let(block) inline fun ReadableMap.withString( key: String, - block: (String) -> T -) : T? = getString(key)?.let(block) + block: (String) -> T, +): T? = getString(key)?.let(block) /** * Reads the [Boolean] value from the given [ReadableMap] if the [key] is present. From 09db8f3825e6febcc12ccf4ebf3e6885034d8336 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 10 Nov 2023 12:21:11 +0100 Subject: [PATCH 05/48] fixup! method not called toJson --- .../player/reactnative/RNPlayerView.kt | 7 ++-- .../reactnative/converter/JsonConverter.kt | 38 +++++++++---------- 2 files changed, 22 insertions(+), 23 deletions(-) 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 30ffacd0..0610424f 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt @@ -14,8 +14,7 @@ import com.bitmovin.player.api.event.Event 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.reactnative.converter.fromPlayerEvent -import com.bitmovin.player.reactnative.converter.fromSourceEvent +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 @@ -285,8 +284,8 @@ class RNPlayerView( */ private inline fun emitEvent(name: String, event: E) { val payload = when (event) { - is PlayerEvent -> event.fromPlayerEvent() - is SourceEvent -> event.fromSourceEvent() + is PlayerEvent -> event.toJson() + is SourceEvent -> event.toJson() else -> throw IllegalArgumentException() } val reactContext = context as ReactContext 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 c7f3475b..3670ad19 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 @@ -274,7 +274,7 @@ fun String.toSourceType(): SourceType? = when (this) { /** * Converts any given `Source` object into its `json` representation. */ -fun Source.fromSource(): WritableMap = Arguments.createMap().apply { +fun Source.toJson(): WritableMap = Arguments.createMap().apply { putDouble("duration", duration) putBoolean("isActive", isActive) putBoolean("isAttachedToPlayer", isAttachedToPlayer) @@ -285,25 +285,25 @@ fun Source.fromSource(): WritableMap = Arguments.createMap().apply { /** * Converts any given `SeekPosition` object into its `json` representation. */ -fun SeekPosition.fromSeekPosition(): WritableMap = Arguments.createMap().apply { +fun SeekPosition.toJson(): WritableMap = Arguments.createMap().apply { putDouble("time", time) - putMap("source", source.fromSource()) + putMap("source", source.toJson()) } /** * Converts any given `SourceEvent` object into its `json` representation. */ -fun SourceEvent.fromSourceEvent(): WritableMap { +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.fromSource()) + json.putMap("source", source.toJson()) } is SourceEvent.Loaded -> { - json.putMap("source", source.fromSource()) + json.putMap("source", source.toJson()) } is SourceEvent.Error -> { @@ -355,8 +355,8 @@ fun SourceEvent.fromSourceEvent(): WritableMap { } is SourceEvent.VideoDownloadQualityChanged -> { - json.putMap("newVideoQuality", newVideoQuality?.fromVideoQuality()) - json.putMap("oldVideoQuality", oldVideoQuality?.fromVideoQuality()) + json.putMap("newVideoQuality", newVideoQuality?.toJson()) + json.putMap("oldVideoQuality", oldVideoQuality?.toJson()) } else -> { @@ -369,7 +369,7 @@ fun SourceEvent.fromSourceEvent(): WritableMap { /** * Converts any given `PlayerEvent` object into its `json` representation. */ -fun PlayerEvent.fromPlayerEvent(): WritableMap { +fun PlayerEvent.toJson(): WritableMap { val json = Arguments.createMap() json.putString("name", getName()) json.putDouble("timestamp", timestamp.toDouble()) @@ -401,8 +401,8 @@ fun PlayerEvent.fromPlayerEvent(): WritableMap { } is PlayerEvent.Seek -> { - json.putMap("from", from.fromSeekPosition()) - json.putMap("to", to.fromSeekPosition()) + json.putMap("from", from.toJson()) + json.putMap("to", to.toJson()) } is PlayerEvent.TimeShift -> { @@ -472,12 +472,12 @@ fun PlayerEvent.fromPlayerEvent(): WritableMap { } is PlayerEvent.VideoPlaybackQualityChanged -> { - json.putMap("newVideoQuality", newVideoQuality?.fromVideoQuality()) - json.putMap("oldVideoQuality", oldVideoQuality?.fromVideoQuality()) + json.putMap("newVideoQuality", newVideoQuality?.toJson()) + json.putMap("oldVideoQuality", oldVideoQuality?.toJson()) } is PlayerEvent.CastWaitingForDevice -> { - json.putMap("castPayload", fromCastPayload(castPayload)) + json.putMap("castPayload", castPayload.toJson()) } is PlayerEvent.CastStarted -> { @@ -706,7 +706,7 @@ fun SourceMetadata.toJson(): ReadableMap = customData.toJson().also { /** * Converts any `VideoQuality` value into its json representation. */ -fun VideoQuality.fromVideoQuality(): WritableMap = Arguments.createMap().apply { +fun VideoQuality.toJson(): WritableMap = Arguments.createMap().apply { putString("id", id) putString("label", label) putInt("bitrate", bitrate) @@ -822,10 +822,10 @@ fun String.toMediaType(): MediaType? = when (this) { /** * 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?) { From 2cb51f81323d791aa502b8b5d3a61ad9203605b4 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 10 Nov 2023 13:17:55 +0100 Subject: [PATCH 06/48] refactor: use getPlayer instead of direct map access --- .../player/reactnative/BufferModule.kt | 4 +- .../reactnative/PlayerAnalyticsModule.kt | 10 +- .../player/reactnative/PlayerModule.kt | 150 ++++++++---------- .../player/reactnative/RNPlayerViewManager.kt | 6 +- 4 files changed, 72 insertions(+), 98 deletions(-) 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 d1ebb6b6..c3066ba8 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt @@ -22,7 +22,7 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getLevel(nativeId: NativeId, type: String, promise: Promise) { addUIBlock(promise) { - val player = playerModule().getPlayer(nativeId) ?: return@addUIBlock + val player = playerModule().getPlayer(nativeId) val bufferType = type.toBufferType() if (bufferType == null) { promise.reject("Error: ", "Invalid buffer type") @@ -45,7 +45,7 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun setTargetLevel(nativeId: NativeId, type: String, value: Double, promise: Promise) { addUIBlock(promise) { - val player = playerModule().getPlayer(nativeId) ?: return@addUIBlock + val player = playerModule().getPlayer(nativeId) val bufferType = type.toBufferType() ?: return@addUIBlock player.buffer.setTargetLevel(bufferType, value) } 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 9fcad3cc..f8a75718 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt @@ -21,11 +21,9 @@ class PlayerAnalyticsModule(context: ReactApplicationContext) : BitmovinBaseModu * @param json Custom data config json. */ @ReactMethod - fun sendCustomDataEvent(nativeId: NativeId, json: ReadableMap?, promise: Promise) { + fun sendCustomDataEvent(nativeId: NativeId, json: ReadableMap, promise: Promise) { addUIBlock(promise) { - json?.toAnalyticsCustomData()?.let { - playerModule().getPlayer(nativeId)?.analytics?.sendCustomDataEvent(it) - } + playerModule().getPlayer(nativeId).analytics?.sendCustomDataEvent(json.toAnalyticsCustomData()) } } @@ -37,9 +35,7 @@ class PlayerAnalyticsModule(context: ReactApplicationContext) : BitmovinBaseModu @ReactMethod fun getUserId(playerId: NativeId, promise: Promise) { addUIBlock(promise) { - playerModule().getPlayer(playerId)?.analytics?.let { - promise.resolve(it.userId) - } + playerModule().getPlayer(playerId).analytics?.userId } } } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt index 11dafdb0..59f6e908 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -5,8 +5,6 @@ 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.fromSource -import com.bitmovin.player.reactnative.converter.fromVideoQuality import com.bitmovin.player.reactnative.converter.toAdItem import com.bitmovin.player.reactnative.converter.toAnalyticsConfig import com.bitmovin.player.reactnative.converter.toAnalyticsDefaultMetadata @@ -14,6 +12,7 @@ import com.bitmovin.player.reactnative.converter.toJson import com.bitmovin.player.reactnative.converter.toPlayerConfig import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule +import java.security.InvalidParameterException private const val MODULE_NAME = "PlayerModule" @@ -29,17 +28,18 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ override fun getName() = MODULE_NAME + /** + * Fetches the `Player` instance associated with `nativeId` from the internal players, or throw if there are none. + */ + fun getPlayer(nativeId: NativeId): Player = getPlayerOrNull(nativeId) + ?: throw IllegalArgumentException("Invalid PlayerId") + /** * Fetches the `Player` instance associated with `nativeId` from the internal players. * @param nativeId `Player` instance ID. * @return The associated `Player` instance or `null`. */ - 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. @@ -97,7 +97,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun loadSource(nativeId: NativeId, sourceNativeId: String, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.load(sourceModule().getSource(sourceNativeId)) + getPlayer(nativeId).load(sourceModule().getSource(sourceNativeId)) } } @@ -115,12 +115,11 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex promise: Promise, ) { addUIBlock(promise) { - val offlineSourceConfig = offlineModule().getOfflineContentManagerBridge(offlineContentManagerBridgeId) - ?.offlineContentManager?.offlineSourceConfig - - if (offlineSourceConfig != null) { - players[nativeId]?.load(offlineSourceConfig) - } + offlineModule() + .getOfflineContentManagerBridge(offlineContentManagerBridgeId) + ?.offlineContentManager + ?.offlineSourceConfig + ?.let { getPlayer(nativeId).load(it) } } } @@ -131,7 +130,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun unload(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.unload() + getPlayer(nativeId).unload() } } @@ -142,7 +141,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun play(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.play() + getPlayer(nativeId).play() } } @@ -153,7 +152,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun pause(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.pause() + getPlayer(nativeId).pause() } } @@ -165,7 +164,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun seek(nativeId: NativeId, time: Double, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.seek(time) + getPlayer(nativeId).seek(time) } } @@ -177,7 +176,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun timeShift(nativeId: NativeId, offset: Double, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.timeShift(offset) + getPlayer(nativeId).timeShift(offset) } } @@ -188,7 +187,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun mute(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.mute() + getPlayer(nativeId).mute() } } @@ -199,7 +198,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun unmute(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.unmute() + getPlayer(nativeId).unmute() } } @@ -210,10 +209,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun destroy(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.let { - it.destroy() - players.remove(nativeId) - } + getPlayer(nativeId).destroy() + players.remove(nativeId) } } @@ -225,7 +222,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun setVolume(nativeId: NativeId, volume: Int, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.volume = volume + getPlayer(nativeId).volume = volume } } @@ -237,7 +234,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getVolume(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.volume) + getPlayer(nativeId).volume } } @@ -249,7 +246,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun source(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.source?.fromSource()) + getPlayer(nativeId).source?.toJson() } } @@ -261,18 +258,13 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun currentTime(nativeId: NativeId, mode: String?, promise: Promise) { addUIBlock(promise) { - 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) + val player = getPlayer(nativeId) + val timeOffset: Double = when (mode) { + "relative" -> player.playbackTimeOffsetToRelativeTime + "absolute" -> player.playbackTimeOffsetToAbsoluteTime + else -> throw InvalidParameterException("Unknown mode $mode") } + player.currentTime + timeOffset } } @@ -284,7 +276,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun duration(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.duration) + getPlayer(nativeId).duration } } @@ -296,7 +288,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun isMuted(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.isMuted) + getPlayer(nativeId).isMuted } } @@ -308,7 +300,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun isPlaying(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.isPlaying) + getPlayer(nativeId).isPlaying } } @@ -320,7 +312,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun isPaused(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.isPaused) + getPlayer(nativeId).isPaused } } @@ -332,7 +324,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun isLive(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.isLive) + getPlayer(nativeId).isLive } } @@ -344,7 +336,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getAudioTrack(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.source?.selectedAudioTrack?.toJson()) + getPlayer(nativeId).source?.selectedAudioTrack?.toJson() } } @@ -356,13 +348,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getAvailableAudioTracks(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - val audioTracks = Arguments.createArray() - players[nativeId]?.source?.availableAudioTracks?.let { tracks -> - tracks.forEach { - audioTracks.pushMap(it.toJson()) - } - } - promise.resolve(audioTracks) + getPlayer(nativeId).source?.availableAudioTracks?.mapToReactArray { it.toJson() } } } @@ -375,8 +361,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun setAudioTrack(nativeId: NativeId, trackIdentifier: String, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.source?.setAudioTrack(trackIdentifier) - promise.resolve(null) + getPlayer(nativeId).source?.setAudioTrack(trackIdentifier) } } @@ -388,7 +373,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getSubtitleTrack(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.source?.selectedSubtitleTrack?.toJson()) + getPlayer(nativeId).source?.selectedSubtitleTrack?.toJson() } } @@ -400,13 +385,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getAvailableSubtitles(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - val subtitleTracks = Arguments.createArray() - players[nativeId]?.source?.availableSubtitleTracks?.let { tracks -> - tracks.forEach { - subtitleTracks.pushMap(it.toJson()) - } - } - promise.resolve(subtitleTracks) + getPlayer(nativeId).source?.availableSubtitleTracks?.mapToReactArray { it.toJson() } } } @@ -419,8 +398,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun setSubtitleTrack(nativeId: NativeId, trackIdentifier: String?, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.source?.setSubtitleTrack(trackIdentifier) - promise.resolve(null) + getPlayer(nativeId).source?.setSubtitleTrack(trackIdentifier) } } @@ -430,11 +408,9 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex * @param adItemJson Json representation of the `AdItem` to be scheduled. */ @ReactMethod - fun scheduleAd(nativeId: NativeId, adItemJson: ReadableMap?, promise: Promise) { - adItemJson?.toAdItem()?.let { adItem -> - addUIBlock(promise) { - players[nativeId]?.scheduleAd(adItem) - } + fun scheduleAd(nativeId: NativeId, adItemJson: ReadableMap, promise: Promise) { + addUIBlock(promise) { + getPlayer(nativeId).scheduleAd(adItemJson.toAdItem() ?: throw IllegalArgumentException("invalid adItem")) } } @@ -446,7 +422,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun skipAd(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.skipAd() + getPlayer(nativeId).skipAd() } } @@ -457,7 +433,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun isAd(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.isAd) + getPlayer(nativeId).isAd } } @@ -469,7 +445,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getTimeShift(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.timeShift) + getPlayer(nativeId).timeShift } } @@ -481,7 +457,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getMaxTimeShift(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.maxTimeShift) + getPlayer(nativeId).maxTimeShift } } @@ -493,7 +469,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun setMaxSelectableBitrate(nativeId: NativeId, maxSelectableBitrate: Int, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.setMaxSelectableVideoBitrate( + getPlayer(nativeId).setMaxSelectableVideoBitrate( maxSelectableBitrate.takeUnless { it == -1 } ?: Integer.MAX_VALUE, ) } @@ -507,7 +483,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getThumbnail(nativeId: NativeId, time: Double, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.source?.getThumbnail(time)?.toJson()) + getPlayer(nativeId).source?.getThumbnail(time)?.toJson() } } @@ -518,7 +494,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun castVideo(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.castVideo() + getPlayer(nativeId).castVideo() } } @@ -528,7 +504,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun castStop(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - players[nativeId]?.castStop() + getPlayer(nativeId).castStop() } } @@ -539,7 +515,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun isCastAvailable(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.isCastAvailable) + getPlayer(nativeId).isCastAvailable } } @@ -549,7 +525,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun isCasting(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.isCasting) + getPlayer(nativeId).isCasting } } @@ -561,7 +537,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getVideoQuality(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - promise.resolve(players[nativeId]?.source?.selectedVideoQuality?.fromVideoQuality()) + getPlayer(nativeId).source?.selectedVideoQuality?.toJson() } } @@ -573,13 +549,11 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getAvailableVideoQualities(nativeId: NativeId, promise: Promise) { addUIBlock(promise) { - val videoQualities = Arguments.createArray() - players[nativeId]?.source?.availableVideoQualities?.let { qualities -> - qualities.forEach { - videoQualities.pushMap(it.fromVideoQuality()) - } - } - promise.resolve(videoQualities) + getPlayer(nativeId).source?.availableVideoQualities?.mapToReactArray { it.toJson() } } } } + +private inline fun List.mapToReactArray( + transform: (T) -> WritableMap, +): WritableArray = Arguments.fromList(map(transform)) 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 3b94bd34..e02beb5c 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt @@ -248,7 +248,11 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple */ private fun attachPlayer(view: RNPlayerView, playerId: NativeId?, playerConfig: ReadableMap?) { Handler(Looper.getMainLooper()).post { - val player = getPlayerModule()?.getPlayer(playerId) + val player = playerId?.let { getPlayerModule()?.getPlayerOrNull(it) } + if (player == null) { + Log.e(MODULE_NAME, "Cannot create a PlayerView, invalid playerId was passed.") + return@post + } val playbackConfig = playerConfig?.getMap("playbackConfig") val isPictureInPictureEnabled = view.config?.pictureInPictureConfig?.isEnabled == true || playbackConfig?.getBooleanOrNull("isPictureInPictureEnabled") == true From 46a753552ca653a697b9ec8432b21138e65a4cb4 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 13 Nov 2023 14:37:50 +0100 Subject: [PATCH 07/48] refactor: introduce module accessors --- .../player/reactnative/BitmovinBaseModule.kt | 22 +++++++++++-------- .../reactnative/BitmovinCastManagerModule.kt | 4 ++-- .../bitmovin/player/reactnative/DrmModule.kt | 10 ++------- .../player/reactnative/OfflineModule.kt | 17 ++++---------- .../extensions/ReactContextExtension.kt | 14 ++++++++++-- 5 files changed, 33 insertions(+), 34 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index b9dcc56f..ce424db8 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -1,5 +1,9 @@ package com.bitmovin.player.reactnative +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 @@ -16,29 +20,29 @@ abstract class BitmovinBaseModule( } } + /** + * Helper function that gets the instantiated [PlayerModule] from modules registry. + */ + protected fun playerModule(): PlayerModule = + context.playerModule ?: throw IllegalArgumentException("PlayerModule not found") + /** * Helper function that returns the initialized [UIManagerModule] instance or throw. */ protected fun uiManager(): UIManagerModule = - context.getNativeModule(UIManagerModule::class.java) ?: throw IllegalStateException("UIManager not found") + context.uiManagerModule ?: throw IllegalStateException("UIManager not found") /** * Helper function that returns the initialized [SourceModule] instance or throw. */ protected fun sourceModule(): SourceModule = - context.getNativeModule(SourceModule::class.java) ?: throw IllegalStateException("SourceModule not found") + context.sourceModule ?: throw IllegalStateException("SourceModule not found") /** * Helper function that returns the initialized [OfflineModule] instance or throw. */ protected fun offlineModule(): OfflineModule = - context.getNativeModule(OfflineModule::class.java) ?: throw IllegalStateException("OfflineModule not found") - - /** - * Helper function that gets the instantiated [PlayerModule] from modules registry. - */ - protected fun playerModule(): PlayerModule = - context.getNativeModule(PlayerModule::class.java) ?: throw IllegalArgumentException("PlayerModule not found") + context.offlineModule ?: throw IllegalStateException("OfflineModule not found") } /** Run [block], forwarding the return value. If it throws, sets [Promise.reject] and return null. */ 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 403b73c1..c81568dd 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt @@ -2,6 +2,7 @@ package com.bitmovin.player.reactnative import com.bitmovin.player.casting.BitmovinCastManager import com.bitmovin.player.reactnative.converter.toCastOptions +import com.bitmovin.player.reactnative.extensions.uiManagerModule import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule @@ -63,8 +64,7 @@ class BitmovinCastManagerModule( } } - private val uiManager: UIManagerModule? - get() = context.getNativeModule(UIManagerModule::class.java) + private val uiManager: UIManagerModule? get() = context.uiManagerModule } /** 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 ea5367b8..5451bc3d 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt @@ -5,9 +5,9 @@ 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.toWidevineConfig +import com.bitmovin.player.reactnative.extensions.uiManagerModule import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.uimanager.UIManagerModule import java.util.concurrent.locks.Condition import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -75,7 +75,7 @@ class DrmModule(private val context: ReactApplicationContext) : ReactContextBase */ @ReactMethod fun initWithConfig(nativeId: NativeId, config: ReadableMap?) { - uiManager()?.addUIBlock { + context.uiManagerModule?.addUIBlock { if (!drmConfigs.containsKey(nativeId)) { config?.toWidevineConfig()?.let { drmConfigs[nativeId] = it @@ -181,10 +181,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 01e26f30..79bc4be8 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt @@ -2,12 +2,13 @@ package com.bitmovin.player.reactnative import com.bitmovin.player.api.offline.options.OfflineOptionEntryState import com.bitmovin.player.reactnative.converter.toSourceConfig +import com.bitmovin.player.reactnative.extensions.drmModule import com.bitmovin.player.reactnative.extensions.toList +import com.bitmovin.player.reactnative.extensions.uiManagerModule 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 private const val OFFLINE_MODULE = "BitmovinOfflineModule" @@ -58,11 +59,11 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun initWithConfig(nativeId: NativeId, config: ReadableMap?, drmNativeId: NativeId?, promise: Promise) { - uiManager()?.addUIBlock { + context.uiManagerModule?.addUIBlock { if (!offlineContentManagerBridges.containsKey(nativeId)) { val identifier = config?.getString("identifier") val sourceConfig = config?.getMap("sourceConfig")?.toSourceConfig() - sourceConfig?.drmConfig = drmModule()?.getConfig(drmNativeId) + sourceConfig?.drmConfig = context.drmModule?.getConfig(drmNativeId) if (identifier.isNullOrEmpty() || sourceConfig == null) { promise.reject(IllegalArgumentException("Identifier and SourceConfig may not be null")) @@ -277,14 +278,4 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext getOfflineContentManagerBridge(nativeId)?.let(runBlock) ?: promise.reject(IllegalArgumentException("Could not find the offline module instance")) } - - /** - * 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/extensions/ReactContextExtension.kt b/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReactContextExtension.kt index 3ca11671..76e23203 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,18 @@ 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.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() From 14fc20823ca233e4fbfa1f7e165700be5e2ef314 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 13 Nov 2023 15:20:55 +0100 Subject: [PATCH 08/48] refactor: rename addUIBlock --- .../player/reactnative/BitmovinBaseModule.kt | 10 +-- .../player/reactnative/BufferModule.kt | 15 ++-- .../reactnative/PlayerAnalyticsModule.kt | 4 +- .../player/reactnative/PlayerModule.kt | 82 +++++++++---------- 4 files changed, 54 insertions(+), 57 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index ce424db8..a0f998a4 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -10,12 +10,12 @@ import com.facebook.react.uimanager.UIManagerModule abstract class BitmovinBaseModule( protected val context: ReactApplicationContext, ) : ReactContextBaseJavaModule(context) { - /** Run [block] in [UIManagerModule.addUIBlock], forwarding the result to the [promise]. */ - protected inline fun addUIBlock(promise: Promise, crossinline block: () -> T) { - val uiManager = runAndRejectOnException(promise) { uiManager() } ?: return + /** [resolve] the [Promise] by running [block] in the UI thread with [UIManagerModule.addUIBlock]. */ + protected inline fun Promise.resolveOnUIThread(crossinline block: () -> T) { + val uiManager = runAndRejectOnException(this) { uiManager() } ?: return uiManager.addUIBlock { - runAndRejectOnException(promise) { - promise.resolve(block()) + runAndRejectOnException(this) { + resolve(block()) } } } 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 c3066ba8..fa5b1afe 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt @@ -21,13 +21,9 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getLevel(nativeId: NativeId, type: String, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { val player = playerModule().getPlayer(nativeId) - val bufferType = type.toBufferType() - if (bufferType == null) { - promise.reject("Error: ", "Invalid buffer type") - return@addUIBlock - } + val bufferType = type.toBufferTypeOrThrow() val bufferLevels = RNBufferLevels( player.buffer.getLevel(bufferType, MediaType.Audio), player.buffer.getLevel(bufferType, MediaType.Video), @@ -36,6 +32,9 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex } } + private fun String.toBufferTypeOrThrow() = toBufferType() + ?: throw IllegalArgumentException("Invalid buffer type") + /** * Sets the target buffer level for the chosen buffer type across all media types. * @param nativeId Target player id. @@ -44,9 +43,9 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun setTargetLevel(nativeId: NativeId, type: String, value: Double, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { val player = playerModule().getPlayer(nativeId) - val bufferType = type.toBufferType() ?: return@addUIBlock + val bufferType = type.toBufferTypeOrThrow() player.buffer.setTargetLevel(bufferType, value) } } 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 f8a75718..8cc5ddb8 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt @@ -22,7 +22,7 @@ class PlayerAnalyticsModule(context: ReactApplicationContext) : BitmovinBaseModu */ @ReactMethod fun sendCustomDataEvent(nativeId: NativeId, json: ReadableMap, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { playerModule().getPlayer(nativeId).analytics?.sendCustomDataEvent(json.toAnalyticsCustomData()) } } @@ -34,7 +34,7 @@ class PlayerAnalyticsModule(context: ReactApplicationContext) : BitmovinBaseModu */ @ReactMethod fun getUserId(playerId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { playerModule().getPlayer(playerId).analytics?.userId } } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt index 59f6e908..e9f7b654 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -36,8 +36,6 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex /** * Fetches the `Player` instance associated with `nativeId` from the internal players. - * @param nativeId `Player` instance ID. - * @return The associated `Player` instance or `null`. */ fun getPlayerOrNull(nativeId: NativeId): Player? = players[nativeId] @@ -47,7 +45,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun initWithConfig(nativeId: NativeId, config: ReadableMap?, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { if (!players.containsKey(nativeId)) { config?.toPlayerConfig()?.let { players[nativeId] = Player.create(context, it) @@ -68,7 +66,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex analyticsConfigJson: ReadableMap?, promise: Promise, ) { - addUIBlock(promise) { + promise.resolveOnUIThread { if (players.containsKey(nativeId)) { throw IllegalArgumentException("Duplicate player creation for id $nativeId") } @@ -96,7 +94,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun loadSource(nativeId: NativeId, sourceNativeId: String, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).load(sourceModule().getSource(sourceNativeId)) } } @@ -114,7 +112,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex options: ReadableMap?, promise: Promise, ) { - addUIBlock(promise) { + promise.resolveOnUIThread { offlineModule() .getOfflineContentManagerBridge(offlineContentManagerBridgeId) ?.offlineContentManager @@ -129,7 +127,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun unload(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).unload() } } @@ -140,7 +138,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun play(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).play() } } @@ -151,7 +149,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun pause(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).pause() } } @@ -163,7 +161,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun seek(nativeId: NativeId, time: Double, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).seek(time) } } @@ -175,7 +173,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun timeShift(nativeId: NativeId, offset: Double, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).timeShift(offset) } } @@ -186,7 +184,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun mute(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).mute() } } @@ -197,7 +195,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun unmute(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).unmute() } } @@ -208,7 +206,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun destroy(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).destroy() players.remove(nativeId) } @@ -221,7 +219,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun setVolume(nativeId: NativeId, volume: Int, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).volume = volume } } @@ -233,7 +231,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getVolume(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).volume } } @@ -245,7 +243,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun source(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).source?.toJson() } } @@ -257,7 +255,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun currentTime(nativeId: NativeId, mode: String?, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { val player = getPlayer(nativeId) val timeOffset: Double = when (mode) { "relative" -> player.playbackTimeOffsetToRelativeTime @@ -275,7 +273,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun duration(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).duration } } @@ -287,7 +285,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isMuted(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).isMuted } } @@ -299,7 +297,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isPlaying(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).isPlaying } } @@ -311,7 +309,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isPaused(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).isPaused } } @@ -323,7 +321,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isLive(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).isLive } } @@ -335,7 +333,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getAudioTrack(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).source?.selectedAudioTrack?.toJson() } } @@ -347,7 +345,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getAvailableAudioTracks(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).source?.availableAudioTracks?.mapToReactArray { it.toJson() } } } @@ -360,7 +358,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun setAudioTrack(nativeId: NativeId, trackIdentifier: String, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).source?.setAudioTrack(trackIdentifier) } } @@ -372,7 +370,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getSubtitleTrack(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).source?.selectedSubtitleTrack?.toJson() } } @@ -384,7 +382,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getAvailableSubtitles(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).source?.availableSubtitleTracks?.mapToReactArray { it.toJson() } } } @@ -397,7 +395,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun setSubtitleTrack(nativeId: NativeId, trackIdentifier: String?, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).source?.setSubtitleTrack(trackIdentifier) } } @@ -409,7 +407,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun scheduleAd(nativeId: NativeId, adItemJson: ReadableMap, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).scheduleAd(adItemJson.toAdItem() ?: throw IllegalArgumentException("invalid adItem")) } } @@ -421,7 +419,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun skipAd(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).skipAd() } } @@ -432,7 +430,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isAd(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).isAd } } @@ -444,7 +442,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getTimeShift(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).timeShift } } @@ -456,7 +454,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getMaxTimeShift(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).maxTimeShift } } @@ -468,7 +466,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun setMaxSelectableBitrate(nativeId: NativeId, maxSelectableBitrate: Int, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).setMaxSelectableVideoBitrate( maxSelectableBitrate.takeUnless { it == -1 } ?: Integer.MAX_VALUE, ) @@ -482,7 +480,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getThumbnail(nativeId: NativeId, time: Double, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).source?.getThumbnail(time)?.toJson() } } @@ -493,7 +491,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun castVideo(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).castVideo() } } @@ -503,7 +501,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun castStop(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).castStop() } } @@ -514,7 +512,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isCastAvailable(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).isCastAvailable } } @@ -524,7 +522,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isCasting(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).isCasting } } @@ -536,7 +534,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getVideoQuality(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).source?.selectedVideoQuality?.toJson() } } @@ -548,7 +546,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getAvailableVideoQualities(nativeId: NativeId, promise: Promise) { - addUIBlock(promise) { + promise.resolveOnUIThread { getPlayer(nativeId).source?.availableVideoQualities?.mapToReactArray { it.toJson() } } } From b39491e050674d0a134c35a1e9690c90872eebc2 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 13 Nov 2023 15:35:01 +0100 Subject: [PATCH 09/48] refactor: introduce throw safe block --- .../player/reactnative/BitmovinBaseModule.kt | 49 ++++++++----------- .../player/reactnative/BufferModule.kt | 4 +- .../reactnative/PlayerAnalyticsModule.kt | 4 +- .../player/reactnative/PlayerModule.kt | 4 +- 4 files changed, 26 insertions(+), 35 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index a0f998a4..9cc530ff 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -11,44 +11,35 @@ abstract class BitmovinBaseModule( protected val context: ReactApplicationContext, ) : ReactContextBaseJavaModule(context) { /** [resolve] the [Promise] by running [block] in the UI thread with [UIManagerModule.addUIBlock]. */ - protected inline fun Promise.resolveOnUIThread(crossinline block: () -> T) { - val uiManager = runAndRejectOnException(this) { uiManager() } ?: return + protected inline fun Promise.resolveOnUIThread(crossinline block: PromiseRejectOnExceptionBlock.() -> T) { + val uiManager = runAndRejectOnException { uiManager } ?: return uiManager.addUIBlock { - runAndRejectOnException(this) { + runAndRejectOnException { resolve(block()) } } } - /** - * Helper function that gets the instantiated [PlayerModule] from modules registry. - */ - protected fun playerModule(): PlayerModule = - context.playerModule ?: throw IllegalArgumentException("PlayerModule not found") - - /** - * Helper function that returns the initialized [UIManagerModule] instance or throw. - */ - protected fun uiManager(): UIManagerModule = - context.uiManagerModule ?: throw IllegalStateException("UIManager not found") - - /** - * Helper function that returns the initialized [SourceModule] instance or throw. - */ - protected fun sourceModule(): SourceModule = - context.sourceModule ?: throw IllegalStateException("SourceModule not found") - - /** - * Helper function that returns the initialized [OfflineModule] instance or throw. - */ - protected fun offlineModule(): OfflineModule = - context.offlineModule ?: throw IllegalStateException("OfflineModule not found") + protected val PromiseRejectOnExceptionBlock.playerModule: PlayerModule get() = context.playerModule + ?: throw IllegalArgumentException("PlayerModule not found") + + protected val PromiseRejectOnExceptionBlock.uiManager: UIManagerModule get() = context.uiManagerModule + ?: throw IllegalStateException("UIManager not found") + + protected val PromiseRejectOnExceptionBlock.sourceModule: SourceModule get() = context.sourceModule + ?: throw IllegalStateException("SourceModule not found") + + protected val PromiseRejectOnExceptionBlock.offlineModule: OfflineModule get() = context.offlineModule + ?: throw IllegalStateException("OfflineModule not found") } /** Run [block], forwarding the return value. If it throws, sets [Promise.reject] and return null. */ -inline fun runAndRejectOnException(promise: Promise, crossinline block: () -> T): T? = try { - block() +inline fun Promise.runAndRejectOnException(crossinline block: PromiseRejectOnExceptionBlock.() -> T): T? = try { + PromiseRejectOnExceptionBlock.block() } catch (e: Exception) { - promise.reject(e) + reject(e) null } + +/** Receiver of code that can safely throw when resolving a [Promise]. */ +object PromiseRejectOnExceptionBlock 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 fa5b1afe..64e25497 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt @@ -22,7 +22,7 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getLevel(nativeId: NativeId, type: String, promise: Promise) { promise.resolveOnUIThread { - val player = playerModule().getPlayer(nativeId) + val player = playerModule.getPlayer(nativeId) val bufferType = type.toBufferTypeOrThrow() val bufferLevels = RNBufferLevels( player.buffer.getLevel(bufferType, MediaType.Audio), @@ -44,7 +44,7 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun setTargetLevel(nativeId: NativeId, type: String, value: Double, promise: Promise) { promise.resolveOnUIThread { - val player = playerModule().getPlayer(nativeId) + val player = playerModule.getPlayer(nativeId) val bufferType = type.toBufferTypeOrThrow() player.buffer.setTargetLevel(bufferType, value) } 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 8cc5ddb8..d48eaa5d 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt @@ -23,7 +23,7 @@ class PlayerAnalyticsModule(context: ReactApplicationContext) : BitmovinBaseModu @ReactMethod fun sendCustomDataEvent(nativeId: NativeId, json: ReadableMap, promise: Promise) { promise.resolveOnUIThread { - playerModule().getPlayer(nativeId).analytics?.sendCustomDataEvent(json.toAnalyticsCustomData()) + playerModule.getPlayer(nativeId).analytics?.sendCustomDataEvent(json.toAnalyticsCustomData()) } } @@ -35,7 +35,7 @@ class PlayerAnalyticsModule(context: ReactApplicationContext) : BitmovinBaseModu @ReactMethod fun getUserId(playerId: NativeId, promise: Promise) { promise.resolveOnUIThread { - playerModule().getPlayer(playerId).analytics?.userId + playerModule.getPlayer(playerId).analytics?.userId } } } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt index e9f7b654..bf3ff987 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -95,7 +95,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun loadSource(nativeId: NativeId, sourceNativeId: String, promise: Promise) { promise.resolveOnUIThread { - getPlayer(nativeId).load(sourceModule().getSource(sourceNativeId)) + getPlayer(nativeId).load(sourceModule.getSource(sourceNativeId)) } } @@ -113,7 +113,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex promise: Promise, ) { promise.resolveOnUIThread { - offlineModule() + offlineModule .getOfflineContentManagerBridge(offlineContentManagerBridgeId) ?.offlineContentManager ?.offlineSourceConfig From f2ff634e7045f1399b537de620e811bb58d94dc2 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 13 Nov 2023 15:51:33 +0100 Subject: [PATCH 10/48] fix: remove extra promise.resolve --- .../player/reactnative/BufferModule.kt | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) 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 64e25497..ddde0983 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt @@ -8,6 +8,7 @@ import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule private const val MODULE_NAME = "BufferModule" +private const val INVALID_BUFFER_TYPE = "Invalid buffer type" @ReactModule(name = MODULE_NAME) class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(context) { @@ -23,18 +24,14 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex fun getLevel(nativeId: NativeId, type: String, promise: Promise) { promise.resolveOnUIThread { val player = playerModule.getPlayer(nativeId) - val bufferType = type.toBufferTypeOrThrow() - val bufferLevels = RNBufferLevels( - player.buffer.getLevel(bufferType, MediaType.Audio), - player.buffer.getLevel(bufferType, MediaType.Video), - ) - promise.resolve(bufferLevels.toJson()) + val bufferType = type.toBufferType() ?: throw IllegalArgumentException(INVALID_BUFFER_TYPE) + RNBufferLevels( + audio = player.buffer.getLevel(bufferType, MediaType.Audio), + video = player.buffer.getLevel(bufferType, MediaType.Video), + ).toJson() } } - private fun String.toBufferTypeOrThrow() = toBufferType() - ?: throw IllegalArgumentException("Invalid buffer type") - /** * Sets the target buffer level for the chosen buffer type across all media types. * @param nativeId Target player id. @@ -44,11 +41,12 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun setTargetLevel(nativeId: NativeId, type: String, value: Double, promise: Promise) { promise.resolveOnUIThread { - val player = playerModule.getPlayer(nativeId) - val bufferType = type.toBufferTypeOrThrow() - player.buffer.setTargetLevel(bufferType, value) + playerModule.getPlayer(nativeId).buffer.setTargetLevel(type.toBufferTypeOrThrow(), value) } } + + private fun String.toBufferTypeOrThrow() = toBufferType() + ?: throw IllegalArgumentException("Invalid buffer type") } /** From f8e3611ad0bcf17a76d7ec2bda193f1d40e52aeb Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 13 Nov 2023 16:08:17 +0100 Subject: [PATCH 11/48] refactor: compile time safe getPlayer --- .../com/bitmovin/player/reactnative/BitmovinBaseModule.kt | 4 ++++ .../java/com/bitmovin/player/reactnative/BufferModule.kt | 6 +++--- .../bitmovin/player/reactnative/PlayerAnalyticsModule.kt | 4 ++-- .../java/com/bitmovin/player/reactnative/PlayerModule.kt | 6 ------ 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index 9cc530ff..58073344 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -1,5 +1,6 @@ package com.bitmovin.player.reactnative +import com.bitmovin.player.api.Player import com.bitmovin.player.reactnative.extensions.offlineModule import com.bitmovin.player.reactnative.extensions.playerModule import com.bitmovin.player.reactnative.extensions.sourceModule @@ -31,6 +32,9 @@ abstract class BitmovinBaseModule( protected val PromiseRejectOnExceptionBlock.offlineModule: OfflineModule get() = context.offlineModule ?: throw IllegalStateException("OfflineModule not found") + + fun PromiseRejectOnExceptionBlock.getPlayer(nativeId: NativeId): Player = playerModule.getPlayerOrNull(nativeId) + ?: throw IllegalArgumentException("Invalid PlayerId") } /** Run [block], forwarding the return value. If it throws, sets [Promise.reject] and return null. */ 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 ddde0983..614d2dd3 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt @@ -23,8 +23,8 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getLevel(nativeId: NativeId, type: String, promise: Promise) { promise.resolveOnUIThread { - val player = playerModule.getPlayer(nativeId) - val bufferType = type.toBufferType() ?: throw IllegalArgumentException(INVALID_BUFFER_TYPE) + val player = getPlayer(nativeId) + val bufferType = type.toBufferTypeOrThrow() RNBufferLevels( audio = player.buffer.getLevel(bufferType, MediaType.Audio), video = player.buffer.getLevel(bufferType, MediaType.Video), @@ -41,7 +41,7 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun setTargetLevel(nativeId: NativeId, type: String, value: Double, promise: Promise) { promise.resolveOnUIThread { - playerModule.getPlayer(nativeId).buffer.setTargetLevel(type.toBufferTypeOrThrow(), value) + getPlayer(nativeId).buffer.setTargetLevel(type.toBufferTypeOrThrow(), value) } } 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 d48eaa5d..b1035f30 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt @@ -23,7 +23,7 @@ class PlayerAnalyticsModule(context: ReactApplicationContext) : BitmovinBaseModu @ReactMethod fun sendCustomDataEvent(nativeId: NativeId, json: ReadableMap, promise: Promise) { promise.resolveOnUIThread { - playerModule.getPlayer(nativeId).analytics?.sendCustomDataEvent(json.toAnalyticsCustomData()) + getPlayer(nativeId).analytics?.sendCustomDataEvent(json.toAnalyticsCustomData()) } } @@ -35,7 +35,7 @@ class PlayerAnalyticsModule(context: ReactApplicationContext) : BitmovinBaseModu @ReactMethod fun getUserId(playerId: NativeId, promise: Promise) { promise.resolveOnUIThread { - playerModule.getPlayer(playerId).analytics?.userId + getPlayer(playerId).analytics?.userId } } } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt index bf3ff987..619c01de 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -28,12 +28,6 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ override fun getName() = MODULE_NAME - /** - * Fetches the `Player` instance associated with `nativeId` from the internal players, or throw if there are none. - */ - fun getPlayer(nativeId: NativeId): Player = getPlayerOrNull(nativeId) - ?: throw IllegalArgumentException("Invalid PlayerId") - /** * Fetches the `Player` instance associated with `nativeId` from the internal players. */ From 899d4f0749701ad6aba1fdd93810d1f53be2eb97 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 13 Nov 2023 17:14:38 +0100 Subject: [PATCH 12/48] refactor: add withArray --- .../reactnative/converter/JsonConverter.kt | 65 +++++++------------ .../extensions/ReadableMapExtension.kt | 6 ++ 2 files changed, 29 insertions(+), 42 deletions(-) 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 3670ad19..577b2201 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 @@ -62,6 +61,7 @@ import com.bitmovin.player.reactnative.extensions.set import com.bitmovin.player.reactnative.extensions.toList import com.bitmovin.player.reactnative.extensions.toReadableArray import com.bitmovin.player.reactnative.extensions.toReadableMap +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 @@ -106,10 +106,7 @@ fun ReadableMap.toBufferConfig(): BufferConfig = BufferConfig().apply { */ private fun ReadableMap.toRemoteControlConfig(): RemoteControlConfig = RemoteControlConfig().apply { withString("receiverStylesheetUrl") { receiverStylesheetUrl = it } - getMap("customReceiverConfig") - ?.toHashMap() - ?.mapValues { entry -> entry.value as String } - ?.let { customReceiverConfig = it } + withMap("customReceiverConfig") { customReceiverConfig = it.castValues() } withBoolean("isCastEnabled") { isCastEnabled = it } withBoolean("sendManifestRequestsWithCredentials") { sendManifestRequestsWithCredentials = it } withBoolean("sendSegmentRequestsWithCredentials") { sendSegmentRequestsWithCredentials = it } @@ -166,17 +163,13 @@ fun ReadableMap.toStyleConfig(): StyleConfig = StyleConfig().apply { fun ReadableMap.toTweaksConfig(): TweaksConfig = TweaksConfig().apply { withDouble("timeChangedInterval") { timeChangedInterval = it } withInt("bandwidthEstimateWeightLimit") { bandwidthEstimateWeightLimit = it } - getMap("devicesThatRequireSurfaceWorkaround")?.let { devices -> - val deviceNames = devices.getArray("deviceNames") - ?.toList() - ?.filterNotNull() - ?.map { DeviceName(it) } - ?: emptyList() - val modelNames = devices.getArray("modelNames") - ?.toList() - ?.filterNotNull() - ?.map { ModelName(it) } - ?: emptyList() + withMap("devicesThatRequireSurfaceWorkaround") { devices -> + val deviceNames = devices.withArray("deviceNames") { + it.toList().filterNotNull().map(::DeviceName) + } ?: emptyList() + val modelNames = devices.withArray("modelNames") { + it.toList().filterNotNull().map(::DeviceName) + } ?: emptyList() devicesThatRequireSurfaceWorkaround = deviceNames + modelNames } withBoolean("languagePropertyNormalization") { languagePropertyNormalization = it } @@ -234,29 +227,20 @@ fun ReadableMap.toSourceConfig(): SourceConfig? { return null } return SourceConfig(url, type).apply { - title = getString("title") - description = getString("description") - posterSource = getString("poster") - if (hasKey("isPosterPersistent")) { - isPosterPersistent = getBoolean("isPosterPersistent") - } - if (hasKey("subtitleTracks")) { - val subtitleTracks = getArray("subtitleTracks") as ReadableArray + 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) } } } - if (hasKey("thumbnailTrack")) { - thumbnailTrack = getString("thumbnailTrack")?.toThumbnailTrack() - } - if (hasKey("metadata")) { - metadata = getMap("metadata") - ?.toHashMap() - ?.mapValues { entry -> entry.value as String } - } - getMap("options")?.let { options = it.toSourceOptions() } + withString("thumbnailTrack") { thumbnailTrack = it.toThumbnailTrack() } + withMap("metadata") { metadata = it.castValues() } + withMap("options") { options = it.toSourceOptions() } } } @@ -504,12 +488,9 @@ fun ReadableMap.toCastOptions(): BitmovinCastManagerOptions = BitmovinCastManage */ fun ReadableMap.toWidevineConfig(): WidevineConfig? = getMap("widevine")?.run { WidevineConfig(getString("licenseUrl")).apply { - preferredSecurityLevel = getOrDefault("preferredSecurityLevel", null) - getBooleanOrNull("shouldKeepDrmSessionsAlive")?.let { shouldKeepDrmSessionsAlive = it } - httpHeaders = getMap("httpHeaders") - ?.toHashMap() - ?.mapValues { entry -> entry.value as String } - ?.toMutableMap() + withString("preferredSecurityLevel") { preferredSecurityLevel = it } + withBoolean("shouldKeepDrmSessionsAlive") { shouldKeepDrmSessionsAlive = it } + withMap("httpHeaders") { httpHeaders = it.castValues().toMutableMap() } } } @@ -828,6 +809,6 @@ private fun CastPayload.toJson(): WritableMap = Arguments.createMap().apply { putString("type", type) } -private fun WritableMap.putStringIfNotNull(name: String, value: String?) { - value?.let { putString(name, value) } -} +private fun WritableMap.putStringIfNotNull(name: String, value: String?) = value?.let { putString(name, value) } + +private inline fun ReadableMap.castValues(): Map = toHashMap().mapValues { it.value as T } 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 699dc860..1d66454e 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,5 +1,6 @@ package com.bitmovin.player.reactnative.extensions +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap fun ReadableMap.getBooleanOrNull( @@ -35,6 +36,11 @@ inline fun ReadableMap.withString( block: (String) -> T, ): T? = getString(key)?.let(block) +inline fun ReadableMap.withArray( + key: String, + block: (ReadableArray) -> T, +): T? = getArray(key)?.let(block) + /** * Reads the [Boolean] value from the given [ReadableMap] if the [key] is present. * Returns the [default] value otherwise. From f60caee6604a61748997fa16946477cc6af44f15 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 13 Nov 2023 17:16:22 +0100 Subject: [PATCH 13/48] chore: add documentation --- .../bitmovin/player/reactnative/BitmovinBaseModule.kt | 9 +++++++++ .../java/com/bitmovin/player/reactnative/BufferModule.kt | 3 +-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index 58073344..cae5fbb9 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -8,6 +8,15 @@ import com.bitmovin.player.reactnative.extensions.uiManagerModule import com.facebook.react.bridge.* import com.facebook.react.uimanager.UIManagerModule +/** + * Base for Bitmovin React Module. + * + * 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) { 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 614d2dd3..e9ee63f3 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt @@ -45,8 +45,7 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex } } - private fun String.toBufferTypeOrThrow() = toBufferType() - ?: throw IllegalArgumentException("Invalid buffer type") + private fun String.toBufferTypeOrThrow() = toBufferType() ?: throw IllegalArgumentException(INVALID_BUFFER_TYPE) } /** From 7b81d83d8ad9b3cea5cfb1c553b4650f55f85112 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 13 Nov 2023 18:49:57 +0100 Subject: [PATCH 14/48] refactor: make map&array helpers type safe --- .../player/reactnative/BitmovinBaseModule.kt | 2 +- .../player/reactnative/OfflineModule.kt | 6 +-- .../reactnative/converter/JsonConverter.kt | 39 ++++++++++--------- .../reactnative/extensions/ReadableArray.kt | 39 +++++-------------- .../reactnative/extensions/ReadableMap.kt | 23 ++++++----- .../extensions/ReadableMapExtension.kt | 23 ++--------- 6 files changed, 48 insertions(+), 84 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index cae5fbb9..990f13ab 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -47,7 +47,7 @@ abstract class BitmovinBaseModule( } /** Run [block], forwarding the return value. If it throws, sets [Promise.reject] and return null. */ -inline fun Promise.runAndRejectOnException(crossinline block: PromiseRejectOnExceptionBlock.() -> T): T? = try { +inline fun Promise.runAndRejectOnException(block: PromiseRejectOnExceptionBlock.() -> T): T? = try { PromiseRejectOnExceptionBlock.block() } catch (e: Exception) { reject(e) 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 79bc4be8..f6fad488 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt @@ -3,7 +3,7 @@ package com.bitmovin.player.reactnative import com.bitmovin.player.api.offline.options.OfflineOptionEntryState import com.bitmovin.player.reactnative.converter.toSourceConfig import com.bitmovin.player.reactnative.extensions.drmModule -import com.bitmovin.player.reactnative.extensions.toList +import com.bitmovin.player.reactnative.extensions.toStringList import com.bitmovin.player.reactnative.extensions.uiManagerModule import com.bitmovin.player.reactnative.offline.OfflineContentManagerBridge import com.bitmovin.player.reactnative.offline.OfflineDownloadRequest @@ -141,8 +141,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext return@safeOfflineContentManager } - val audioOptionIds = request.getArray("audioOptionIds")?.toList()?.filterNotNull() - val textOptionIds = request.getArray("textOptionIds")?.toList()?.filterNotNull() + val audioOptionIds = request.getArray("audioOptionIds")?.toStringList()?.filterNotNull() + val textOptionIds = request.getArray("textOptionIds")?.toStringList()?.filterNotNull() getOfflineContentManagerBridge(nativeId)?.process( OfflineDownloadRequest(minimumBitRate, audioOptionIds, textOptionIds), 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 577b2201..eb2bdce0 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 @@ -51,14 +51,12 @@ import com.bitmovin.player.reactnative.RNBufferLevels import com.bitmovin.player.reactnative.RNPlayerViewConfigWrapper 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.putBoolean import com.bitmovin.player.reactnative.extensions.putDouble import com.bitmovin.player.reactnative.extensions.putInt import com.bitmovin.player.reactnative.extensions.set -import com.bitmovin.player.reactnative.extensions.toList +import com.bitmovin.player.reactnative.extensions.toMapList import com.bitmovin.player.reactnative.extensions.toReadableArray import com.bitmovin.player.reactnative.extensions.toReadableMap import com.bitmovin.player.reactnative.extensions.withArray @@ -67,6 +65,7 @@ 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 @@ -117,7 +116,7 @@ private fun ReadableMap.toRemoteControlConfig(): RemoteControlConfig = RemoteCon * Converts an arbitrary `json` to `SourceOptions`. */ fun ReadableMap.toSourceOptions(): SourceOptions = SourceOptions( - startOffset = getDoubleOrNull("startOffset"), + startOffset = getDouble("startOffset"), startOffsetTimelineReference = getString("startOffsetTimelineReference")?.toTimelineReferencePoint(), ) @@ -164,11 +163,11 @@ fun ReadableMap.toTweaksConfig(): TweaksConfig = TweaksConfig().apply { withDouble("timeChangedInterval") { timeChangedInterval = it } withInt("bandwidthEstimateWeightLimit") { bandwidthEstimateWeightLimit = it } withMap("devicesThatRequireSurfaceWorkaround") { devices -> - val deviceNames = devices.withArray("deviceNames") { - it.toList().filterNotNull().map(::DeviceName) + val deviceNames = devices.withStringArray("deviceNames") { + it.filterNotNull().map(::DeviceName) } ?: emptyList() - val modelNames = devices.withArray("modelNames") { - it.toList().filterNotNull().map(::DeviceName) + val modelNames = devices.withStringArray("modelNames") { + it.filterNotNull().map(::DeviceName) } ?: emptyList() devicesThatRequireSurfaceWorkaround = deviceNames + modelNames } @@ -183,19 +182,21 @@ fun ReadableMap.toTweaksConfig(): TweaksConfig = TweaksConfig().apply { /** * Converts any JS object into an `AdvertisingConfig` object. */ -fun ReadableMap.toAdvertisingConfig(): AdvertisingConfig? = getArray("schedule") - ?.toList() - ?.mapNotNull { it?.toAdItem() } - ?.let { AdvertisingConfig(it) } +fun ReadableMap.toAdvertisingConfig(): AdvertisingConfig? { + return AdvertisingConfig( + getArray("schedule")?.toMapList()?.mapNotNull { it?.toAdItem() } ?: return null, + ) +} /** * Converts any JS object into an `AdItem` object. */ -fun ReadableMap.toAdItem(): AdItem? = getArray("sources") - ?.toList() - ?.mapNotNull { it?.toAdSource() } - ?.toTypedArray() - ?.let { AdItem(it, getString("position") ?: "pre") } +fun ReadableMap.toAdItem(): AdItem? { + return AdItem( + sources = getArray("sources") ?.toMapList()?.mapNotNull { it?.toAdSource() }?.toTypedArray() ?: return null, + position = getString("position") ?: "pre", + ) +} /** * Converts any JS object into an `AdSource` object. @@ -479,8 +480,8 @@ fun PlayerEvent.toJson(): WritableMap { * Converts an arbitrary `json` into [BitmovinCastManagerOptions]. */ fun ReadableMap.toCastOptions(): BitmovinCastManagerOptions = BitmovinCastManagerOptions( - applicationId = getOrDefault("applicationId", null), - messageNamespace = getOrDefault("messageNamespace", null), + applicationId = getString("applicationId"), + messageNamespace = getString("messageNamespace"), ) /** 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..9d2b3e87 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,15 @@ 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 - } - } +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.toReadableArray(): ReadableArray = Arguments.fromList(this) 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..a541b79e 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,17 @@ package com.bitmovin.player.reactnative.extensions import com.facebook.react.bridge.* -inline fun Map.toReadableMap(): ReadableMap = Arguments.createMap().apply { +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) } } + +fun Map.toReadableMap(): ReadableMap = toReadableMap(WritableMap::putBoolean) +fun Map.toReadableMap(): ReadableMap = toReadableMap(WritableMap::putString) +fun Map.toReadableMap(): ReadableMap = toReadableMap(WritableMap::putDouble) +fun Map.toReadableMap(): ReadableMap = toReadableMap(WritableMap::putInt) +fun Map.toReadableMap(): ReadableMap = toReadableMap(WritableMap::putArray) +fun Map.toReadableMap(): ReadableMap = toReadableMap(WritableMap::putMap) 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 1d66454e..e7531390 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 @@ -7,10 +7,6 @@ fun ReadableMap.getBooleanOrNull( key: String, ): Boolean? = takeIf { hasKey(key) }?.getBoolean(key) -fun ReadableMap.getDoubleOrNull( - key: String, -): Double? = takeIf { hasKey(key) }?.getDouble(key) - inline fun ReadableMap.withDouble( key: String, block: (Double) -> T, @@ -41,20 +37,7 @@ inline fun ReadableMap.withArray( block: (ReadableArray) -> T, ): T? = getArray(key)?.let(block) -/** - * Reads the [Boolean] value from the given [ReadableMap] if the [key] is present. - * Returns the [default] value otherwise. - */ -fun ReadableMap.getOrDefault( - key: String, - default: Boolean, -) = if (hasKey(key)) getBoolean(key) else default - -/** - * Reads the [String] value from the given [ReadableMap] if the [key] is present. - * Returns the [default] value otherwise. - */ -fun ReadableMap.getOrDefault( +inline fun ReadableMap.withStringArray( key: String, - default: String?, -) = if (hasKey(key)) getString(key) else default + block: (List) -> T, +): T? = getArray(key)?.toStringList()?.let(block) From 508caaa9c061bf5844565beb9deb0323be0c2bfb Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 13 Nov 2023 18:52:48 +0100 Subject: [PATCH 15/48] refactor: cast manager module --- .../reactnative/BitmovinCastManagerModule.kt | 42 +++++++------------ 1 file changed, 14 insertions(+), 28 deletions(-) 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 c81568dd..26fb799a 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt @@ -2,69 +2,55 @@ package com.bitmovin.player.reactnative import com.bitmovin.player.casting.BitmovinCastManager import com.bitmovin.player.reactnative.converter.toCastOptions -import com.bitmovin.player.reactnative.extensions.uiManagerModule 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.resolveOnUIThread { + BitmovinCastManager.isInitialized() } /** * Initializes the [BitmovinCastManager] with the given options. */ @ReactMethod - fun initializeCastManager(options: ReadableMap?, promise: Promise) { + fun initializeCastManager(options: ReadableMap?, promise: Promise) = promise.resolveOnUIThread { val castOptions = options?.toCastOptions() - uiManager?.addUIBlock { - BitmovinCastManager.initialize( - castOptions?.applicationId, - castOptions?.messageNamespace, - ) - promise.resolve(null) - } + 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.resolveOnUIThread { + BitmovinCastManager.getInstance().sendMessage(message, messageNamespace) + promise.resolve(null) } /** * 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.resolveOnUIThread { + BitmovinCastManager.getInstance().updateContext(currentActivity) + promise.resolve(null) } - - private val uiManager: UIManagerModule? get() = context.uiManagerModule } /** From ce3d5b851029e48adb27f408bd0e413f081ff40c Mon Sep 17 00:00:00 2001 From: Bitmovin Release Automation Date: Tue, 14 Nov 2023 09:45:14 +0000 Subject: [PATCH 16/48] chore: prepare release 0.14.0 --- CHANGELOG.md | 2 +- example/ios/Podfile.lock | 6 +++--- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcf0bd20..faa9cefd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.14.0] (2023-11-14) ### Added 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", From 5cd38c8054b59caf27a7e2ffa9cd4ea18d00d8c3 Mon Sep 17 00:00:00 2001 From: Michele Pozzi <96702967+123mpozzi@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:46:44 +0100 Subject: [PATCH 17/48] Remove unnecessary dot from CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index faa9cefd..ec0a1aa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### 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 From 9c8644721d45f25f89266bddde0d99dccc6398ee Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Tue, 14 Nov 2023 14:23:34 +0100 Subject: [PATCH 18/48] fix: don't return `Unit` --- .../java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index 990f13ab..4998a946 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -25,7 +25,8 @@ abstract class BitmovinBaseModule( val uiManager = runAndRejectOnException { uiManager } ?: return uiManager.addUIBlock { runAndRejectOnException { - resolve(block()) + // Promise only support built-in types. Functions that return [Unit] must resolve to `null`. + resolve(block().takeUnless { it is Unit }) } } } From c969a41d334706f76037253574332fa4c46b031a Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Tue, 14 Nov 2023 14:24:09 +0100 Subject: [PATCH 19/48] fix: remove unnecessary promise resolve --- .../bitmovin/player/reactnative/BitmovinCastManagerModule.kt | 2 -- 1 file changed, 2 deletions(-) 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 26fb799a..4db9eaa3 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt @@ -40,7 +40,6 @@ class BitmovinCastManagerModule(context: ReactApplicationContext) : BitmovinBase @ReactMethod fun sendMessage(message: String, messageNamespace: String?, promise: Promise) = promise.resolveOnUIThread { BitmovinCastManager.getInstance().sendMessage(message, messageNamespace) - promise.resolve(null) } /** @@ -49,7 +48,6 @@ class BitmovinCastManagerModule(context: ReactApplicationContext) : BitmovinBase @ReactMethod fun updateContext(promise: Promise) = promise.resolveOnUIThread { BitmovinCastManager.getInstance().updateContext(currentActivity) - promise.resolve(null) } } From 598d8bfd83327882563d185c945839f61e0721b4 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Tue, 14 Nov 2023 14:24:36 +0100 Subject: [PATCH 20/48] fix: factorize with* methods --- .../extensions/ReadableMapExtension.kt | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) 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 e7531390..0aadfeab 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 @@ -10,34 +10,40 @@ fun ReadableMap.getBooleanOrNull( inline fun ReadableMap.withDouble( key: String, block: (Double) -> T, -): T? = takeIf { hasKey(key) }?.getDouble(key)?.let(block) +): T? = mapValue(key, ReadableMap::getDouble, block) inline fun ReadableMap.withMap( key: String, block: (ReadableMap) -> T, -): T? = takeIf { hasKey(key) }?.getMap(key)?.let(block) +): T? = mapValue(key, ReadableMap::getMap, block) inline fun ReadableMap.withInt( key: String, block: (Int) -> T, -): T? = takeIf { hasKey(key) }?.getInt(key)?.let(block) +): T? = mapValue(key, ReadableMap::getInt, block) inline fun ReadableMap.withBoolean( key: String, block: (Boolean) -> T, -): T? = takeIf { hasKey(key) }?.getBoolean(key)?.let(block) +): T? = mapValue(key, ReadableMap::getBoolean, block) inline fun ReadableMap.withString( key: String, block: (String) -> T, -): T? = getString(key)?.let(block) +): T? = mapValue(key, ReadableMap::getString, block) inline fun ReadableMap.withArray( key: String, block: (ReadableArray) -> T, -): T? = getArray(key)?.let(block) +): T? = mapValue(key, ReadableMap::getArray, block) inline fun ReadableMap.withStringArray( key: String, block: (List) -> T, -): T? = getArray(key)?.toStringList()?.let(block) +): T? = mapValue(key, { getArray(it)?.toStringList() }, block) + +inline fun ReadableMap.mapValue( + key: String, + get: ReadableMap.(String) -> T?, + block: (T) -> R, +) = takeIf{ hasKey(key) }?.get(key)?.let(block) \ No newline at end of file From efe8065cb3fe78a15c1fe8360f6098f7e347a5c2 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Tue, 14 Nov 2023 15:15:49 +0100 Subject: [PATCH 21/48] refactor: move helper method to extension --- .../player/reactnative/converter/JsonConverter.kt | 9 ++++----- .../player/reactnative/extensions/ReadableArray.kt | 2 +- .../player/reactnative/extensions/ReadableMap.kt | 3 ++- .../reactnative/extensions/ReadableMapExtension.kt | 7 ++++--- 4 files changed, 11 insertions(+), 10 deletions(-) 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 eb2bdce0..88c77b84 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 @@ -56,6 +56,7 @@ 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.set +import com.bitmovin.player.reactnative.extensions.toMap import com.bitmovin.player.reactnative.extensions.toMapList import com.bitmovin.player.reactnative.extensions.toReadableArray import com.bitmovin.player.reactnative.extensions.toReadableMap @@ -105,7 +106,7 @@ fun ReadableMap.toBufferConfig(): BufferConfig = BufferConfig().apply { */ private fun ReadableMap.toRemoteControlConfig(): RemoteControlConfig = RemoteControlConfig().apply { withString("receiverStylesheetUrl") { receiverStylesheetUrl = it } - withMap("customReceiverConfig") { customReceiverConfig = it.castValues() } + withMap("customReceiverConfig") { customReceiverConfig = it.toMap() } withBoolean("isCastEnabled") { isCastEnabled = it } withBoolean("sendManifestRequestsWithCredentials") { sendManifestRequestsWithCredentials = it } withBoolean("sendSegmentRequestsWithCredentials") { sendSegmentRequestsWithCredentials = it } @@ -240,7 +241,7 @@ fun ReadableMap.toSourceConfig(): SourceConfig? { } } withString("thumbnailTrack") { thumbnailTrack = it.toThumbnailTrack() } - withMap("metadata") { metadata = it.castValues() } + withMap("metadata") { metadata = it.toMap() } withMap("options") { options = it.toSourceOptions() } } } @@ -491,7 +492,7 @@ fun ReadableMap.toWidevineConfig(): WidevineConfig? = getMap("widevine")?.run { WidevineConfig(getString("licenseUrl")).apply { withString("preferredSecurityLevel") { preferredSecurityLevel = it } withBoolean("shouldKeepDrmSessionsAlive") { shouldKeepDrmSessionsAlive = it } - withMap("httpHeaders") { httpHeaders = it.castValues().toMutableMap() } + withMap("httpHeaders") { httpHeaders = it.toMap().toMutableMap() } } } @@ -811,5 +812,3 @@ private fun CastPayload.toJson(): WritableMap = Arguments.createMap().apply { } private fun WritableMap.putStringIfNotNull(name: String, value: String?) = value?.let { putString(name, value) } - -private inline fun ReadableMap.castValues(): Map = toHashMap().mapValues { it.value as T } 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 9d2b3e87..885fc1df 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,7 +2,7 @@ package com.bitmovin.player.reactnative.extensions import com.facebook.react.bridge.* -fun ReadableArray.toList(convert: (Dynamic) -> T): List = (0 until size()).map { i -> +inline fun ReadableArray.toList(convert: (Dynamic) -> T): List = (0 until size()).map { i -> convert(getDynamic(i)) } 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 a541b79e..9d8dc03e 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,7 +2,8 @@ package com.bitmovin.player.reactnative.extensions import com.facebook.react.bridge.* -inline fun Map.toReadableMap( +/** 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 { 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 0aadfeab..0224623b 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,7 +1,6 @@ package com.bitmovin.player.reactnative.extensions -import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.* fun ReadableMap.getBooleanOrNull( key: String, @@ -46,4 +45,6 @@ inline fun ReadableMap.mapValue( key: String, get: ReadableMap.(String) -> T?, block: (T) -> R, -) = takeIf{ hasKey(key) }?.get(key)?.let(block) \ No newline at end of file +) = takeIf { hasKey(key) }?.get(key)?.let(block) + +inline fun ReadableMap.toMap(): Map = toHashMap().mapValues { it.value as T } From d3c9572e533825451b9b995202b91c471cab2167 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Tue, 14 Nov 2023 15:20:42 +0100 Subject: [PATCH 22/48] refactor: rename PromiseRejectOnExceptionBlock to RejectPromiseOnExceptionBlock --- .../player/reactnative/BitmovinBaseModule.kt | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index 4998a946..2d9fba31 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -1,6 +1,7 @@ package com.bitmovin.player.reactnative import com.bitmovin.player.api.Player +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 @@ -9,19 +10,20 @@ import com.facebook.react.bridge.* import com.facebook.react.uimanager.UIManagerModule /** - * Base for Bitmovin React Module. + * 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) { /** [resolve] the [Promise] by running [block] in the UI thread with [UIManagerModule.addUIBlock]. */ - protected inline fun Promise.resolveOnUIThread(crossinline block: PromiseRejectOnExceptionBlock.() -> T) { + protected inline fun Promise.resolveOnUIThread(crossinline block: RejectPromiseOnExceptionBlock.() -> T) { val uiManager = runAndRejectOnException { uiManager } ?: return uiManager.addUIBlock { runAndRejectOnException { @@ -31,29 +33,32 @@ abstract class BitmovinBaseModule( } } - protected val PromiseRejectOnExceptionBlock.playerModule: PlayerModule get() = context.playerModule + protected val RejectPromiseOnExceptionBlock.playerModule: PlayerModule get() = context.playerModule ?: throw IllegalArgumentException("PlayerModule not found") - protected val PromiseRejectOnExceptionBlock.uiManager: UIManagerModule get() = context.uiManagerModule + protected val RejectPromiseOnExceptionBlock.uiManager: UIManagerModule get() = context.uiManagerModule ?: throw IllegalStateException("UIManager not found") - protected val PromiseRejectOnExceptionBlock.sourceModule: SourceModule get() = context.sourceModule + protected val RejectPromiseOnExceptionBlock.sourceModule: SourceModule get() = context.sourceModule ?: throw IllegalStateException("SourceModule not found") - protected val PromiseRejectOnExceptionBlock.offlineModule: OfflineModule get() = context.offlineModule + protected val RejectPromiseOnExceptionBlock.offlineModule: OfflineModule get() = context.offlineModule ?: throw IllegalStateException("OfflineModule not found") - fun PromiseRejectOnExceptionBlock.getPlayer(nativeId: NativeId): Player = playerModule.getPlayerOrNull(nativeId) + protected val RejectPromiseOnExceptionBlock.drmModule: DrmModule get() = context.drmModule + ?: throw IllegalStateException("DrmModule not found") + + fun RejectPromiseOnExceptionBlock.getPlayer(nativeId: NativeId): Player = playerModule.getPlayerOrNull(nativeId) ?: throw IllegalArgumentException("Invalid PlayerId") } /** Run [block], forwarding the return value. If it throws, sets [Promise.reject] and return null. */ -inline fun Promise.runAndRejectOnException(block: PromiseRejectOnExceptionBlock.() -> T): T? = try { - PromiseRejectOnExceptionBlock.block() +inline fun Promise.runAndRejectOnException(block: RejectPromiseOnExceptionBlock.() -> T): T? = try { + RejectPromiseOnExceptionBlock.block() } catch (e: Exception) { reject(e) null } /** Receiver of code that can safely throw when resolving a [Promise]. */ -object PromiseRejectOnExceptionBlock +object RejectPromiseOnExceptionBlock From a6528bb32186a16a2b8c00f465f40ae113d10114 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Tue, 14 Nov 2023 15:21:07 +0100 Subject: [PATCH 23/48] refactor: migrate SourceModule to BitmovinBaseModule --- .../player/reactnative/SourceModule.kt | 119 ++++++------------ .../bitmovin/player/reactnative/UuidModule.kt | 2 +- 2 files changed, 38 insertions(+), 83 deletions(-) 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 686b3a81..f69438df 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt @@ -1,25 +1,22 @@ 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.toAnalyticsSourceMetadata import com.bitmovin.player.reactnative.converter.toJson import com.bitmovin.player.reactnative.converter.toSourceConfig +import com.bitmovin.player.reactnative.extensions.toMap 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. */ @@ -51,15 +48,9 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB drmNativeId: NativeId?, config: ReadableMap?, sourceRemoteControlConfig: ReadableMap?, - analyticsSourceMetadata: ReadableMap?, - ) { - uiManager()?.addUIBlock { - val sourceMetadata = analyticsSourceMetadata?.toAnalyticsSourceMetadata() ?: 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 @@ -75,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 = config?.toSourceConfig()?.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.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) } } @@ -120,8 +106,8 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isAttachedToPlayer(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(sources[nativeId]?.isAttachedToPlayer) + promise.resolveOnUIThread { + getSource(nativeId).isAttachedToPlayer } } @@ -132,55 +118,48 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun isActive(nativeId: NativeId, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(sources[nativeId]?.isActive) + promise.resolveOnUIThread { + getSource(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.resolveOnUIThread { + getSource(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.resolveOnUIThread { + getSource(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.resolveOnUIThread { + getSource(nativeId).config.metadata } } /** * 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.resolveOnUIThread { + getSource(nativeId).config.metadata = metadata?.toMap() } } @@ -191,32 +170,8 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB */ @ReactMethod fun getThumbnail(nativeId: NativeId, time: Double, promise: Promise) { - uiManager()?.addUIBlock { - promise.resolve(sources[nativeId]?.getThumbnail(time)?.toJson()) - } - } - - /** - * Helper method that converts a React `ReadableMap` into a kotlin String -> String map. - */ - private fun asStringMap(readableMap: ReadableMap?): Map? { - if (readableMap == null) { - return null - } - val map = mutableMapOf() - for (entry in readableMap.entryIterator) { - map[entry.key] = entry.value.toString() + promise.resolveOnUIThread { + getSource(nativeId).getThumbnail(time)?.toJson() } - 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) } 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..f56fb959 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/UuidModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/UuidModule.kt @@ -7,7 +7,7 @@ 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. */ From fe244b9dec7f860df221d52a8b3a4fd1223639be Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 17 Nov 2023 09:33:06 +0100 Subject: [PATCH 24/48] refactor: migrate offline module --- .../player/reactnative/BitmovinBaseModule.kt | 7 + .../player/reactnative/OfflineModule.kt | 153 +++++++----------- .../bitmovin/player/reactnative/UuidModule.kt | 1 - .../extensions/ReadableMapExtension.kt | 17 +- .../offline/OfflineContentManagerBridge.kt | 21 ++- 5 files changed, 91 insertions(+), 108 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index 2d9fba31..979ceafa 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -60,5 +60,12 @@ inline fun Promise.runAndRejectOnException(block: RejectPromiseOnExceptionBl null } +/** Resolve the [Promise] with the value returned by [block]. If it throws, sets [Promise.reject]. */ +inline fun Promise.resolveOnCurrentThread(block: RejectPromiseOnExceptionBlock.() -> T): Unit = try { + resolve(RejectPromiseOnExceptionBlock.block()) +} catch (e: Exception) { + reject(e) +} + /** Receiver of code that can safely throw when resolving a [Promise]. */ object RejectPromiseOnExceptionBlock 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 f6fad488..464833e8 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt @@ -3,17 +3,18 @@ package com.bitmovin.player.reactnative import com.bitmovin.player.api.offline.options.OfflineOptionEntryState import com.bitmovin.player.reactnative.converter.toSourceConfig import com.bitmovin.player.reactnative.extensions.drmModule -import com.bitmovin.player.reactnative.extensions.toStringList -import com.bitmovin.player.reactnative.extensions.uiManagerModule +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 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. @@ -27,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. @@ -59,33 +60,32 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun initWithConfig(nativeId: NativeId, config: ReadableMap?, drmNativeId: NativeId?, promise: Promise) { - context.uiManagerModule?.addUIBlock { - if (!offlineContentManagerBridges.containsKey(nativeId)) { - val identifier = config?.getString("identifier") - val sourceConfig = config?.getMap("sourceConfig")?.toSourceConfig() - sourceConfig?.drmConfig = context.drmModule?.getConfig(drmNativeId) + promise.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.resolveWithBridge(nativeId) { + state.name } } @@ -96,9 +96,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun getOptions(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.resolveWithBridge(nativeId) { getOptions() - promise.resolve(null) } } @@ -110,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")?.toStringList()?.filterNotNull() - val textOptionIds = request.getArray("textOptionIds")?.toStringList()?.filterNotNull() - - getOfflineContentManagerBridge(nativeId)?.process( - OfflineDownloadRequest(minimumBitRate, audioOptionIds, textOptionIds), + fun download(nativeId: NativeId, request: ReadableMap, promise: Promise) { + promise.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)) } } @@ -160,9 +135,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun resume(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.resolveWithBridge(nativeId) { resume() - promise.resolve(null) } } @@ -172,9 +146,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun suspend(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.resolveWithBridge(nativeId) { suspend() - promise.resolve(null) } } @@ -184,9 +157,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun cancelDownload(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.resolveWithBridge(nativeId) { cancelDownload() - promise.resolve(null) } } @@ -196,7 +168,7 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun usedStorage(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.resolveWithBridge(nativeId) { promise.resolve(offlineContentManager.usedStorage.toDouble()) } } @@ -207,9 +179,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun deleteAll(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.resolveWithBridge(nativeId) { deleteAll() - promise.resolve(null) } } @@ -221,9 +192,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun downloadLicense(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.resolveWithBridge(nativeId) { downloadLicense() - promise.resolve(null) } } @@ -235,9 +205,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun releaseLicense(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.resolveWithBridge(nativeId) { releaseLicense() - promise.resolve(null) } } @@ -249,9 +218,8 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun renewOfflineLicense(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.resolveWithBridge(nativeId) { renewOfflineLicense() - promise.resolve(null) } } @@ -263,19 +231,18 @@ class OfflineModule(private val context: ReactApplicationContext) : ReactContext */ @ReactMethod fun release(nativeId: NativeId, promise: Promise) { - safeOfflineContentManager(nativeId, promise) { + promise.resolveWithBridge(nativeId) { release() offlineContentManagerBridges.remove(nativeId) - promise.resolve(null) } } - private fun safeOfflineContentManager( + private fun Promise.resolveWithBridge( nativeId: NativeId, - promise: Promise, runBlock: OfflineContentManagerBridge.() -> Unit, ) { - getOfflineContentManagerBridge(nativeId)?.let(runBlock) - ?: promise.reject(IllegalArgumentException("Could not find the offline module instance")) + resolveOnCurrentThread { + getOfflineContentManagerBridge(nativeId).runBlock() + } } } 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 f56fb959..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,7 +1,6 @@ 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 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 0224623b..56acac40 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 @@ -4,7 +4,16 @@ import com.facebook.react.bridge.* fun ReadableMap.getBooleanOrNull( key: String, -): Boolean? = takeIf { hasKey(key) }?.getBoolean(key) +): Boolean? = getValueOrNull(key, ReadableMap::getBoolean) + +fun ReadableMap.getIntOrNull( + key: String, +): Int? = getValueOrNull(key, ReadableMap::getInt) + +inline fun ReadableMap.getValueOrNull( + key: String, + get: ReadableMap.(String) -> T?, +) = takeIf { hasKey(key) }?.get(key) inline fun ReadableMap.withDouble( key: String, @@ -39,12 +48,14 @@ inline fun ReadableMap.withArray( inline fun ReadableMap.withStringArray( key: String, block: (List) -> T, -): T? = mapValue(key, { getArray(it)?.toStringList() }, block) +): T? = mapValue(key, ReadableMap::getStringArray, block) + +fun ReadableMap.getStringArray(it: String) = getArray(it)?.toStringList() inline fun ReadableMap.mapValue( key: String, get: ReadableMap.(String) -> T?, block: (T) -> R, -) = takeIf { hasKey(key) }?.get(key)?.let(block) +) = 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 56b00572..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 @@ -13,7 +13,7 @@ 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, @@ -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) From 9f8abdbe48aefab35fcb2043f358904f9f7dd3dc Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 17 Nov 2023 09:33:17 +0100 Subject: [PATCH 25/48] refactor: migrate drm module --- .../bitmovin/player/reactnative/DrmModule.kt | 70 +++++++++---------- 1 file changed, 32 insertions(+), 38 deletions(-) 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 5451bc3d..2ab23055 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt @@ -5,9 +5,9 @@ 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.toWidevineConfig -import com.bitmovin.player.reactnative.extensions.uiManagerModule import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule +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?) { - context.uiManagerModule?.addUIBlock { - if (!drmConfigs.containsKey(nativeId)) { - config?.toWidevineConfig()?.let { - drmConfigs[nativeId] = it - initPrepareMessage(nativeId, config) - initPrepareLicense(nativeId, config) - } + fun initWithConfig(nativeId: NativeId, config: ReadableMap, promise: Promise) { + promise.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) } /** From 3fd09069aa7fc29042181f79c01c20bc1be421c9 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 17 Nov 2023 09:33:47 +0100 Subject: [PATCH 26/48] refactor: remove duplicated promise.resolve --- .../java/com/bitmovin/player/reactnative/PlayerModule.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 00c12c64..6222f17a 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -108,7 +108,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex ) { promise.resolveOnUIThread { offlineModule - .getOfflineContentManagerBridge(offlineContentManagerBridgeId) + .getOfflineContentManagerBridgeOrNull(offlineContentManagerBridgeId) ?.offlineContentManager ?.offlineSourceConfig ?.let { getPlayer(nativeId).load(it) } @@ -547,7 +547,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun getPlaybackSpeed(nativeId: NativeId, promise: Promise) { promise.resolveOnUIThread { - promise.resolve(players[nativeId]?.playbackSpeed) + getPlayer(nativeId).playbackSpeed } } @@ -557,7 +557,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun setPlaybackSpeed(nativeId: NativeId, playbackSpeed: Float, promise: Promise) { promise.resolveOnUIThread { - players[nativeId]?.playbackSpeed = playbackSpeed + getPlayer(nativeId).playbackSpeed = playbackSpeed } } } From edd2bfa0aab4efbaf2435713aadf5e497b3dfeaf Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 17 Nov 2023 09:34:16 +0100 Subject: [PATCH 27/48] refactor: remove duplicated promise.resolve --- .../main/java/com/bitmovin/player/reactnative/OfflineModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 464833e8..9e4945bd 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt @@ -169,7 +169,7 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte @ReactMethod fun usedStorage(nativeId: NativeId, promise: Promise) { promise.resolveWithBridge(nativeId) { - promise.resolve(offlineContentManager.usedStorage.toDouble()) + offlineContentManager.usedStorage.toDouble() } } From 2aff015d83788c32330e47f7117a34545377623f Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 22 Nov 2023 10:10:49 +0100 Subject: [PATCH 28/48] fix: allow duration not propagated --- .../java/com/bitmovin/player/reactnative/OfflineModule.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 9e4945bd..e12511de 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt @@ -237,9 +237,9 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte } } - private fun Promise.resolveWithBridge( + private fun Promise.resolveWithBridge( nativeId: NativeId, - runBlock: OfflineContentManagerBridge.() -> Unit, + runBlock: OfflineContentManagerBridge.() -> T, ) { resolveOnCurrentThread { getOfflineContentManagerBridge(nativeId).runBlock() From 8d1d05c0e88ecb2dd539651b40fb2bc64793ea52 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 22 Nov 2023 10:15:51 +0100 Subject: [PATCH 29/48] fix: don't resolve Unit --- .../com/bitmovin/player/reactnative/BitmovinBaseModule.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index 979ceafa..c6f4c2db 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -26,9 +26,8 @@ abstract class BitmovinBaseModule( protected inline fun Promise.resolveOnUIThread(crossinline block: RejectPromiseOnExceptionBlock.() -> T) { val uiManager = runAndRejectOnException { uiManager } ?: return uiManager.addUIBlock { - runAndRejectOnException { - // Promise only support built-in types. Functions that return [Unit] must resolve to `null`. - resolve(block().takeUnless { it is Unit }) + resolveOnCurrentThread { + resolve(block()) } } } @@ -62,7 +61,8 @@ inline fun Promise.runAndRejectOnException(block: RejectPromiseOnExceptionBl /** Resolve the [Promise] with the value returned by [block]. If it throws, sets [Promise.reject]. */ inline fun Promise.resolveOnCurrentThread(block: RejectPromiseOnExceptionBlock.() -> T): Unit = try { - resolve(RejectPromiseOnExceptionBlock.block()) + // Promise only support built-in types. Functions that return [Unit] must resolve to `null`. + resolve(RejectPromiseOnExceptionBlock.block().takeUnless { it is Unit }) } catch (e: Exception) { reject(e) } From d733d09cd4c37bbcd3f1df164a89391efecd15cb Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 29 Nov 2023 20:07:12 +0100 Subject: [PATCH 30/48] refactor: introduce a type safe promise --- .../player/reactnative/BitmovinBaseModule.kt | 44 +++-- .../reactnative/BitmovinCastManagerModule.kt | 8 +- .../player/reactnative/BufferModule.kt | 4 +- .../bitmovin/player/reactnative/DrmModule.kt | 2 +- .../player/reactnative/OfflineModule.kt | 32 ++-- .../reactnative/PlayerAnalyticsModule.kt | 4 +- .../player/reactnative/PlayerModule.kt | 173 +++++++++--------- .../player/reactnative/SourceModule.kt | 47 +++-- .../reactnative/extensions/ReadableMap.kt | 6 +- 9 files changed, 174 insertions(+), 146 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index c6f4c2db..d67be85e 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -1,6 +1,7 @@ package com.bitmovin.player.reactnative 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 @@ -22,13 +23,11 @@ import com.facebook.react.uimanager.UIManagerModule abstract class BitmovinBaseModule( protected val context: ReactApplicationContext, ) : ReactContextBaseJavaModule(context) { - /** [resolve] the [Promise] by running [block] in the UI thread with [UIManagerModule.addUIBlock]. */ - protected inline fun Promise.resolveOnUIThread(crossinline block: RejectPromiseOnExceptionBlock.() -> T) { + /** [resolve] the [TPromise] by running [block] in the UI thread with [UIManagerModule.addUIBlock]. */ + protected inline fun TPromise.resolveOnUiThread(crossinline block: RejectPromiseOnExceptionBlock.() -> R) { val uiManager = runAndRejectOnException { uiManager } ?: return uiManager.addUIBlock { - resolveOnCurrentThread { - resolve(block()) - } + resolveOnCurrentThread{ block() } } } @@ -47,12 +46,19 @@ abstract class BitmovinBaseModule( protected val RejectPromiseOnExceptionBlock.drmModule: DrmModule get() = context.drmModule ?: throw IllegalStateException("DrmModule not found") - fun RejectPromiseOnExceptionBlock.getPlayer(nativeId: NativeId): Player = playerModule.getPlayerOrNull(nativeId) - ?: throw IllegalArgumentException("Invalid PlayerId") + fun RejectPromiseOnExceptionBlock.getPlayer( + nativeId: NativeId, + playerModule: PlayerModule = this.playerModule + ): Player = playerModule.getPlayerOrNull(nativeId) ?: throw IllegalArgumentException("Invalid PlayerId") + + fun RejectPromiseOnExceptionBlock.getSource( + nativeId: NativeId, + sourceModule: SourceModule = this.sourceModule + ): Source = sourceModule.getSourceOrNull(nativeId) ?: throw IllegalArgumentException("Invalid SourceId") } /** Run [block], forwarding the return value. If it throws, sets [Promise.reject] and return null. */ -inline fun Promise.runAndRejectOnException(block: RejectPromiseOnExceptionBlock.() -> T): T? = try { +inline fun TPromise.runAndRejectOnException(block: RejectPromiseOnExceptionBlock.() -> R): R? = try { RejectPromiseOnExceptionBlock.block() } catch (e: Exception) { reject(e) @@ -60,12 +66,28 @@ inline fun Promise.runAndRejectOnException(block: RejectPromiseOnExceptionBl } /** Resolve the [Promise] with the value returned by [block]. If it throws, sets [Promise.reject]. */ -inline fun Promise.resolveOnCurrentThread(block: RejectPromiseOnExceptionBlock.() -> T): Unit = try { - // Promise only support built-in types. Functions that return [Unit] must resolve to `null`. - resolve(RejectPromiseOnExceptionBlock.block().takeUnless { it is Unit }) +inline fun TPromise.resolveOnCurrentThread(crossinline block: RejectPromiseOnExceptionBlock.() -> T): Unit = try { + resolve(RejectPromiseOnExceptionBlock.block()) } catch (e: Exception) { reject(e) } /** 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) = promise.reject(throwable) +} +val Promise.int get() = TPromise(this) +val Promise.unit get() = TPromise(this) +val Promise.string get() = TPromise(this) +val Promise.double get() = TPromise(this) +val Promise.float get() = TPromise(this) +val Promise.bool get() = TPromise(this) +val Promise.map get() = TPromise(this) +val Promise.array get() = TPromise(this) +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 4db9eaa3..6d9cfbd4 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt @@ -18,7 +18,7 @@ class BitmovinCastManagerModule(context: ReactApplicationContext) : BitmovinBase * Returns whether the [BitmovinCastManager] is initialized. */ @ReactMethod - fun isInitialized(promise: Promise) = promise.resolveOnUIThread { + fun isInitialized(promise: Promise) = promise.unit.resolveOnUiThread { BitmovinCastManager.isInitialized() } @@ -26,7 +26,7 @@ class BitmovinCastManagerModule(context: ReactApplicationContext) : BitmovinBase * Initializes the [BitmovinCastManager] with the given options. */ @ReactMethod - fun initializeCastManager(options: ReadableMap?, promise: Promise) = promise.resolveOnUIThread { + fun initializeCastManager(options: ReadableMap?, promise: Promise) = promise.unit.resolveOnUiThread { val castOptions = options?.toCastOptions() BitmovinCastManager.initialize( castOptions?.applicationId, @@ -38,7 +38,7 @@ class BitmovinCastManagerModule(context: ReactApplicationContext) : BitmovinBase * Sends a message to the receiver. */ @ReactMethod - fun sendMessage(message: String, messageNamespace: String?, promise: Promise) = promise.resolveOnUIThread { + fun sendMessage(message: String, messageNamespace: String?, promise: Promise) = promise.unit.resolveOnUiThread { BitmovinCastManager.getInstance().sendMessage(message, messageNamespace) } @@ -46,7 +46,7 @@ class BitmovinCastManagerModule(context: ReactApplicationContext) : BitmovinBase * Updates the context of the [BitmovinCastManager] to the current activity. */ @ReactMethod - fun updateContext(promise: Promise) = promise.resolveOnUIThread { + fun updateContext(promise: Promise) = promise.unit.resolveOnUiThread { BitmovinCastManager.getInstance().updateContext(currentActivity) } } 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 e9ee63f3..a106ef10 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BufferModule.kt @@ -22,7 +22,7 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getLevel(nativeId: NativeId, type: String, promise: Promise) { - promise.resolveOnUIThread { + promise.map.resolveOnUiThread { val player = getPlayer(nativeId) val bufferType = type.toBufferTypeOrThrow() RNBufferLevels( @@ -40,7 +40,7 @@ class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun setTargetLevel(nativeId: NativeId, type: String, value: Double, promise: Promise) { - promise.resolveOnUIThread { + promise.unit.resolveOnUiThread { getPlayer(nativeId).buffer.setTargetLevel(type.toBufferTypeOrThrow(), value) } } 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 2ab23055..650df817 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt @@ -75,7 +75,7 @@ class DrmModule(context: ReactApplicationContext) : BitmovinBaseModule(context) */ @ReactMethod fun initWithConfig(nativeId: NativeId, config: ReadableMap, promise: Promise) { - promise.resolveOnUIThread { + promise.unit.resolveOnUiThread { if (drmConfigs.containsKey(nativeId)) { throw InvalidParameterException("NativeId already exists $nativeId") } 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 e12511de..956e46be 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt @@ -60,7 +60,7 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte */ @ReactMethod fun initWithConfig(nativeId: NativeId, config: ReadableMap?, drmNativeId: NativeId?, promise: Promise) { - promise.resolveOnUIThread { + promise.unit.resolveOnUiThread { if (offlineContentManagerBridges.containsKey(nativeId)) { throw InvalidParameterException("content manager bridge id already exists: $nativeId") } @@ -84,7 +84,7 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte @ReactMethod fun getState(nativeId: NativeId, promise: Promise) { - promise.resolveWithBridge(nativeId) { + promise.string.resolveWithBridge(nativeId) { state.name } } @@ -96,7 +96,7 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte */ @ReactMethod fun getOptions(nativeId: NativeId, promise: Promise) { - promise.resolveWithBridge(nativeId) { + promise.unit.resolveWithBridge(nativeId) { getOptions() } } @@ -110,7 +110,7 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte */ @ReactMethod fun download(nativeId: NativeId, request: ReadableMap, promise: Promise) { - promise.resolveWithBridge(nativeId) { + promise.unit.resolveWithBridge(nativeId) { when (state) { OfflineOptionEntryState.Downloaded -> throw IllegalStateException("Download already completed") OfflineOptionEntryState.Downloading, OfflineOptionEntryState.Failed -> throw IllegalStateException( @@ -135,7 +135,7 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte */ @ReactMethod fun resume(nativeId: NativeId, promise: Promise) { - promise.resolveWithBridge(nativeId) { + promise.unit.resolveWithBridge(nativeId) { resume() } } @@ -146,7 +146,7 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte */ @ReactMethod fun suspend(nativeId: NativeId, promise: Promise) { - promise.resolveWithBridge(nativeId) { + promise.unit.resolveWithBridge(nativeId) { suspend() } } @@ -157,7 +157,7 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte */ @ReactMethod fun cancelDownload(nativeId: NativeId, promise: Promise) { - promise.resolveWithBridge(nativeId) { + promise.unit.resolveWithBridge(nativeId) { cancelDownload() } } @@ -168,7 +168,7 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte */ @ReactMethod fun usedStorage(nativeId: NativeId, promise: Promise) { - promise.resolveWithBridge(nativeId) { + promise.double.resolveWithBridge(nativeId) { offlineContentManager.usedStorage.toDouble() } } @@ -179,7 +179,7 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte */ @ReactMethod fun deleteAll(nativeId: NativeId, promise: Promise) { - promise.resolveWithBridge(nativeId) { + promise.unit.resolveWithBridge(nativeId) { deleteAll() } } @@ -192,7 +192,7 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte */ @ReactMethod fun downloadLicense(nativeId: NativeId, promise: Promise) { - promise.resolveWithBridge(nativeId) { + promise.unit.resolveWithBridge(nativeId) { downloadLicense() } } @@ -205,7 +205,7 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte */ @ReactMethod fun releaseLicense(nativeId: NativeId, promise: Promise) { - promise.resolveWithBridge(nativeId) { + promise.unit.resolveWithBridge(nativeId) { releaseLicense() } } @@ -218,7 +218,7 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte */ @ReactMethod fun renewOfflineLicense(nativeId: NativeId, promise: Promise) { - promise.resolveWithBridge(nativeId) { + promise.unit.resolveWithBridge(nativeId) { renewOfflineLicense() } } @@ -231,18 +231,18 @@ class OfflineModule(context: ReactApplicationContext) : BitmovinBaseModule(conte */ @ReactMethod fun release(nativeId: NativeId, promise: Promise) { - promise.resolveWithBridge(nativeId) { + promise.unit.resolveWithBridge(nativeId) { release() offlineContentManagerBridges.remove(nativeId) } } - private fun Promise.resolveWithBridge( + private inline fun TPromise.resolveWithBridge( nativeId: NativeId, - runBlock: OfflineContentManagerBridge.() -> T, + crossinline block: OfflineContentManagerBridge.() -> T, ) { resolveOnCurrentThread { - getOfflineContentManagerBridge(nativeId).runBlock() + getOfflineContentManagerBridge(nativeId).block() } } } 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 b1035f30..c874e99b 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt @@ -22,7 +22,7 @@ class PlayerAnalyticsModule(context: ReactApplicationContext) : BitmovinBaseModu */ @ReactMethod fun sendCustomDataEvent(nativeId: NativeId, json: ReadableMap, promise: Promise) { - promise.resolveOnUIThread { + promise.unit.resolveOnUiThread { getPlayer(nativeId).analytics?.sendCustomDataEvent(json.toAnalyticsCustomData()) } } @@ -34,7 +34,7 @@ class PlayerAnalyticsModule(context: ReactApplicationContext) : BitmovinBaseModu */ @ReactMethod fun getUserId(playerId: NativeId, promise: Promise) { - promise.resolveOnUIThread { + promise.string.nullable.resolveOnUiThread { getPlayer(playerId).analytics?.userId } } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt index 6222f17a..c41aec97 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -39,7 +39,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun initWithConfig(nativeId: NativeId, config: ReadableMap?, promise: Promise) { - promise.resolveOnUIThread { + promise.unit.resolveOnUiThread { if (!players.containsKey(nativeId)) { config?.toPlayerConfig()?.let { players[nativeId] = Player.create(context, it) @@ -60,7 +60,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex analyticsConfigJson: ReadableMap?, promise: Promise, ) { - promise.resolveOnUIThread { + promise.unit.resolveOnUiThread { if (players.containsKey(nativeId)) { throw IllegalArgumentException("Duplicate player creation for id $nativeId") } @@ -88,8 +88,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun loadSource(nativeId: NativeId, sourceNativeId: String, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).load(sourceModule.getSource(sourceNativeId)) + promise.unit.resolveOnUiThread { + getPlayer(nativeId).load(getSource(sourceNativeId)) } } @@ -106,7 +106,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex options: ReadableMap?, promise: Promise, ) { - promise.resolveOnUIThread { + promise.unit.resolveOnUiThread { offlineModule .getOfflineContentManagerBridgeOrNull(offlineContentManagerBridgeId) ?.offlineContentManager @@ -121,8 +121,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun unload(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).unload() + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + unload() } } @@ -132,8 +132,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun play(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).play() + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + play() } } @@ -143,8 +143,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun pause(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).pause() + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + pause() } } @@ -155,8 +155,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun seek(nativeId: NativeId, time: Double, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).seek(time) + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + seek(time) } } @@ -167,8 +167,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun timeShift(nativeId: NativeId, offset: Double, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).timeShift(offset) + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + timeShift(offset) } } @@ -178,8 +178,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun mute(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).mute() + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + mute() } } @@ -189,8 +189,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun unmute(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).unmute() + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + unmute() } } @@ -200,8 +200,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun destroy(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).destroy() + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + destroy() players.remove(nativeId) } } @@ -213,8 +213,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun setVolume(nativeId: NativeId, volume: Int, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).volume = volume + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + this.volume = volume } } @@ -225,8 +225,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getVolume(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).volume + promise.int.resolveOnUiThreadWithPlayer(nativeId) { + volume } } @@ -237,8 +237,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun source(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).source?.toJson() + promise.map.nullable.resolveOnUiThreadWithPlayer(nativeId) { + source?.toJson() } } @@ -249,14 +249,12 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun currentTime(nativeId: NativeId, mode: String?, promise: Promise) { - promise.resolveOnUIThread { - val player = getPlayer(nativeId) - val timeOffset: Double = when (mode) { - "relative" -> player.playbackTimeOffsetToRelativeTime - "absolute" -> player.playbackTimeOffsetToAbsoluteTime + promise.double.resolveOnUiThreadWithPlayer(nativeId) { + currentTime + when (mode) { + "relative" -> playbackTimeOffsetToRelativeTime + "absolute" -> playbackTimeOffsetToAbsoluteTime else -> throw InvalidParameterException("Unknown mode $mode") } - player.currentTime + timeOffset } } @@ -267,8 +265,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun duration(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).duration + promise.double.resolveOnUiThreadWithPlayer(nativeId) { + duration } } @@ -279,8 +277,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isMuted(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).isMuted + promise.bool.resolveOnUiThreadWithPlayer(nativeId) { + isMuted } } @@ -291,8 +289,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isPlaying(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).isPlaying + promise.bool.resolveOnUiThreadWithPlayer(nativeId) { + isPlaying } } @@ -303,8 +301,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isPaused(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).isPaused + promise.bool.resolveOnUiThreadWithPlayer(nativeId) { + isPaused } } @@ -315,8 +313,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isLive(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).isLive + promise.bool.resolveOnUiThreadWithPlayer(nativeId) { + isLive } } @@ -327,8 +325,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getAudioTrack(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).source?.selectedAudioTrack?.toJson() + promise.map.nullable.resolveOnUiThreadWithPlayer(nativeId) { + source?.selectedAudioTrack?.toJson() } } @@ -339,8 +337,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getAvailableAudioTracks(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).source?.availableAudioTracks?.mapToReactArray { it.toJson() } + promise.array.nullable.resolveOnUiThreadWithPlayer(nativeId) { + source?.availableAudioTracks?.mapToReactArray { it.toJson() } } } @@ -352,8 +350,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun setAudioTrack(nativeId: NativeId, trackIdentifier: String, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).source?.setAudioTrack(trackIdentifier) + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + source?.setAudioTrack(trackIdentifier) } } @@ -364,8 +362,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getSubtitleTrack(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).source?.selectedSubtitleTrack?.toJson() + promise.map.nullable.resolveOnUiThreadWithPlayer(nativeId) { + source?.selectedSubtitleTrack?.toJson() } } @@ -376,8 +374,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getAvailableSubtitles(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).source?.availableSubtitleTracks?.mapToReactArray { it.toJson() } + promise.array.nullable.resolveOnUiThreadWithPlayer(nativeId) { + source?.availableSubtitleTracks?.mapToReactArray { it.toJson() } } } @@ -389,8 +387,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun setSubtitleTrack(nativeId: NativeId, trackIdentifier: String?, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).source?.setSubtitleTrack(trackIdentifier) + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + source?.setSubtitleTrack(trackIdentifier) } } @@ -401,8 +399,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun scheduleAd(nativeId: NativeId, adItemJson: ReadableMap, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).scheduleAd(adItemJson.toAdItem() ?: throw IllegalArgumentException("invalid adItem")) + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + scheduleAd(adItemJson.toAdItem() ?: throw IllegalArgumentException("invalid adItem")) } } @@ -413,8 +411,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun skipAd(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).skipAd() + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + skipAd() } } @@ -424,8 +422,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isAd(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).isAd + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + isAd } } @@ -436,8 +434,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getTimeShift(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).timeShift + promise.double.resolveOnUiThreadWithPlayer(nativeId) { + timeShift } } @@ -448,8 +446,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getMaxTimeShift(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).maxTimeShift + promise.double.resolveOnUiThreadWithPlayer(nativeId) { + maxTimeShift } } @@ -460,8 +458,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun setMaxSelectableBitrate(nativeId: NativeId, maxSelectableBitrate: Int, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).setMaxSelectableVideoBitrate( + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + setMaxSelectableVideoBitrate( maxSelectableBitrate.takeUnless { it == -1 } ?: Integer.MAX_VALUE, ) } @@ -474,8 +472,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getThumbnail(nativeId: NativeId, time: Double, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).source?.getThumbnail(time)?.toJson() + promise.map.nullable.resolveOnUiThreadWithPlayer(nativeId) { + source?.getThumbnail(time)?.toJson() } } @@ -485,8 +483,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun castVideo(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).castVideo() + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + castVideo() } } @@ -495,8 +493,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun castStop(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).castStop() + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + castStop() } } @@ -506,8 +504,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isCastAvailable(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).isCastAvailable + promise.bool.resolveOnUiThreadWithPlayer(nativeId) { + isCastAvailable } } @@ -516,8 +514,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isCasting(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).isCasting + promise.bool.resolveOnUiThreadWithPlayer(nativeId) { + isCasting } } @@ -526,8 +524,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getVideoQuality(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).source?.selectedVideoQuality?.toJson() + promise.map.nullable.resolveOnUiThreadWithPlayer(nativeId) { + source?.selectedVideoQuality?.toJson() } } @@ -536,8 +534,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getAvailableVideoQualities(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).source?.availableVideoQualities?.mapToReactArray { it.toJson() } + promise.array.nullable.resolveOnUiThreadWithPlayer(nativeId) { + source?.availableVideoQualities?.mapToReactArray { it.toJson() } } } @@ -546,8 +544,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getPlaybackSpeed(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).playbackSpeed + promise.float.resolveOnUiThreadWithPlayer(nativeId) { + playbackSpeed } } @@ -556,10 +554,15 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun setPlaybackSpeed(nativeId: NativeId, playbackSpeed: Float, promise: Promise) { - promise.resolveOnUIThread { - getPlayer(nativeId).playbackSpeed = playbackSpeed + promise.unit.resolveOnUiThreadWithPlayer(nativeId) { + this.playbackSpeed = playbackSpeed } } + + private inline fun TPromise.resolveOnUiThreadWithPlayer( + nativeId: NativeId, + crossinline block: Player.() -> T, + ) = resolveOnUiThread { getPlayer(nativeId, this@PlayerModule).block() } } private inline fun List.mapToReactArray( 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 f69438df..dda547d2 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt @@ -6,6 +6,7 @@ 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.ReactMethod @@ -28,10 +29,9 @@ class SourceModule(context: ReactApplicationContext) : BitmovinBaseModule(contex override fun getName() = MODULE_NAME /** - * Fetches the [Source] instance associated with [nativeId] from internal sources or throws. + * Fetches the [Source] instance associated with [nativeId] from internal sources or null. */ - fun getSource(nativeId: NativeId): Source = sources[nativeId] - ?: throw IllegalArgumentException("No source matching provided id") + fun getSourceOrNull(nativeId: NativeId): Source? = sources[nativeId] /** * Creates a new `Source` instance inside the internal sources using the provided @@ -75,7 +75,7 @@ class SourceModule(context: ReactApplicationContext) : BitmovinBaseModule(contex config: ReadableMap?, analyticsSourceMetadata: ReadableMap?, promise: Promise, - ) = promise.resolveOnUIThread { + ) = promise.unit.resolveOnUiThread { val drmConfig = drmNativeId?.let { drmModule.getConfig(it) } val sourceConfig = config?.toSourceConfig() ?: throw InvalidParameterException("Invalid SourceConfig") val sourceMetadata = analyticsSourceMetadata?.toAnalyticsSourceMetadata() @@ -95,8 +95,10 @@ class SourceModule(context: ReactApplicationContext) : BitmovinBaseModule(contex * @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) + } } /** @@ -106,8 +108,8 @@ class SourceModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isAttachedToPlayer(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getSource(nativeId).isAttachedToPlayer + promise.bool.resolveOnUiThreadWithSource(nativeId) { + isAttachedToPlayer } } @@ -118,8 +120,8 @@ class SourceModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun isActive(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getSource(nativeId).isActive + promise.bool.resolveOnUiThreadWithSource(nativeId) { + isActive } } @@ -128,8 +130,8 @@ class SourceModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun duration(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getSource(nativeId).duration + promise.double.resolveOnUiThreadWithSource(nativeId) { + duration } } @@ -138,8 +140,8 @@ class SourceModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun loadingState(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getSource(nativeId).loadingState.ordinal + promise.int.resolveOnUiThreadWithSource(nativeId) { + loadingState.ordinal } } @@ -148,8 +150,8 @@ class SourceModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getMetadata(nativeId: NativeId, promise: Promise) { - promise.resolveOnUIThread { - getSource(nativeId).config.metadata + promise.map.nullable.resolveOnUiThreadWithSource(nativeId) { + config.metadata?.toReadableMap() } } @@ -158,8 +160,8 @@ class SourceModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun setMetadata(nativeId: NativeId, metadata: ReadableMap?, promise: Promise) { - promise.resolveOnUIThread { - getSource(nativeId).config.metadata = metadata?.toMap() + promise.unit.resolveOnUiThreadWithSource(nativeId) { + config.metadata = metadata?.toMap() } } @@ -170,8 +172,13 @@ class SourceModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getThumbnail(nativeId: NativeId, time: Double, promise: Promise) { - promise.resolveOnUIThread { - getSource(nativeId).getThumbnail(time)?.toJson() + promise.map.nullable.resolveOnUiThreadWithSource(nativeId) { + getThumbnail(time)?.toJson() } } + + 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/extensions/ReadableMap.kt b/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMap.kt index 9d8dc03e..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 @@ -11,9 +11,5 @@ private inline fun Map.toReadableMap( } } -fun Map.toReadableMap(): ReadableMap = toReadableMap(WritableMap::putBoolean) +@JvmName("toReadableStringMap") fun Map.toReadableMap(): ReadableMap = toReadableMap(WritableMap::putString) -fun Map.toReadableMap(): ReadableMap = toReadableMap(WritableMap::putDouble) -fun Map.toReadableMap(): ReadableMap = toReadableMap(WritableMap::putInt) -fun Map.toReadableMap(): ReadableMap = toReadableMap(WritableMap::putArray) -fun Map.toReadableMap(): ReadableMap = toReadableMap(WritableMap::putMap) From 4e6f164d40ee23db80c2fdb7b1a0e67faf0e34b7 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 29 Nov 2023 20:08:11 +0100 Subject: [PATCH 31/48] fix: code formating --- .../player/reactnative/BitmovinBaseModule.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index d67be85e..e0da9e0d 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -24,10 +24,12 @@ abstract class BitmovinBaseModule( protected val context: ReactApplicationContext, ) : ReactContextBaseJavaModule(context) { /** [resolve] the [TPromise] by running [block] in the UI thread with [UIManagerModule.addUIBlock]. */ - protected inline fun TPromise.resolveOnUiThread(crossinline block: RejectPromiseOnExceptionBlock.() -> R) { + protected inline fun TPromise.resolveOnUiThread( + crossinline block: RejectPromiseOnExceptionBlock.() -> R, + ) { val uiManager = runAndRejectOnException { uiManager } ?: return uiManager.addUIBlock { - resolveOnCurrentThread{ block() } + resolveOnCurrentThread { block() } } } @@ -48,12 +50,12 @@ abstract class BitmovinBaseModule( fun RejectPromiseOnExceptionBlock.getPlayer( nativeId: NativeId, - playerModule: PlayerModule = this.playerModule + playerModule: PlayerModule = this.playerModule, ): Player = playerModule.getPlayerOrNull(nativeId) ?: throw IllegalArgumentException("Invalid PlayerId") fun RejectPromiseOnExceptionBlock.getSource( nativeId: NativeId, - sourceModule: SourceModule = this.sourceModule + sourceModule: SourceModule = this.sourceModule, ): Source = sourceModule.getSourceOrNull(nativeId) ?: throw IllegalArgumentException("Invalid SourceId") } @@ -66,7 +68,9 @@ inline fun TPromise.runAndRejectOnException(block: RejectPromiseOnExce } /** Resolve the [Promise] with the value returned by [block]. If it throws, sets [Promise.reject]. */ -inline fun TPromise.resolveOnCurrentThread(crossinline block: RejectPromiseOnExceptionBlock.() -> T): Unit = try { +inline fun TPromise.resolveOnCurrentThread( + crossinline block: RejectPromiseOnExceptionBlock.() -> T, +): Unit = try { resolve(RejectPromiseOnExceptionBlock.block()) } catch (e: Exception) { reject(e) From cb07f50a959d4c96fcd9bf5bc614760d40d596d4 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 1 Dec 2023 13:48:14 +0100 Subject: [PATCH 32/48] refactor: prefer early return on failure --- .../reactnative/converter/JsonConverter.kt | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) 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 a0c5e025..d0feffc4 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 @@ -225,11 +225,8 @@ private fun String.toAdSourceType(): AdSourceType? = when (this) { * Converts an arbitrary `json` to `SourceConfig`. */ fun ReadableMap.toSourceConfig(): SourceConfig? { - val url = getString("url") - val type = getString("type")?.toSourceType() - if (url == null || type == null) { - return null - } + 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 } @@ -518,14 +515,9 @@ fun AudioTrack.toJson(): WritableMap = Arguments.createMap().apply { * Converts an arbitrary `json` into a `SubtitleTrack`. */ fun ReadableMap.toSubtitleTrack(): SubtitleTrack? { - val url = this.getString("url") - val label = this.getString("label") - if (url == null || label == null) { - return null - } return SubtitleTrack( - url = url, - label = label, + url = getString("url") ?: return null, + label = getString("label") ?: return null, id = getString("identifier") ?: UUID.randomUUID().toString(), isDefault = getBoolean("isDefault"), language = getString("language"), From 8773de7fd55d2b9cc5cb989d80ef831ad03ea60a Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 1 Dec 2023 14:34:24 +0100 Subject: [PATCH 33/48] fix: reject invalid command --- .../player/reactnative/RNPlayerViewManager.kt | 74 +++++++------------ 1 file changed, 27 insertions(+), 47 deletions(-) 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 d2cd34c2..46397dde 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt @@ -13,6 +13,7 @@ import com.bitmovin.player.reactnative.converter.toRNPlayerViewConfigWrapper import com.bitmovin.player.reactnative.converter.toRNStyleConfigWrapperFromPlayerConfig import com.bitmovin.player.reactnative.extensions.getBooleanOrNull import com.bitmovin.player.reactnative.extensions.getModule +import com.bitmovin.player.reactnative.extensions.playerModule import com.bitmovin.player.reactnative.ui.CustomMessageHandlerModule import com.bitmovin.player.reactnative.ui.FullscreenHandlerModule import com.bitmovin.player.reactnative.ui.RNPictureInPictureHandler @@ -21,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" @@ -155,38 +157,18 @@ 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()) } } @@ -216,9 +198,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 + runInMainLooperAndLogException { + val playerView = view.playerView ?: throw IllegalStateException("The player view is not yet created") + if (playerView.isPictureInPicture != isPictureInPictureRequested) return@runInMainLooperAndLogException if (isPictureInPictureRequested) { playerView.enterPictureInPicture() } else { @@ -251,13 +233,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 = playerId?.let { getPlayerModule()?.getPlayerOrNull(it) } - if (player == null) { - Log.e(MODULE_NAME, "Cannot create a PlayerView, invalid playerId was passed.") - return@post - } + private fun attachPlayer(view: RNPlayerView, playerId: NativeId, playerConfig: ReadableMap?) { + runInMainLooperAndLogException { + val player = playerId.let { context.playerModule?.getPlayerOrNull(it) } + ?: throw InvalidParameterException("Cannot create a PlayerView, invalid playerId was passed.") val playbackConfig = playerConfig?.getMap("playbackConfig") val isPictureInPictureEnabled = view.config?.pictureInPictureConfig?.isEnabled == true || playbackConfig?.getBooleanOrNull("isPictureInPictureEnabled") == true @@ -273,10 +252,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) @@ -306,10 +282,14 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } } - /** - * Helper function that gets the instantiated `PlayerModule` from modules registry. - */ - private fun getPlayerModule(): PlayerModule? = context.getModule() + private inline fun runInMainLooperAndLogException(crossinline block: () -> Unit) { + Handler(Looper.getMainLooper()).post { + try { + block() + } catch (e: Exception) { + Log.e(MODULE_NAME, "Error while command", e) + } + } + } } -private fun Int.toCommand(): RNPlayerViewManager.Commands? = RNPlayerViewManager.Commands.values().getOrNull(this) From c30b6fe68c947e6a62b79f6667fce7eb5651b7ef Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 1 Dec 2023 14:50:55 +0100 Subject: [PATCH 34/48] fix: formating --- .../com/bitmovin/player/reactnative/RNPlayerViewManager.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 46397dde..8af80bf6 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt @@ -165,7 +165,10 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple when (command) { 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_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()) @@ -292,4 +295,3 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } } } - From 6231fd9d4f46a9de51fe2f7d1610d231f06c15a7 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 1 Dec 2023 15:05:34 +0100 Subject: [PATCH 35/48] refactor: improve documentation --- .../player/reactnative/BitmovinBaseModule.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index e0da9e0d..668fe292 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -23,7 +23,10 @@ import com.facebook.react.uimanager.UIManagerModule abstract class BitmovinBaseModule( protected val context: ReactApplicationContext, ) : ReactContextBaseJavaModule(context) { - /** [resolve] the [TPromise] by running [block] in the UI thread with [UIManagerModule.addUIBlock]. */ + /** + * 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, ) { @@ -59,7 +62,7 @@ abstract class BitmovinBaseModule( ): Source = sourceModule.getSourceOrNull(nativeId) ?: throw IllegalArgumentException("Invalid SourceId") } -/** Run [block], forwarding the return value. If it throws, sets [Promise.reject] and return null. */ +/** 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) { @@ -67,7 +70,10 @@ inline fun TPromise.runAndRejectOnException(block: RejectPromiseOnExce null } -/** Resolve the [Promise] with the value returned by [block]. If it throws, sets [Promise.reject]. */ +/** + * [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 = try { From f32a92f1aa3462b5f2cda932c6a063dc522918d2 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 4 Dec 2023 10:50:31 +0100 Subject: [PATCH 36/48] fix: reject if analytics is disable --- .../reactnative/PlayerAnalyticsModule.kt | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) 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 c874e99b..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,5 +1,6 @@ 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.toAnalyticsCustomData import com.facebook.react.bridge.* @@ -17,25 +18,33 @@ class PlayerAnalyticsModule(context: ReactApplicationContext) : BitmovinBaseModu /** * 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, promise: Promise) { - promise.unit.resolveOnUiThread { - getPlayer(nativeId).analytics?.sendCustomDataEvent(json.toAnalyticsCustomData()) + 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) { - promise.string.nullable.resolveOnUiThread { - getPlayer(playerId).analytics?.userId + promise.string.resolveOnUiThreadWithAnalytics(playerId) { + userId } } + + private inline fun TPromise.resolveOnUiThreadWithAnalytics( + playerId: NativeId, + crossinline block: AnalyticsApi.() -> T, + ) = resolveOnUiThread { + val analytics = getPlayer(playerId).analytics ?: throw IllegalStateException("Analytics is disabled") + analytics.block() + } } From 4373ab6da165a3e144906099c8a38a97c9864fb4 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 4 Dec 2023 10:57:11 +0100 Subject: [PATCH 37/48] refactor: mark noop function as inline --- .../player/reactnative/BitmovinBaseModule.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index 668fe292..85e70c38 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -92,12 +92,13 @@ value class TPromise(val promise: Promise) { fun resolve(value: T): Unit = promise.resolve(value.takeUnless { it is Unit }) fun reject(throwable: Throwable) = promise.reject(throwable) } -val Promise.int get() = TPromise(this) -val Promise.unit get() = TPromise(this) -val Promise.string get() = TPromise(this) -val Promise.double get() = TPromise(this) -val Promise.float get() = TPromise(this) -val Promise.bool get() = TPromise(this) -val Promise.map get() = TPromise(this) -val Promise.array get() = TPromise(this) -val TPromise.nullable get() = TPromise(promise) + +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) From 08ab485fc0a942ec41a5633dfea13efc0b2f49f4 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 4 Dec 2023 11:29:38 +0100 Subject: [PATCH 38/48] fix: made all array non nullable (empty on null) --- .../com/bitmovin/player/reactnative/PlayerModule.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 c41aec97..c1a509f4 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -337,8 +337,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getAvailableAudioTracks(nativeId: NativeId, promise: Promise) { - promise.array.nullable.resolveOnUiThreadWithPlayer(nativeId) { - source?.availableAudioTracks?.mapToReactArray { it.toJson() } + promise.array.resolveOnUiThreadWithPlayer(nativeId) { + source?.availableAudioTracks?.mapToReactArray { it.toJson() } ?: Arguments.createArray() } } @@ -374,8 +374,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getAvailableSubtitles(nativeId: NativeId, promise: Promise) { - promise.array.nullable.resolveOnUiThreadWithPlayer(nativeId) { - source?.availableSubtitleTracks?.mapToReactArray { it.toJson() } + promise.array.resolveOnUiThreadWithPlayer(nativeId) { + source?.availableSubtitleTracks?.mapToReactArray { it.toJson() } ?: Arguments.createArray() } } @@ -534,8 +534,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun getAvailableVideoQualities(nativeId: NativeId, promise: Promise) { - promise.array.nullable.resolveOnUiThreadWithPlayer(nativeId) { - source?.availableVideoQualities?.mapToReactArray { it.toJson() } + promise.array.resolveOnUiThreadWithPlayer(nativeId) { + source?.availableVideoQualities?.mapToReactArray { it.toJson() } ?: Arguments.createArray() } } From 2c0d8877db300287a4c1217c26acba70f7f76779 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 4 Dec 2023 22:38:34 +0100 Subject: [PATCH 39/48] fix: serializing methods --- .../player/reactnative/PlayerModule.kt | 4 ---- .../reactnative/converter/JsonConverter.kt | 21 ++++++++++--------- .../reactnative/extensions/ReadableArray.kt | 6 +++++- .../extensions/ReadableMapExtension.kt | 12 ++++------- 4 files changed, 20 insertions(+), 23 deletions(-) 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 c1a509f4..19007a99 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -564,7 +564,3 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex crossinline block: Player.() -> T, ) = resolveOnUiThread { getPlayer(nativeId, this@PlayerModule).block() } } - -private inline fun List.mapToReactArray( - transform: (T) -> WritableMap, -): WritableArray = Arguments.fromList(map(transform)) 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 d0feffc4..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 @@ -53,14 +53,15 @@ 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.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.set import com.bitmovin.player.reactnative.extensions.toMap import com.bitmovin.player.reactnative.extensions.toMapList -import com.bitmovin.player.reactnative.extensions.toReadableArray import com.bitmovin.player.reactnative.extensions.toReadableMap import com.bitmovin.player.reactnative.extensions.withArray import com.bitmovin.player.reactnative.extensions.withBoolean @@ -119,7 +120,7 @@ private fun ReadableMap.toRemoteControlConfig(): RemoteControlConfig = RemoteCon * Converts an arbitrary `json` to `SourceOptions`. */ fun ReadableMap.toSourceOptions(): SourceOptions = SourceOptions( - startOffset = getDouble("startOffset"), + startOffset = getDoubleOrNull("startOffset"), startOffsetTimelineReference = getString("startOffsetTimelineReference")?.toTimelineReferencePoint(), ) @@ -519,9 +520,9 @@ fun ReadableMap.toSubtitleTrack(): SubtitleTrack? { url = getString("url") ?: return null, label = getString("label") ?: return null, id = getString("identifier") ?: UUID.randomUUID().toString(), - isDefault = getBoolean("isDefault"), + isDefault = getBooleanOrNull("isDefault") ?: false, language = getString("language"), - isForced = getBoolean("isForced"), + isForced = getBooleanOrNull("isForced") ?: false, mimeType = getString("format")?.takeIf { it.isNotEmpty() }?.toSubtitleMimeType(), ) } @@ -553,7 +554,7 @@ private fun String.textMimeTypeToJson(): String = split("/").last() * Converts any `AdBreak` object into its json representation. */ fun AdBreak.toJson(): WritableMap = Arguments.createMap().apply { - putArray("ads", ads.map { it.toJson() }.toReadableArray()) + putArray("ads", ads.mapToReactArray { it.toJson() }) putString("id", id) putDouble("scheduleTime", scheduleTime) } @@ -593,7 +594,7 @@ fun AdConfig.toJson(): WritableMap = Arguments.createMap().apply { */ fun AdItem.toJson(): WritableMap = Arguments.createMap().apply { putString("position", position) - putArray("sources", sources.map { it.toJson() }.toReadableArray()) + putArray("sources", sources.toList().mapToReactArray { it.toJson() }) } /** @@ -668,7 +669,7 @@ fun ReadableMap.toAnalyticsSourceMetadata(): SourceMetadata = SourceMetadata( videoId = getString("videoId"), cdnProvider = getString("cdnProvider"), path = getString("path"), - isLive = getBoolean("isLive"), + isLive = getBooleanOrNull("isLive"), customData = toAnalyticsCustomData(), ) @@ -705,8 +706,8 @@ fun OfflineOptionEntry.toJson(): WritableMap = Arguments.createMap().apply { * Converts any `OfflineContentOptions` into its json representation. */ fun OfflineContentOptions.toJson(): WritableMap = Arguments.createMap().apply { - putArray("audioOptions", audioOptions.map { it.toJson() }.toReadableArray()) - putArray("textOptions", textOptions.map { it.toJson() }.toReadableArray()) + putArray("audioOptions", audioOptions.mapToReactArray { it.toJson() }) + putArray("textOptions", textOptions.mapToReactArray { it.toJson() }) } fun Thumbnail.toJson(): WritableMap = Arguments.createMap().apply { @@ -721,7 +722,7 @@ fun Thumbnail.toJson(): WritableMap = Arguments.createMap().apply { } fun ReadableMap.toPictureInPictureConfig(): PictureInPictureConfig = PictureInPictureConfig( - isEnabled = getBoolean("isEnabled"), + isEnabled = getBooleanOrNull("isEnabled") ?: false, ) /** 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 885fc1df..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 @@ -13,4 +13,8 @@ fun ReadableArray.toIntList() = toList { it.asInt() } fun ReadableArray.toListOfArrays() = toList { it.asArray() } fun ReadableArray.toMapList() = toList { it.asMap() } -inline fun List.toReadableArray(): ReadableArray = Arguments.fromList(this) +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/ReadableMapExtension.kt b/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMapExtension.kt index 56acac40..1632c994 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 @@ -2,13 +2,9 @@ package com.bitmovin.player.reactnative.extensions import com.facebook.react.bridge.* -fun ReadableMap.getBooleanOrNull( - key: String, -): Boolean? = getValueOrNull(key, ReadableMap::getBoolean) - -fun ReadableMap.getIntOrNull( - key: String, -): Int? = getValueOrNull(key, ReadableMap::getInt) +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, @@ -50,7 +46,7 @@ inline fun ReadableMap.withStringArray( block: (List) -> T, ): T? = mapValue(key, ReadableMap::getStringArray, block) -fun ReadableMap.getStringArray(it: String) = getArray(it)?.toStringList() +fun ReadableMap.getStringArray(it: String) : List? = getArray(it)?.toStringList() inline fun ReadableMap.mapValue( key: String, From 779b1e3ce15bccd47da836424e7fc6d55a6f3a5d Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Mon, 4 Dec 2023 22:45:11 +0100 Subject: [PATCH 40/48] fix: don't skip player creation if config is missing --- .../player/reactnative/BitmovinBaseModule.kt | 18 ++++++++++-------- .../player/reactnative/PlayerModule.kt | 18 ++++++++++-------- .../extensions/ReadableMapExtension.kt | 2 +- src/components/PlayerView/index.tsx | 3 ++- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt index 85e70c38..a5157b73 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinBaseModule.kt @@ -1,5 +1,6 @@ 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 @@ -10,6 +11,8 @@ 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. * @@ -54,12 +57,12 @@ abstract class BitmovinBaseModule( fun RejectPromiseOnExceptionBlock.getPlayer( nativeId: NativeId, playerModule: PlayerModule = this.playerModule, - ): Player = playerModule.getPlayerOrNull(nativeId) ?: throw IllegalArgumentException("Invalid PlayerId") + ): 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") + ): 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. */ @@ -76,11 +79,7 @@ inline fun TPromise.runAndRejectOnException(block: RejectPromiseOnExce */ inline fun TPromise.resolveOnCurrentThread( crossinline block: RejectPromiseOnExceptionBlock.() -> T, -): Unit = try { - resolve(RejectPromiseOnExceptionBlock.block()) -} catch (e: Exception) { - reject(e) -} +): Unit = runAndRejectOnException { this@resolveOnCurrentThread.resolve(block()) } ?: Unit /** Receiver of code that can safely throw when resolving a [Promise]. */ object RejectPromiseOnExceptionBlock @@ -90,7 +89,10 @@ object RejectPromiseOnExceptionBlock 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) = promise.reject(throwable) + fun reject(throwable: Throwable) { + Log.e(MODULE_NAME, "Failed to execute Bitmovin method", throwable) + promise.reject(throwable) + } } inline val Promise.int get() = TPromise(this) 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 19007a99..78522f27 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -10,6 +10,7 @@ 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 java.security.InvalidParameterException @@ -39,13 +40,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun initWithConfig(nativeId: NativeId, config: ReadableMap?, promise: Promise) { - promise.unit.resolveOnUiThread { - if (!players.containsKey(nativeId)) { - config?.toPlayerConfig()?.let { - players[nativeId] = Player.create(context, it) - } - } - } + init(nativeId, config, analyticsConfigJson = null, promise) } /** @@ -55,6 +50,13 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun initWithAnalyticsConfig( + nativeId: NativeId, + playerConfigJson: ReadableMap?, + analyticsConfigJson: ReadableMap, + promise: Promise, + ) = init(nativeId, playerConfigJson, analyticsConfigJson, promise) + + private fun init( nativeId: NativeId, playerConfigJson: ReadableMap?, analyticsConfigJson: ReadableMap?, @@ -89,7 +91,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex @ReactMethod fun loadSource(nativeId: NativeId, sourceNativeId: String, promise: Promise) { promise.unit.resolveOnUiThread { - getPlayer(nativeId).load(getSource(sourceNativeId)) + getPlayer(nativeId, this@PlayerModule).load(getSource(sourceNativeId)) } } 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 1632c994..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 @@ -46,7 +46,7 @@ inline fun ReadableMap.withStringArray( block: (List) -> T, ): T? = mapValue(key, ReadableMap::getStringArray, block) -fun ReadableMap.getStringArray(it: String) : List? = getArray(it)?.toStringList() +fun ReadableMap.getStringArray(it: String): List? = getArray(it)?.toStringList() inline fun ReadableMap.mapValue( key: String, diff --git a/src/components/PlayerView/index.tsx b/src/components/PlayerView/index.tsx index 20df3edd..d46b2a1b 100644 --- a/src/components/PlayerView/index.tsx +++ b/src/components/PlayerView/index.tsx @@ -10,6 +10,7 @@ import { NativePlayerView } from './native'; import { useProxy } from '../../hooks/useProxy'; import { FullscreenHandlerBridge } from '../../ui/fullscreenhandlerbridge'; import { CustomMessageHandlerBridge } from '../../ui/custommessagehandlerbridge'; +import { ScalingMode } from '../../styleConfig'; import { PlayerViewProps } from './properties'; /** @@ -49,7 +50,7 @@ export function PlayerView({ fullscreenHandler, customMessageHandler, isFullscreenRequested = false, - scalingMode, + scalingMode = ScalingMode.Fit, isPictureInPictureRequested = false, ...props }: PlayerViewProps) { From d77c9556368820139cfa3bafe2c9127a288af648 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 6 Dec 2023 09:53:00 +0100 Subject: [PATCH 41/48] fix: don't send scaling mode if undefined --- src/components/PlayerView/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/PlayerView/index.tsx b/src/components/PlayerView/index.tsx index d46b2a1b..86155c25 100644 --- a/src/components/PlayerView/index.tsx +++ b/src/components/PlayerView/index.tsx @@ -10,7 +10,6 @@ import { NativePlayerView } from './native'; import { useProxy } from '../../hooks/useProxy'; import { FullscreenHandlerBridge } from '../../ui/fullscreenhandlerbridge'; import { CustomMessageHandlerBridge } from '../../ui/custommessagehandlerbridge'; -import { ScalingMode } from '../../styleConfig'; import { PlayerViewProps } from './properties'; /** @@ -50,7 +49,7 @@ export function PlayerView({ fullscreenHandler, customMessageHandler, isFullscreenRequested = false, - scalingMode = ScalingMode.Fit, + scalingMode, isPictureInPictureRequested = false, ...props }: PlayerViewProps) { @@ -133,7 +132,7 @@ export function PlayerView({ useEffect(() => { const node = findNodeHandle(nativeView.current); - if (node) { + if (node && scalingMode) { dispatch('setScalingMode', node, scalingMode); } }, [scalingMode, nativeView]); From 49945624a1fbf70083d290e001cd8ee836f40e50 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 6 Dec 2023 10:00:44 +0100 Subject: [PATCH 42/48] fix: rename&use runOnMainLooperAndLogException --- .../player/reactnative/RNPlayerViewManager.kt | 14 +++++++------- .../extensions/ReactContextExtension.kt | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) 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 8af80bf6..367a45f7 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt @@ -11,10 +11,10 @@ import com.bitmovin.player.api.ui.ScalingMode import com.bitmovin.player.api.ui.UiConfig 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.extensions.playerModule -import com.bitmovin.player.reactnative.ui.CustomMessageHandlerModule import com.bitmovin.player.reactnative.ui.FullscreenHandlerModule import com.bitmovin.player.reactnative.ui.RNPictureInPictureHandler import com.facebook.react.bridge.* @@ -201,9 +201,9 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } private fun setPictureInPicture(view: RNPlayerView, isPictureInPictureRequested: Boolean) { - runInMainLooperAndLogException { + runOnMainLooperAndLogException { val playerView = view.playerView ?: throw IllegalStateException("The player view is not yet created") - if (playerView.isPictureInPicture != isPictureInPictureRequested) return@runInMainLooperAndLogException + if (playerView.isPictureInPicture != isPictureInPictureRequested) return@runOnMainLooperAndLogException if (isPictureInPictureRequested) { playerView.enterPictureInPicture() } else { @@ -213,7 +213,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } private fun setScalingMode(view: RNPlayerView, scalingMode: String) { - Handler(Looper.getMainLooper()).post { + runOnMainLooperAndLogException { view.playerView?.scalingMode = ScalingMode.valueOf(scalingMode) } } @@ -225,7 +225,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple private fun attachCustomMessageHandlerBridge(view: RNPlayerView) { view.playerView?.setCustomMessageHandler( - context.getModule() + context.customMessageHandlerModule ?.getInstance(customMessageHandlerBridgeId) ?.customMessageHandler, ) @@ -237,7 +237,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple * @param playerId `Player` instance id inside `PlayerModule`'s registry. */ private fun attachPlayer(view: RNPlayerView, playerId: NativeId, playerConfig: ReadableMap?) { - runInMainLooperAndLogException { + runOnMainLooperAndLogException { val player = playerId.let { context.playerModule?.getPlayerOrNull(it) } ?: throw InvalidParameterException("Cannot create a PlayerView, invalid playerId was passed.") val playbackConfig = playerConfig?.getMap("playbackConfig") @@ -285,7 +285,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } } - private inline fun runInMainLooperAndLogException(crossinline block: () -> Unit) { + private inline fun runOnMainLooperAndLogException(crossinline block: () -> Unit) { Handler(Looper.getMainLooper()).post { try { block() 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 76e23203..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 @@ -4,6 +4,7 @@ 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 @@ -16,3 +17,4 @@ 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() From d36c14d701c4c6beb0ed6af1d12d883e718868ac Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 6 Dec 2023 10:06:33 +0100 Subject: [PATCH 43/48] fix: log invalid playerId --- .../java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 367a45f7..b178a030 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt @@ -239,7 +239,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple private fun attachPlayer(view: RNPlayerView, playerId: NativeId, playerConfig: ReadableMap?) { runOnMainLooperAndLogException { val player = playerId.let { context.playerModule?.getPlayerOrNull(it) } - ?: throw InvalidParameterException("Cannot create a PlayerView, invalid playerId was passed.") + ?: 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 From ff1148c842a51b853899828471020ea43ebb791b Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 6 Dec 2023 10:56:40 +0100 Subject: [PATCH 44/48] fix: error message --- .../java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b178a030..5c2df5f5 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt @@ -290,7 +290,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple try { block() } catch (e: Exception) { - Log.e(MODULE_NAME, "Error while command", e) + Log.e(MODULE_NAME, "Error while executing command", e) } } } From d27223627ce611989c64c880d124941e0799817b Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 6 Dec 2023 11:11:02 +0100 Subject: [PATCH 45/48] refactor: avoid creating a handler for each request --- .../player/reactnative/RNPlayerViewManager.kt | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) 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 5c2df5f5..d0d65d39 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt @@ -46,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 @@ -181,7 +182,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } private fun attachFullscreenBridge(view: RNPlayerView, fullscreenBridgeId: NativeId) { - Handler(Looper.getMainLooper()).post { + handler.postAndLogException { view.playerView?.setFullscreenHandler( context.getModule()?.getInstance(fullscreenBridgeId), ) @@ -189,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 { @@ -201,9 +202,9 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } private fun setPictureInPicture(view: RNPlayerView, isPictureInPictureRequested: Boolean) { - runOnMainLooperAndLogException { + handler.postAndLogException { val playerView = view.playerView ?: throw IllegalStateException("The player view is not yet created") - if (playerView.isPictureInPicture != isPictureInPictureRequested) return@runOnMainLooperAndLogException + if (playerView.isPictureInPicture != isPictureInPictureRequested) return@postAndLogException if (isPictureInPictureRequested) { playerView.enterPictureInPicture() } else { @@ -213,7 +214,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } private fun setScalingMode(view: RNPlayerView, scalingMode: String) { - runOnMainLooperAndLogException { + handler.postAndLogException { view.playerView?.scalingMode = ScalingMode.valueOf(scalingMode) } } @@ -237,7 +238,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple * @param playerId `Player` instance id inside `PlayerModule`'s registry. */ private fun attachPlayer(view: RNPlayerView, playerId: NativeId, playerConfig: ReadableMap?) { - runOnMainLooperAndLogException { + 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") @@ -285,13 +286,12 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple } } - private inline fun runOnMainLooperAndLogException(crossinline block: () -> Unit) { - Handler(Looper.getMainLooper()).post { - try { - block() - } catch (e: Exception) { - Log.e(MODULE_NAME, "Error while executing command", e) - } + /** 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) } } } From e035a363af287eb1af0ef4541e99cc8cc3c1b77b Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 6 Dec 2023 13:30:09 +0100 Subject: [PATCH 46/48] refactor: use single line function --- .../player/reactnative/PlayerModule.kt | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) 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 78522f27..d0740029 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -61,25 +61,23 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex 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(), - ) - } + ) = 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(), + ) } } From 653fcd2754e4a6b0a013e442d425d027bea88d5f Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 6 Dec 2023 13:48:43 +0100 Subject: [PATCH 47/48] refactor: invalid ref in kdoc --- .../main/java/com/bitmovin/player/reactnative/PlayerModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d0740029..5c5fa2bf 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -20,7 +20,7 @@ private const val MODULE_NAME = "PlayerModule" @ReactModule(name = MODULE_NAME) 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() From ae6f3da98adddfe0047c1831496520e185ee6442 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Thu, 7 Dec 2023 09:28:03 +0100 Subject: [PATCH 48/48] fix: inverted condition breaking PiP --- .../java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d0d65d39..e798524b 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt @@ -204,7 +204,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple private fun setPictureInPicture(view: RNPlayerView, isPictureInPictureRequested: Boolean) { handler.postAndLogException { val playerView = view.playerView ?: throw IllegalStateException("The player view is not yet created") - if (playerView.isPictureInPicture != isPictureInPictureRequested) return@postAndLogException + if (playerView.isPictureInPicture == isPictureInPictureRequested) return@postAndLogException if (isPictureInPictureRequested) { playerView.enterPictureInPicture() } else {