From 373b2360a965542ffd8572c6d14d74a9d69cde6e Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 8 Nov 2023 17:18:17 +0100 Subject: [PATCH 01/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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 9c8644721d45f25f89266bddde0d99dccc6398ee Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Tue, 14 Nov 2023 14:23:34 +0100 Subject: [PATCH 16/56] 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 17/56] 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 18/56] 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 19/56] 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 20/56] 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 21/56] 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 22/56] 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 23/56] 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 24/56] 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 25/56] 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 26/56] 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 27/56] 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 76ab8ac41054a92353d5a4d08c866cb818a91948 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Wed, 29 Nov 2023 16:42:31 +0100 Subject: [PATCH 28/56] fix(silentiosplayback): fix silent iOS playback when device is muted --- example/src/App.tsx | 18 ++++++++++++++++-- example/src/screens/BasicPictureInPicture.tsx | 14 -------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 18e1a31d..227f1ad9 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,9 +1,9 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Platform, Button } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { SourceType } from 'bitmovin-player-react-native'; +import { AudioSession, SourceType } from 'bitmovin-player-react-native'; import ExamplesList from './screens/ExamplesList'; import BasicAds from './screens/BasicAds'; import BasicAnalytics from './screens/BasicAnalytics'; @@ -63,6 +63,20 @@ const RootStack = createNativeStackNavigator(); const isTVOS = Platform.OS === 'ios' && Platform.isTV; export default function App() { + useEffect(() => { + // iOS audio session category must be set to `playback` first, otherwise playback + // will have no audio when the device is silenced. + // This is also required to make Picture in Picture work on iOS. + // + // Usually it's desireable to set the audio's category only once during your app's main component + // initialization. This way you can guarantee that your app's audio category is properly + // configured throughout the whole lifecycle of the application. + AudioSession.setCategory('playback').catch((error) => { + // Handle any native errors that might occur while setting the audio's category. + console.log("Failed to set app's audio category to `playback`:\n", error); + }); + }); + const stackParams = { data: [ { diff --git a/example/src/screens/BasicPictureInPicture.tsx b/example/src/screens/BasicPictureInPicture.tsx index d1fa44f0..98971d03 100644 --- a/example/src/screens/BasicPictureInPicture.tsx +++ b/example/src/screens/BasicPictureInPicture.tsx @@ -6,7 +6,6 @@ import { usePlayer, PlayerView, SourceType, - AudioSession, PictureInPictureEnterEvent, PictureInPictureExitEvent, PlayerViewConfig, @@ -51,19 +50,6 @@ export default function BasicPictureInPicture({ useFocusEffect( useCallback(() => { - // iOS audio session must be set to `playback` first otherwise PiP mode won't work. - // - // Usually it's desireable to set the audio's category only once during your app's main component - // initialization. This way you can guarantee that your app's audio category is properly - // configured throughout the whole lifecycle of the application. - AudioSession.setCategory('playback').catch((error) => { - // Handle any native errors that might occur while setting the audio's category. - console.log( - "[BasicPictureInPicture] Failed to set app's audio category to `playback`:\n", - error - ); - }); - // Load desired source configuration player.load({ url: From d733d09cd4c37bbcd3f1df164a89391efecd15cb Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Wed, 29 Nov 2023 20:07:12 +0100 Subject: [PATCH 29/56] 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 30/56] 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 7ab087bc2d8cb7989644fa3f9bf5b3ad1147d532 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Thu, 30 Nov 2023 14:07:43 +0100 Subject: [PATCH 31/56] fix(loadingstate): loadingState was missing in source events --- CHANGELOG.md | 1 + ios/Event+JSON.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab4c10c3..3e77293e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - iOS: `onEvent` on iOS has incomplete payload information - tvOS: Picture in Picture sample screen has unwanted padding - iOS: hide home indicator when entering fullscreen mode in the example application +- iOS: invalid `loadingState` value in `SeekEvent`, `SourceLoadEvent`, `SourceLoadedEvent` and in `SourceUnloadedEvent` ## [0.14.1] (2023-11-16) diff --git a/ios/Event+JSON.swift b/ios/Event+JSON.swift index 0b6c817c..12b06ef5 100644 --- a/ios/Event+JSON.swift +++ b/ios/Event+JSON.swift @@ -5,7 +5,7 @@ extension Source { var json: [AnyHashable: Any] = [ "duration": duration, "isActive": isActive, - "loadingState": loadingState, + "loadingState": loadingState.rawValue, "isAttachedToPlayer": isAttachedToPlayer ] if let metadata { From fe9c38b3a50d0fda3b2aac1b4d00bf123daad68f Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Thu, 30 Nov 2023 15:37:24 +0100 Subject: [PATCH 32/56] fix(loadingstate): add missing loadingState to EventSource --- src/events.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/events.ts b/src/events.ts index c467ebf7..3f357808 100644 --- a/src/events.ts +++ b/src/events.ts @@ -9,6 +9,7 @@ import { import { SubtitleTrack } from './subtitleTrack'; import { VideoQuality } from './media'; import { AudioTrack } from './audioTrack'; +import { LoadingState } from './source'; /** * Base event type for all events. @@ -139,6 +140,10 @@ export interface EventSource { * Metadata for this event's source. */ metadata?: Record; + /** + * The current `LoadingState` of the source. + */ + loadingState: LoadingState; } /** From bbcab5dc15cd96855b0d5001bbbce57e1a32fd3d Mon Sep 17 00:00:00 2001 From: Update Bot Date: Fri, 1 Dec 2023 12:11:38 +0000 Subject: [PATCH 33/56] chore(ios): update ios player version to 3.50.0 --- RNBitmovinPlayer.podspec | 2 +- example/ios/Podfile.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/RNBitmovinPlayer.podspec b/RNBitmovinPlayer.podspec index 320e8253..07672831 100644 --- a/RNBitmovinPlayer.podspec +++ b/RNBitmovinPlayer.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,swift}" s.dependency "React-Core" - s.dependency "BitmovinPlayer", "3.49.0" + s.dependency "BitmovinPlayer", "3.50.0" s.ios.dependency "GoogleAds-IMA-iOS-SDK", "3.18.4" s.tvos.dependency "GoogleAds-IMA-tvOS-SDK", "4.8.2" end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 22405c3f..701f2902 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -3,10 +3,10 @@ PODS: - BitmovinAnalyticsCollector/Core - BitmovinPlayerCore (~> 3.48) - BitmovinAnalyticsCollector/Core (3.3.0) - - BitmovinPlayer (3.49.0): + - BitmovinPlayer (3.50.0): - BitmovinAnalyticsCollector/BitmovinPlayer (~> 3.0) - - BitmovinPlayerCore (= 3.49.0) - - BitmovinPlayerCore (3.49.0) + - BitmovinPlayerCore (= 3.50.0) + - BitmovinPlayerCore (3.50.0) - boost (1.76.0) - CocoaAsyncSocket (7.6.5) - DoubleConversion (1.1.6) @@ -346,7 +346,7 @@ PODS: - React-logger (= 0.69.6-2) - React-perflogger (= 0.69.6-2) - RNBitmovinPlayer (0.14.2): - - BitmovinPlayer (= 3.49.0) + - BitmovinPlayer (= 3.50.0) - GoogleAds-IMA-iOS-SDK (= 3.18.4) - GoogleAds-IMA-tvOS-SDK (= 4.8.2) - React-Core @@ -524,8 +524,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: BitmovinAnalyticsCollector: b3bf447e609a87d41e51a7c029cb16a3bcb3404c - BitmovinPlayer: 920ee8a7df47dc09a038a44a36b24b0c31427ad8 - BitmovinPlayerCore: 0040c6c171079ae1a1342baab182c7956deaf6d8 + BitmovinPlayer: 582f36a5bb2826b4d7c9b83ea664737979074dff + BitmovinPlayerCore: 2625310bf6e1adb659babfb6fba6a0c89a71b003 boost: a7c83b31436843459a1961bfd74b96033dc77234 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 234abba95e31cc2aada0cf3b97cdb11bc5b90575 @@ -574,7 +574,7 @@ SPEC CHECKSUMS: React-RCTText: f72442f7436fd8624494963af4906000a5465ce6 React-runtimeexecutor: f1383f6460ea3d66ed122b4defb0b5ba664ee441 ReactCommon: 7857ab475239c5ba044b7ed946ba564f2e7f1626 - RNBitmovinPlayer: 020d9bcad225ddee065212cbba660673e456f391 + RNBitmovinPlayer: 8b890e2cab8c0c17df25fd80050781137f69e612 RNCPicker: 0250e95ad170569a96f5b0555cdd5e65b9084dca RNScreens: 4a1af06327774490d97342c00aee0c2bafb497b7 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 @@ -583,4 +583,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 253d54be44299ecb3c274fb9bf8415c9c99cd6c6 -COCOAPODS: 1.12.1 +COCOAPODS: 1.14.2 From cb07f50a959d4c96fcd9bf5bc614760d40d596d4 Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 1 Dec 2023 13:48:14 +0100 Subject: [PATCH 34/56] 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 cfed18c8ae4d098d62a19a3a2e5b84f04ebb710c Mon Sep 17 00:00:00 2001 From: Vehovec Date: Fri, 1 Dec 2023 14:28:47 +0100 Subject: [PATCH 35/56] chore: add changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab4c10c3..2df8a54a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [unreleased] + +### Changed + +- React Native version to `0.72.6` + ## [0.14.2] (2023-11-27) ### Fixed From 8773de7fd55d2b9cc5cb989d80ef831ad03ea60a Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Fri, 1 Dec 2023 14:34:24 +0100 Subject: [PATCH 36/56] 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 37/56] 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 38/56] 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 39/56] 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 40/56] 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 41/56] 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 42/56] 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 43/56] 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 44/56] 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 45/56] 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 46/56] 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 47/56] 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 48/56] 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 49/56] 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 50/56] 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 51/56] 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 { From e137689109a035bcb4b948ddc272ced6c79d1a3f Mon Sep 17 00:00:00 2001 From: Kevin Rocard Date: Thu, 7 Dec 2023 13:53:36 +0100 Subject: [PATCH 52/56] Treat TS lint warnings as error There is currently no warning and we want to keep it that way. The warning currently have to be spotted by reviewers, leading to longer merge time. untracked --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3cf6baa6..4b07d266 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "scripts": { "test": "jest", "typescript": "tsc --noEmit", - "lint": "eslint \"**/*.{ts,tsx}\"", + "lint": "eslint --max-warnings=0 -- \"**/*.{ts,tsx}\"", "format": "prettier --write .", "build": "tsup ./src/index.ts --dts --target es2020 --format cjs,esm -d lib", "example": "yarn --cwd example", From f3b191b50141bb5eaca0b3d68d64f1156526a960 Mon Sep 17 00:00:00 2001 From: Update Bot Date: Mon, 11 Dec 2023 13:00:20 +0000 Subject: [PATCH 53/56] chore(android): update android player version to 3.53.0+jason --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 7cca20f2..49207a51 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -59,7 +59,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.31.0' implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1' - implementation 'com.bitmovin.player:player:3.51.0+jason' + implementation 'com.bitmovin.player:player:3.53.0+jason' //noinspection GradleDynamicVersion implementation 'com.facebook.react:react-native:+' // From node_modules } From 25c9347a05a2def7aeea5621b9334b23a4145823 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Mon, 11 Dec 2023 16:37:37 +0100 Subject: [PATCH 54/56] chore: increae min RN version to 0.65 --- CHANGELOG.md | 1 + README.md | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48c22469..b821e31e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - React Native version to `0.72.6` +- React Native peer dependency version to `0.65.0+` ## [0.14.2] (2023-11-27) diff --git a/README.md b/README.md index eb5a7bab..88da175c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This is an open-source project created to enable customers to integrate the Bitm ## Platform Support -This library requires at least React Native 0.64+ and React 17+ to work properly. The **officially supported** platforms are: +This library requires at least React Native 0.65+ and React 17+ to work properly. The **officially supported** platforms are: - **iOS/iPadOS/tvOS:** 14.0+ - **Android:** 5.0+ diff --git a/package.json b/package.json index 4c2ac361..e7ce7b97 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ }, "peerDependencies": { "react": ">=17", - "react-native": ">=0.64" + "react-native": ">=0.65" }, "commitlint": { "extends": [ From 118b30cb87027233d2cf26771d9e14fa6fd74d8d Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Wed, 13 Dec 2023 08:57:18 +0100 Subject: [PATCH 55/56] feat(playertesting): improve test structure based on feedback --- integration_test/tests/playbackTest.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/integration_test/tests/playbackTest.ts b/integration_test/tests/playbackTest.ts index a2e8307d..214f6df0 100644 --- a/integration_test/tests/playbackTest.ts +++ b/integration_test/tests/playbackTest.ts @@ -13,19 +13,18 @@ import { import { Sources } from './helper/Sources'; export default (spec: TestScope) => { - spec.describe('calling player API when a source is loaded', () => { - spec.it( - 'emits a Play and Playing events after calling play API', - async () => { - await startPlayerTest({}, async () => { - await loadSourceConfig(Sources.artOfMotionHls); - await callPlayerAndExpectEvents((player) => { - player.play(); - }, EventSequence(EventType.Play, EventType.Playing)); - }); - } - ); - spec.it('emits a Paused event after calling pause API', async () => { + spec.describe('calling play when a source is loaded', () => { + spec.it('emits a Play and Playing events', async () => { + await startPlayerTest({}, async () => { + await loadSourceConfig(Sources.artOfMotionHls); + await callPlayerAndExpectEvents((player) => { + player.play(); + }, EventSequence(EventType.Play, EventType.Playing)); + }); + }); + }); + spec.describe('calling pause when a source is loaded', () => { + spec.it('emits a Paused event', async () => { await startPlayerTest({}, async () => { await loadSourceConfig(Sources.artOfMotionHls); await playFor(1); From d3643a6cb66ce9d7cc93a75994c910f20e8cf2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20K=C3=A1konyi?= Date: Wed, 13 Dec 2023 10:46:52 +0100 Subject: [PATCH 56/56] chore: Improve code style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Žiga Vehovec --- integration_test/playertesting/PlayerTestWorld.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_test/playertesting/PlayerTestWorld.ts b/integration_test/playertesting/PlayerTestWorld.ts index 13f208c3..f46a1c62 100644 --- a/integration_test/playertesting/PlayerTestWorld.ts +++ b/integration_test/playertesting/PlayerTestWorld.ts @@ -278,7 +278,7 @@ export default class PlayerTestWorld { }; private ensurePlayerInitialized = async (): Promise => { - if (this.isPlayerInitialized === false) { + if (!this.isPlayerInitialized) { // Trick to make sure the player is initialized, // otherwise method calls might have no effect. await new Promise((resolve) => setTimeout(resolve, 150));