mirror of
https://github.com/zoriya/react-native-video.git
synced 2026-08-16 02:56:22 +00:00
chore(android): refactor & clean codebase
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -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
|
||||
}
|
||||
}
|
||||
+28
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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()
|
||||
}
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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)
|
||||
+2
-2
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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
-1
@@ -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;"
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user