Skip to content

Commit

Permalink
Merge pull request #317 from bitmovin/PRN-74/refactor--improve-null-s…
Browse files Browse the repository at this point in the history
…afety

## Problem (PRN-74)
The codebase had many issues:
 - invalid null parameters were ignored instead of reporting an error
 - invalid state (module not loaded) were ignored instead of reporting an error
 - many functions were taking unnecessary null parameters, returning null unnecessarily
 - errors were not reported
 - some promise were not resolve (deadlock if awaited)
 - exception do not reject promisses

## Changes
Simplify the code and improve error visibility by:
- Errors are bubbled up to the caller with `@ReactNative`'s `promise.reject` instead of ignoring them or logging them.
- Refactor json serialization to use null operator `?.` rather than operating on null values.
- Make `toJson` extension function to easily chain `?.` operations
- Make `Json.toXXX` extension function to easily chain `?.` operations
- Add `XXXModule` variant that throws an exception if the module are not loaded, which should never happen 
- Wrap all `@ReactNative` methods in a catch to report any exception instead of crashing the app
- Introduce a base Bitmovin module for helper functions
- Wrap promise in a strongly typed interface
- All `@ReactCommand` promisses are guarantied to be resolved or rejected
- Exception in the PlayerViewManager are logged instead of crashing the app

## Checklist
- [x] 🗒 `CHANGELOG` entry if applicable
  • Loading branch information
krocard authored Dec 7, 2023
2 parents ff59feb + ae6f3da commit fef1113
Show file tree
Hide file tree
Showing 20 changed files with 1,362 additions and 1,871 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.bitmovin.player.reactnative

import android.util.Log
import com.bitmovin.player.api.Player
import com.bitmovin.player.api.source.Source
import com.bitmovin.player.reactnative.extensions.drmModule
import com.bitmovin.player.reactnative.extensions.offlineModule
import com.bitmovin.player.reactnative.extensions.playerModule
import com.bitmovin.player.reactnative.extensions.sourceModule
import com.bitmovin.player.reactnative.extensions.uiManagerModule
import com.facebook.react.bridge.*
import com.facebook.react.uimanager.UIManagerModule

private const val MODULE_NAME = "BitmovinBaseModule"

/**
* Base for Bitmovin React modules.
*
* Provides many helper methods that are promise exception safe.
*
* In general, code should not throw while resolving a [Promise]. Instead, [Promise.reject] should be used.
* This doesn't match Kotlin's error style, which uses exception. The helper methods in this class, provide such
* convenience, they can only be called in a context that will catch any Exception and reject the [Promise].
*
*/
abstract class BitmovinBaseModule(
protected val context: ReactApplicationContext,
) : ReactContextBaseJavaModule(context) {
/**
* Runs [block] on the UI thread with [UIManagerModule.addUIBlock] and [TPromise.resolve] [this] with
* its return value. If [block] throws, [Promise.reject] [this] with the [Throwable].
*/
protected inline fun <T, R : T> TPromise<T>.resolveOnUiThread(
crossinline block: RejectPromiseOnExceptionBlock.() -> R,
) {
val uiManager = runAndRejectOnException { uiManager } ?: return
uiManager.addUIBlock {
resolveOnCurrentThread { block() }
}
}

protected val RejectPromiseOnExceptionBlock.playerModule: PlayerModule get() = context.playerModule
?: throw IllegalArgumentException("PlayerModule not found")

protected val RejectPromiseOnExceptionBlock.uiManager: UIManagerModule get() = context.uiManagerModule
?: throw IllegalStateException("UIManager not found")

protected val RejectPromiseOnExceptionBlock.sourceModule: SourceModule get() = context.sourceModule
?: throw IllegalStateException("SourceModule not found")

protected val RejectPromiseOnExceptionBlock.offlineModule: OfflineModule get() = context.offlineModule
?: throw IllegalStateException("OfflineModule not found")

protected val RejectPromiseOnExceptionBlock.drmModule: DrmModule get() = context.drmModule
?: throw IllegalStateException("DrmModule not found")

fun RejectPromiseOnExceptionBlock.getPlayer(
nativeId: NativeId,
playerModule: PlayerModule = this.playerModule,
): Player = playerModule.getPlayerOrNull(nativeId) ?: throw IllegalArgumentException("Invalid PlayerId $nativeId")

fun RejectPromiseOnExceptionBlock.getSource(
nativeId: NativeId,
sourceModule: SourceModule = this.sourceModule,
): Source = sourceModule.getSourceOrNull(nativeId) ?: throw IllegalArgumentException("Invalid SourceId $nativeId")
}

