chore(android): refactor & clean codebase

This commit is contained in:
Krzysztof Moch
2025-01-23 21:38:26 +01:00
parent 677b8545e1
commit 815d677127
16 changed files with 211 additions and 177 deletions
@@ -0,0 +1,41 @@
package com.video.core.utils
import android.os.Handler
import android.os.Looper
import com.margelo.nitro.NitroModules
import java.util.concurrent.Callable
import java.util.concurrent.FutureTask
object Threading {
@JvmStatic
fun runOnMainThread(action: () -> Unit) {
// We are already on the main thread, run and return
if (Looper.myLooper() == Looper.getMainLooper()) {
action()
return
}
// If application context is null, throw an error
if (NitroModules.applicationContext == null) {
throw IllegalStateException("Application context is null")
}
// Post the action to the main thread
Handler(NitroModules.applicationContext!!.mainLooper).post {
action()
}
}
@JvmStatic
fun <T> runOnMainThreadSync(action: Callable<T>): T {
return if (Looper.myLooper() == Looper.getMainLooper()) {
// Already on the main thread, run and return the result
action.call()
} else {
// Post the action to the main thread and wait for the result
val futureTask = FutureTask(action)
Handler(Looper.getMainLooper()).post(futureTask)
futureTask.get()
}
}
}
@@ -0,0 +1,50 @@
package com.video.core.utils
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.webkit.URLUtil
import com.margelo.nitro.NitroModules
import java.io.File
import java.net.URL
import java.net.URLConnection
object VideoFileHelper {
private fun hasReadPermission(): Boolean {
return NitroModules.applicationContext?.checkSelfPermission(
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
}
fun validateReadPermission(uri: String) {
if (!hasReadPermission()) throw Error("App does not have permission to read file at path: $uri")
val file = File(Uri.parse(uri).path ?: return)
// Check if file exists and is readable
if (!file.exists()) throw Error("File does not exist at path: $uri")
// Check if file is readable
if (!file.canRead()) throw Error("File is not readable at path: $uri")
}
fun getFileSizeFromUri(uri: String): Long {
return try {
when {
URLUtil.isFileUrl(uri) -> {
validateReadPermission(uri)
val file = File(Uri.parse(uri).path ?: return -1)
if (file.exists()) file.length() else -1
}
URLUtil.isNetworkUrl(uri) -> {
val connection: URLConnection = URL(uri).openConnection()
connection.connect()
connection.contentLength.toLong()
}
else -> -1
}
} catch (e: Exception) {
-1
}
}
}
@@ -0,0 +1,66 @@
package com.video.core.utils
import android.media.MediaFormat
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.webkit.URLUtil
import com.margelo.nitro.video.VideoInformation
object VideoInformationUtils {
fun fromUri(uri: String, headers: Map<String, String> = emptyMap()): VideoInformation {
val retriever = MediaMetadataRetriever()
when {
URLUtil.isFileUrl(uri) -> {
retriever.setDataSource(Uri.parse(uri).path)
}
else -> {
retriever.setDataSource(uri, headers)
}
}
// Get dimensions
val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toDoubleOrNull() ?: Double.NaN
val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toDoubleOrNull() ?: Double.NaN
// Get duration in milliseconds, convert to long
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() ?: -1L
// If we have some valid info, but there is no duration it might be live
val isLive = !width.isNaN() && !height.isNaN() && duration <= 0
// Get bitrate
val bitrate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)?.toDoubleOrNull() ?: Double.NaN
// Get rotation
val rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull() ?: 0
// Check for HDR by looking at color transfer (API 30+)
val isHDR = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val colorTransfer = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER)?.toIntOrNull()
colorTransfer == MediaFormat.COLOR_TRANSFER_ST2084 || colorTransfer == MediaFormat.COLOR_TRANSFER_HLG
} else {
false
}
// Clean up
retriever.release()
// Get file size
val fileSize = VideoFileHelper.getFileSizeFromUri(uri)
val videoInfo = VideoInformation(
bitrate = bitrate,
width = width,
height = height,
duration = duration,
fileSize = fileSize,
isHDR = isHDR,
isLive = isLive,
orientation = VideoOrientationUtils.fromWHR(width.toInt(), height.toInt(), rotation)
)
return videoInfo
}
}
@@ -0,0 +1,28 @@
package com.video.core.utils
import com.margelo.nitro.video.VideoOrientation
object VideoOrientationUtils {
fun fromWHR(width: Int?, height: Int?, rotation: Int?): VideoOrientation {
if (width == 0 || height == 0 || height == null || width == null) return VideoOrientation.UNKNOWN
// Check if video is portrait or landscape using natural size
val isNaturalSizePortrait = height > width
// If rotation is not available, use natural size to determine orientation
if (rotation == null) {
return if (isNaturalSizePortrait) VideoOrientation.PORTRAIT else VideoOrientation.LANDSCAPE_RIGHT
}
// Normalize rotation to 0-360 range
val normalizedRotation = ((rotation % 360) + 360) % 360
return when (normalizedRotation) {
0 -> if (isNaturalSizePortrait) VideoOrientation.PORTRAIT else VideoOrientation.LANDSCAPE_RIGHT
90 -> VideoOrientation.PORTRAIT
180 -> if (isNaturalSizePortrait) VideoOrientation.PORTRAIT_UPSIDE_DOWN else VideoOrientation.LANDSCAPE_LEFT
270 -> VideoOrientation.PORTRAIT_UPSIDE_DOWN
else -> if (isNaturalSizePortrait) VideoOrientation.PORTRAIT else VideoOrientation.LANDSCAPE_RIGHT
}
}
}
@@ -10,12 +10,12 @@ import androidx.media3.exoplayer.upstream.DefaultAllocator
import com.facebook.proguard.annotations.DoNotStrip
import com.margelo.nitro.NitroModules
import com.margelo.nitro.core.Promise
import com.video.utils.Threading.Companion.runOnMainThreadSync
import com.video.utils.Threading.Companion.runOnMainThread
import com.video.core.utils.Threading.runOnMainThread
import com.video.core.utils.Threading.runOnMainThreadSync
@UnstableApi
@DoNotStrip
class HybridVideoPlayer() : HybridVideoPlayerSpec(), AutoCloseable {
class HybridVideoPlayer() : HybridVideoPlayerSpec() {
override lateinit var source: HybridVideoPlayerSourceSpec
private var allocator: DefaultAllocator? = null
@@ -129,13 +129,13 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec(), AutoCloseable {
}
}
fun release() {
private fun release() {
runOnMainThread {
playerPointer.release()
}
}
override fun close() {
override fun clean() {
release()
}
@@ -3,7 +3,7 @@ package com.margelo.nitro.video
import androidx.media3.common.MediaItem
import com.facebook.proguard.annotations.DoNotStrip
import com.margelo.nitro.core.Promise
import com.video.utils.AssetUtils
import com.video.core.utils.VideoInformationUtils
@DoNotStrip
class HybridVideoPlayerSource(): HybridVideoPlayerSourceSpec() {
@@ -18,7 +18,7 @@ class HybridVideoPlayerSource(): HybridVideoPlayerSourceSpec() {
override fun getAssetInformationAsync(): Promise<VideoInformation> {
return Promise.async {
return@async AssetUtils.getAssetInformation(uri)
return@async VideoInformationUtils.fromUri(uri)
}
}
@@ -3,8 +3,8 @@ package com.margelo.nitro.video
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import com.facebook.proguard.annotations.DoNotStrip
import com.video.VideoView
import com.video.utils.Threading
import com.video.view.VideoView
import com.video.core.utils.Threading
@DoNotStrip
@OptIn(UnstableApi::class)
@@ -1,4 +1,4 @@
package com.video
package com.video.react
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
@@ -20,7 +20,7 @@ class VideoPackage : ReactPackage {
companion object {
init {
System.loadLibrary("ReactNativeVideo")
System.loadLibrary("ReactNativeVideo")
}
}
}
@@ -1,4 +1,4 @@
package com.video
package com.video.react
import androidx.media3.common.util.UnstableApi
import com.facebook.react.bridge.Arguments
@@ -13,6 +13,7 @@ import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.uimanager.events.Event
import com.facebook.react.viewmanagers.VideoViewManagerDelegate
import com.facebook.react.viewmanagers.VideoViewManagerInterface
import com.video.view.VideoView
internal class NitroIdChange(
surfaceId: Int,
@@ -1,115 +0,0 @@
package com.video.utils
import android.media.MediaFormat
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.webkit.URLUtil
import com.margelo.nitro.video.VideoInformation
import com.margelo.nitro.video.VideoOrientation
import java.io.File
import java.net.URL
import java.net.URLConnection
class AssetUtils {
companion object {
fun getAssetInformation(uri: String): VideoInformation {
val retriever = MediaMetadataRetriever()
when {
URLUtil.isFileUrl(uri) -> {
retriever.setDataSource(Uri.parse(uri).path)
}
else -> {
//TODO: pass headers here
retriever.setDataSource(uri, HashMap<String, String>())
}
}
// Get dimensions
val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toDoubleOrNull() ?: Double.NaN
val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toDoubleOrNull() ?: Double.NaN
// Get duration in milliseconds, convert to long
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() ?: -1L
// If we have some valid info, but there is no duration it might be live
val isLive = !width.isNaN() && !height.isNaN() && duration <= 0
// Get bitrate
val bitrate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)?.toDoubleOrNull() ?: Double.NaN
// Get rotation
val rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull() ?: 0
// Check for HDR by looking at color transfer (API 30+)
val isHDR = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val colorTransfer = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER)?.toIntOrNull()
colorTransfer == MediaFormat.COLOR_TRANSFER_ST2084 || colorTransfer == MediaFormat.COLOR_TRANSFER_HLG
} else {
false
}
// Clean up
retriever.release()
// Get file size
val fileSize = getFileSizeFromUri(uri)
val videoInfo = VideoInformation(
bitrate = bitrate,
width = width,
height = height,
duration = duration,
fileSize = fileSize,
isHDR = isHDR,
isLive = isLive,
orientation = getVideoOrientation(width.toInt(), height.toInt(), rotation)
)
return videoInfo
}
fun getFileSizeFromUri(uri: String): Long {
return try {
when {
URLUtil.isFileUrl(uri) -> {
val file = File(Uri.parse(uri).path ?: return -1)
if (file.exists()) file.length() else -1
}
URLUtil.isNetworkUrl(uri) -> {
val connection: URLConnection = URL(uri).openConnection()
connection.connect()
connection.contentLength.toLong()
}
else -> -1
}
} catch (e: Exception) {
-1
}
}
fun getVideoOrientation(width: Int?, height: Int?, rotation: Int?): VideoOrientation {
if (width == 0 || height == 0 || height == null || width == null) return VideoOrientation.UNKNOWN
// Check if video is portrait or landscape using natural size
val isNaturalSizePortrait = height > width
// If rotation is not available, use natural size to determine orientation
if (rotation == null) {
return if (isNaturalSizePortrait) VideoOrientation.PORTRAIT else VideoOrientation.LANDSCAPE_RIGHT
}
// Normalize rotation to 0-360 range
val normalizedRotation = ((rotation % 360) + 360) % 360
return when (normalizedRotation) {
0 -> if (isNaturalSizePortrait) VideoOrientation.PORTRAIT else VideoOrientation.LANDSCAPE_RIGHT
90 -> VideoOrientation.PORTRAIT
180 -> if (isNaturalSizePortrait) VideoOrientation.PORTRAIT_UPSIDE_DOWN else VideoOrientation.LANDSCAPE_LEFT
270 -> VideoOrientation.PORTRAIT_UPSIDE_DOWN
else -> if (isNaturalSizePortrait) VideoOrientation.PORTRAIT else VideoOrientation.LANDSCAPE_RIGHT
}
}
}
}
@@ -1,39 +0,0 @@
package com.video.utils
import android.os.Handler
import android.os.Looper
import com.margelo.nitro.NitroModules
import java.util.concurrent.Callable
import java.util.concurrent.FutureTask
class Threading {
companion object {
@JvmStatic
fun runOnMainThread(action: () -> Unit) {
if (NitroModules.applicationContext == null) {
throw IllegalStateException("Application context is null")
}
if (Looper.myLooper() == Looper.getMainLooper()) {
action()
}
Handler(NitroModules.applicationContext!!.mainLooper).post {
action()
}
}
@JvmStatic
fun <T> runOnMainThreadSync(action: Callable<T>): T {
return if (Looper.myLooper() == Looper.getMainLooper()) {
// Already on the main thread, run and return the result
action.call()
} else {
// Post the action to the main thread and wait for the result
val futureTask = FutureTask(action)
Handler(Looper.getMainLooper()).post(futureTask)
futureTask.get() // This will block until the result is returned
}
}
}
}
@@ -1,4 +1,4 @@
package com.video
package com.video.view
import android.content.Context
import android.util.AttributeSet
@@ -1,16 +1,18 @@
// https://github.com/react-native-community/cli/blob/main/docs/dependencies.md
module.exports = {
dependency: {
platforms: {
/**
* @type {import('@react-native-community/cli-types').IOSDependencyParams}
*/
ios: {},
/**
* @type {import('@react-native-community/cli-types').AndroidDependencyParams}
*/
android: {},
dependency: {
platforms: {
/**
* @type {import('@react-native-community/cli-types').IOSDependencyParams}
*/
ios: {},
/**
* @type {import('@react-native-community/cli-types').AndroidDependencyParams}
*/
android: {
packageImportPath: "import com.video.react.VideoPackage;"
},
},
},
};