/** Run [block], returning it's return value. If [block] throws, [Promise.reject] [this] and return null. */
inline fun <T, R> TPromise<T>.runAndRejectOnException(block: RejectPromiseOnExceptionBlock.() -> R): R? = try {
RejectPromiseOnExceptionBlock.block()
} catch (e: Exception) {
reject(e)
null
}

/**
* [TPromise.resolve] [this] with [block] return value.
* If [block] throws, [Promise.reject] [this] with the [Throwable].
*/
inline fun <T> TPromise<T>.resolveOnCurrentThread(
crossinline block: RejectPromiseOnExceptionBlock.() -> T,
): Unit = runAndRejectOnException { this@resolveOnCurrentThread.resolve(block()) } ?: Unit

/** Receiver of code that can safely throw when resolving a [Promise]. */
object RejectPromiseOnExceptionBlock

/** Compile time wrapper for Promises to type check the resolved type [T]. */
@JvmInline
value class TPromise<T>(val promise: Promise) {
// Promise only support built-in types. Functions that return [Unit] must resolve to `null`.
fun resolve(value: T): Unit = promise.resolve(value.takeUnless { it is Unit })
fun reject(throwable: Throwable) {
Log.e(MODULE_NAME, "Failed to execute Bitmovin method", throwable)
promise.reject(throwable)
}
}

inline val Promise.int get() = TPromise<Int>(this)
inline val Promise.unit get() = TPromise<Unit>(this)
inline val Promise.string get() = TPromise<String>(this)
inline val Promise.double get() = TPromise<Double>(this)
inline val Promise.float get() = TPromise<Float>(this)
inline val Promise.bool get() = TPromise<Boolean>(this)
inline val Promise.map get() = TPromise<ReadableMap>(this)
inline val Promise.array get() = TPromise<ReadableArray>(this)
inline val <T> TPromise<T>.nullable get() = TPromise<T?>(promise)
Original file line number Diff line number Diff line change
@@ -1,70 +1,54 @@
package com.bitmovin.player.reactnative

import com.bitmovin.player.casting.BitmovinCastManager
import com.bitmovin.player.reactnative.converter.JsonConverter
import com.bitmovin.player.reactnative.converter.toCastOptions
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.UIManagerModule

private const val MODULE_NAME = "BitmovinCastManagerModule"

@ReactModule(name = MODULE_NAME)
class BitmovinCastManagerModule(
private val context: ReactApplicationContext,
) : ReactContextBaseJavaModule(context) {
class BitmovinCastManagerModule(context: ReactApplicationContext) : BitmovinBaseModule(context) {
override fun getName() = MODULE_NAME

/**
* Returns whether the [BitmovinCastManager] is initialized.
*/
@ReactMethod
fun isInitialized(promise: Promise) = uiManager?.addUIBlock {
promise.resolve(BitmovinCastManager.isInitialized())
fun isInitialized(promise: Promise) = promise.unit.resolveOnUiThread {
BitmovinCastManager.isInitialized()
}

/**
* Initializes the [BitmovinCastManager] with the given options.
*/
@ReactMethod
fun initializeCastManager(options: ReadableMap?, promise: Promise) {
val castOptions = JsonConverter.toCastOptions(options)
uiManager?.addUIBlock {
BitmovinCastManager.initialize(
castOptions?.applicationId,
castOptions?.messageNamespace,
)
promise.resolve(null)
}
fun initializeCastManager(options: ReadableMap?, promise: Promise) = promise.unit.resolveOnUiThread {
val castOptions = options?.toCastOptions()
BitmovinCastManager.initialize(
castOptions?.applicationId,
castOptions?.messageNamespace,
)
}

/**
* Sends a message to the receiver.
*/
@ReactMethod
fun sendMessage(message: String, messageNamespace: String?, promise: Promise) {
uiManager?.addUIBlock {
BitmovinCastManager.getInstance().sendMessage(message, messageNamespace)
promise.resolve(null)
}
fun sendMessage(message: String, messageNamespace: String?, promise: Promise) = promise.unit.resolveOnUiThread {
BitmovinCastManager.getInstance().sendMessage(message, messageNamespace)
}

/**
* Updates the context of the [BitmovinCastManager] to the current activity.
*/
@ReactMethod
fun updateContext(promise: Promise) {
uiManager?.addUIBlock {
BitmovinCastManager.getInstance().updateContext(currentActivity)
promise.resolve(null)
}
fun updateContext(promise: Promise) = promise.unit.resolveOnUiThread {
BitmovinCastManager.getInstance().updateContext(currentActivity)
}

private val uiManager: UIManagerModule?
get() = context.getNativeModule(UIManagerModule::class.java)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,50 @@ package com.bitmovin.player.reactnative

import com.bitmovin.player.api.buffer.BufferLevel
import com.bitmovin.player.api.media.MediaType
import com.bitmovin.player.reactnative.converter.JsonConverter
import com.bitmovin.player.reactnative.converter.toBufferType
import com.bitmovin.player.reactnative.converter.toJson
import com.facebook.react.bridge.*
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.UIManagerModule

private const val MODULE_NAME = "BufferModule"
private const val INVALID_BUFFER_TYPE = "Invalid buffer type"

@ReactModule(name = MODULE_NAME)
class BufferModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) {
class BufferModule(context: ReactApplicationContext) : BitmovinBaseModule(context) {
override fun getName() = MODULE_NAME

/**
* Gets the [BufferLevel] from the Player
* @param nativeId Target player id.
* @param type The [type of buffer][JsonConverter.toBufferType] to return the level for.
* @param type The [type of buffer][toBufferType] to return the level for.
* @param promise JS promise object.
*/
@ReactMethod
fun getLevel(nativeId: NativeId, type: String, promise: Promise) {
uiManager()?.addUIBlock { _ ->
val player = playerModule()?.getPlayer(nativeId) ?: return@addUIBlock
val bufferType = JsonConverter.toBufferType(type)
if (bufferType == null) {
promise.reject("Error: ", "Invalid buffer type")
return@addUIBlock
}
val bufferLevels = RNBufferLevels(
player.buffer.getLevel(bufferType, MediaType.Audio),
player.buffer.getLevel(bufferType, MediaType.Video),
)
JsonConverter.fromRNBufferLevels(bufferLevels).let {
promise.resolve(it)
}
promise.map.resolveOnUiThread {
val player = getPlayer(nativeId)
val bufferType = type.toBufferTypeOrThrow()
RNBufferLevels(
audio = player.buffer.getLevel(bufferType, MediaType.Audio),
video = player.buffer.getLevel(bufferType, MediaType.Video),
).toJson()
}
}

/**
* Sets the target buffer level for the chosen buffer type across all media types.
* @param nativeId Target player id.
* @param type The [type of buffer][JsonConverter.toBufferType] to set the target level for.
* @param type The [type of buffer][toBufferType] to set the target level for.
* @param value The value to set.
*/
@ReactMethod
fun setTargetLevel(nativeId: NativeId, type: String, value: Double) {
uiManager()?.addUIBlock { _ ->
val player = playerModule()?.getPlayer(nativeId) ?: return@addUIBlock
val bufferType = JsonConverter.toBufferType(type) ?: return@addUIBlock
player.buffer.setTargetLevel(bufferType, value)
fun setTargetLevel(nativeId: NativeId, type: String, value: Double, promise: Promise) {
promise.unit.resolveOnUiThread {
getPlayer(nativeId).buffer.setTargetLevel(type.toBufferTypeOrThrow(), value)
}
}

/**
* Helper function that gets the instantiated `UIManagerModule` from modules registry.
*/
private fun uiManager(): UIManagerModule? = context.getNativeModule(UIManagerModule::class.java)

/**
* Helper function that gets the instantiated `PlayerModule` from modules registry.
*/
private fun playerModule(): PlayerModule? = context.getNativeModule(PlayerModule::class.java)
private fun String.toBufferTypeOrThrow() = toBufferType() ?: throw IllegalArgumentException(INVALID_BUFFER_TYPE)
}

/**
Expand Down
Loading

0 comments on commit fef1113

Please sign in to comment.