react-native-video-trim 6.2.0 → 6.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- package com.videotrim.enums;
1
+ package com.videotrim.enums
2
2
 
3
- public enum ErrorCode {
3
+ enum class ErrorCode {
4
4
  TRIMMING_FAILED,
5
5
  FAIL_TO_GET_VIDEO_INFO,
6
6
  FAIL_TO_INITIALIZE_AUDIO_PLAYER,
@@ -0,0 +1,5 @@
1
+ package com.videotrim.interfaces
2
+
3
+ interface IVideoTrimmerView {
4
+ fun onDestroy()
5
+ }
@@ -0,0 +1,16 @@
1
+ package com.videotrim.interfaces
2
+
3
+ import com.facebook.react.bridge.WritableMap
4
+ import com.videotrim.enums.ErrorCode
5
+
6
+ interface VideoTrimListener {
7
+ fun onLoad(duration: Int)
8
+ fun onTrimmingProgress(percentage: Int)
9
+ fun onFinishTrim(url: String, startMs: Long, endMs: Long, videoDuration: Int)
10
+ fun onCancelTrim()
11
+ fun onError(errorMessage: String?, errorCode: ErrorCode)
12
+ fun onCancel()
13
+ fun onSave()
14
+ fun onLog(log: WritableMap)
15
+ fun onStatistics(statistics: WritableMap)
16
+ }
@@ -0,0 +1,84 @@
1
+ package com.videotrim.utils
2
+
3
+ import android.media.MediaMetadataRetriever
4
+ import android.net.Uri
5
+ import android.util.Log
6
+ import java.io.IOException
7
+
8
+ object MediaMetadataUtil {
9
+
10
+ private const val TAG = "MediaMetadataUtil"
11
+
12
+ fun getMediaMetadataRetriever(source: String): MediaMetadataRetriever? {
13
+ val retriever = MediaMetadataRetriever()
14
+ return try {
15
+ if (source.startsWith("http://") || source.startsWith("https://")) {
16
+ retriever.setDataSource(source, HashMap())
17
+ } else {
18
+ var filePath = source
19
+
20
+ if (!StorageUtil.isFileExists(filePath)) {
21
+ Log.e(TAG, "File does not exist, trying to parse as URI: $source")
22
+
23
+ val uri = Uri.parse(source)
24
+ filePath = uri.path ?: ""
25
+
26
+ if (!StorageUtil.isFileExists(filePath)) {
27
+ Log.e(TAG, "File does not exist at path: $filePath")
28
+ return null
29
+ }
30
+ }
31
+
32
+ retriever.setDataSource(filePath)
33
+ }
34
+ retriever
35
+ } catch (e: Exception) {
36
+ Log.e(TAG, "Error setting data source", e)
37
+ try {
38
+ retriever.release()
39
+ } catch (ee: Exception) {
40
+ Log.e(TAG, "Error releasing retriever", ee)
41
+ }
42
+ null
43
+ }
44
+ }
45
+
46
+ fun checkFileValidity(urlString: String, callback: FileValidityCallback) {
47
+ Thread {
48
+ val retriever = getMediaMetadataRetriever(urlString)
49
+ if (retriever == null) {
50
+ callback.onResult(false, "unknown", -1L)
51
+ return@Thread
52
+ }
53
+
54
+ val durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
55
+ val duration = durationStr?.toLongOrNull() ?: -1L
56
+
57
+ var isValid = false
58
+ var fileType = "unknown"
59
+
60
+ val hasVideo = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
61
+ if (hasVideo == "yes") {
62
+ fileType = "video"
63
+ isValid = true
64
+ } else {
65
+ val hasAudio = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
66
+ if (hasAudio == "yes") {
67
+ fileType = "audio"
68
+ isValid = true
69
+ }
70
+ }
71
+
72
+ try {
73
+ retriever.release()
74
+ } catch (e: IOException) {
75
+ Log.e(TAG, "Error releasing retriever", e)
76
+ }
77
+ callback.onResult(isValid, fileType, if (isValid) duration else -1L)
78
+ }.start()
79
+ }
80
+
81
+ fun interface FileValidityCallback {
82
+ fun onResult(isValid: Boolean, fileType: String, duration: Long)
83
+ }
84
+ }
@@ -0,0 +1,129 @@
1
+ package com.videotrim.utils
2
+
3
+ import android.content.ContentValues
4
+ import android.content.Context
5
+ import android.media.MediaScannerConnection
6
+ import android.net.Uri
7
+ import android.os.Build
8
+ import android.os.Environment
9
+ import android.provider.MediaStore
10
+ import android.text.TextUtils
11
+ import com.facebook.react.bridge.ReactApplicationContext
12
+ import java.io.File
13
+ import java.io.FileInputStream
14
+ import java.io.FileOutputStream
15
+ import java.io.IOException
16
+ import java.io.InputStream
17
+ import java.io.OutputStream
18
+
19
+ object StorageUtil {
20
+
21
+ fun getOutputPath(context: Context, outputExt: String): String {
22
+ val timestamp = System.currentTimeMillis() / 1000
23
+ val file = File(context.filesDir, "${VideoTrimmerUtil.FILE_PREFIX}_${timestamp}.$outputExt")
24
+ return file.absolutePath
25
+ }
26
+
27
+ fun isFileExists(filePath: String?): Boolean {
28
+ if (TextUtils.isEmpty(filePath)) return false
29
+ return File(filePath!!).exists()
30
+ }
31
+
32
+ fun listFiles(context: Context): Array<String> {
33
+ val filesDir = context.filesDir
34
+ val files = filesDir.listFiles { _, name -> name.startsWith(VideoTrimmerUtil.FILE_PREFIX) }
35
+
36
+ return files?.map { it.absolutePath }?.toTypedArray() ?: emptyArray()
37
+ }
38
+
39
+ fun deleteFile(path: String?): Boolean {
40
+ if (TextUtils.isEmpty(path)) return true
41
+ return deleteFile(File(path!!))
42
+ }
43
+
44
+ fun deleteFile(file: File?): Boolean {
45
+ if (file == null || !file.exists()) return true
46
+
47
+ if (file.isFile) return file.delete()
48
+
49
+ if (!file.isDirectory) return false
50
+
51
+ file.listFiles()?.forEach { f ->
52
+ if (f.isFile) {
53
+ f.delete()
54
+ } else if (f.isDirectory) {
55
+ deleteFile(f)
56
+ }
57
+ }
58
+ return file.delete()
59
+ }
60
+
61
+ @Throws(IOException::class)
62
+ fun saveVideoToGallery(context: ReactApplicationContext, videoFilePath: String?) {
63
+ val videoFile = File(videoFilePath!!)
64
+
65
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
66
+ saveVideoUsingMediaStore(context, videoFile)
67
+ } else {
68
+ try {
69
+ saveVideoUsingTraditionalStorage(context, videoFile)
70
+ } catch (e: IOException) {
71
+ throw RuntimeException(e)
72
+ }
73
+ }
74
+ }
75
+
76
+ private fun saveVideoUsingMediaStore(context: Context, videoFile: File) {
77
+ val values = ContentValues().apply {
78
+ put(MediaStore.Video.Media.TITLE, "My Video Title")
79
+ put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
80
+ put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
81
+ }
82
+ val uri: Uri? = context.contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values)
83
+
84
+ if (uri != null) {
85
+ try {
86
+ val outputStream = context.contentResolver.openOutputStream(uri)
87
+ copyFile(videoFile, outputStream!!)
88
+ MediaScannerConnection.scanFile(context, arrayOf(uri.toString()), arrayOf("video/*"), null)
89
+ } catch (e: IOException) {
90
+ e.printStackTrace()
91
+ }
92
+ }
93
+ }
94
+
95
+ @Throws(IOException::class)
96
+ private fun saveVideoUsingTraditionalStorage(context: Context, videoFile: File) {
97
+ val galleryDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
98
+ val destinationFile = File(galleryDirectory, videoFile.name)
99
+ copyFile(videoFile, destinationFile)
100
+ MediaScannerConnection.scanFile(context, arrayOf(destinationFile.absolutePath), arrayOf("video/*"), null)
101
+ }
102
+
103
+ @Throws(IOException::class)
104
+ private fun copyFile(sourceFile: File, outputStream: OutputStream) {
105
+ val inputStream: InputStream = FileInputStream(sourceFile)
106
+ copyFile(inputStream, outputStream)
107
+ }
108
+
109
+ @Throws(IOException::class)
110
+ private fun copyFile(sourceFile: File, destFile: File) {
111
+ val inputStream: InputStream = FileInputStream(sourceFile)
112
+ val outputStream: OutputStream = FileOutputStream(destFile)
113
+ copyFile(inputStream, outputStream)
114
+ }
115
+
116
+ @Throws(IOException::class)
117
+ private fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
118
+ try {
119
+ val buffer = ByteArray(1024)
120
+ var length: Int
121
+ while (inputStream.read(buffer).also { length = it } > 0) {
122
+ outputStream.write(buffer, 0, length)
123
+ }
124
+ } finally {
125
+ inputStream.close()
126
+ outputStream.close()
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,163 @@
1
+ package com.videotrim.utils
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.graphics.Bitmap
5
+ import android.media.MediaMetadataRetriever
6
+ import android.util.Log
7
+ import com.arthenica.ffmpegkit.FFmpegKit
8
+ import com.arthenica.ffmpegkit.FFmpegSession
9
+ import com.arthenica.ffmpegkit.ReturnCode
10
+ import com.facebook.react.bridge.Arguments
11
+ import com.videotrim.enums.ErrorCode
12
+ import com.videotrim.interfaces.VideoTrimListener
13
+ import iknow.android.utils.DeviceUtil
14
+ import iknow.android.utils.UnitConverter
15
+ import iknow.android.utils.callback.SingleCallback
16
+ import iknow.android.utils.thread.BackgroundExecutor
17
+ import java.text.SimpleDateFormat
18
+ import java.util.Date
19
+ import java.util.TimeZone
20
+
21
+ object VideoTrimmerUtil {
22
+
23
+ private val TAG: String = VideoTrimmerUtil::class.java.simpleName
24
+ const val FILE_PREFIX = "trimmedVideo"
25
+ const val MIN_SHOOT_DURATION = 1000L
26
+ const val VIDEO_MAX_TIME = 10
27
+ const val MAX_SHOOT_DURATION = VIDEO_MAX_TIME * 1000L
28
+ @JvmField var MAX_COUNT_RANGE = 10
29
+ @JvmField var SCREEN_WIDTH_FULL = DeviceUtil.getDeviceWidth()
30
+ @JvmField val RECYCLER_VIEW_PADDING = UnitConverter.dpToPx(35)
31
+ const val DEFAULT_AUDIO_EXTENSION = ".wav"
32
+ @JvmField var VIDEO_FRAMES_WIDTH = SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2
33
+ @JvmField var mThumbWidth = 0
34
+ @JvmField val THUMB_HEIGHT = UnitConverter.dpToPx(50)
35
+ @JvmField val THUMB_WIDTH = UnitConverter.dpToPx(25)
36
+ private const val THUMB_RESOLUTION_RES = 2
37
+
38
+ fun trim(
39
+ inputFile: String,
40
+ outputFile: String,
41
+ videoDuration: Int,
42
+ startMs: Long,
43
+ endMs: Long,
44
+ enableRotation: Boolean,
45
+ rotationAngle: Double,
46
+ callback: VideoTrimListener
47
+ ): FFmpegSession {
48
+ val currentDate = Date()
49
+ @SuppressLint("SimpleDateFormat")
50
+ val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
51
+ dateFormat.timeZone = TimeZone.getTimeZone("UTC")
52
+ val formattedDateTime = dateFormat.format(currentDate)
53
+
54
+ val cmds = mutableListOf<String>()
55
+ cmds.add("-ss")
56
+ cmds.add("${startMs}ms")
57
+ cmds.add("-to")
58
+ cmds.add("${endMs}ms")
59
+
60
+ if (enableRotation) {
61
+ cmds.add("-display_rotation")
62
+ cmds.add(rotationAngle.toString())
63
+ }
64
+
65
+ cmds.add("-i")
66
+ cmds.add(inputFile)
67
+ cmds.add("-c")
68
+ cmds.add("copy")
69
+ cmds.add("-metadata")
70
+ cmds.add("creation_time=$formattedDateTime")
71
+ cmds.add(outputFile)
72
+
73
+ val command = cmds.toTypedArray()
74
+ val cmdStr = "Command: ${command.joinToString(" ")}"
75
+
76
+ Log.d(TAG, cmdStr)
77
+
78
+ val m = Arguments.createMap()
79
+ m.putString("message", cmdStr)
80
+ callback.onLog(m)
81
+
82
+ return FFmpegKit.executeWithArgumentsAsync(command, { session ->
83
+ val state = session.state
84
+ val returnCode = session.returnCode
85
+ when {
86
+ ReturnCode.isSuccess(session.returnCode) -> {
87
+ callback.onFinishTrim(outputFile, startMs, endMs, videoDuration)
88
+ }
89
+ ReturnCode.isCancel(session.returnCode) -> {
90
+ callback.onCancelTrim()
91
+ }
92
+ else -> {
93
+ val errorMessage = "Command failed with state $state and rc $returnCode.${session.failStackTrace}"
94
+ callback.onError(errorMessage, ErrorCode.TRIMMING_FAILED)
95
+ }
96
+ }
97
+ }, { log ->
98
+ Log.d(TAG, "FFmpeg process started with log ${log.message}")
99
+
100
+ val map = Arguments.createMap()
101
+ map.putInt("level", log.level.value)
102
+ map.putString("message", log.message)
103
+ map.putDouble("sessionId", log.sessionId.toDouble())
104
+ map.putString("logStr", log.toString())
105
+ callback.onLog(map)
106
+ }, { statistics ->
107
+ val timeInMilliseconds = statistics.time.toInt()
108
+ if (timeInMilliseconds > 0) {
109
+ val completePercentage = (timeInMilliseconds * 100) / videoDuration
110
+ callback.onTrimmingProgress(completePercentage.coerceIn(0, 100))
111
+ }
112
+
113
+ val map = Arguments.createMap()
114
+ map.putDouble("sessionId", statistics.sessionId.toDouble())
115
+ map.putInt("videoFrameNumber", statistics.videoFrameNumber)
116
+ map.putDouble("videoFps", statistics.videoFps.toDouble())
117
+ map.putDouble("videoQuality", statistics.videoQuality.toDouble())
118
+ map.putDouble("size", statistics.size.toDouble())
119
+ map.putDouble("time", statistics.time)
120
+ map.putDouble("bitrate", statistics.bitrate.toDouble())
121
+ map.putDouble("speed", statistics.speed.toDouble())
122
+ map.putString("statisticsStr", statistics.toString())
123
+ callback.onStatistics(map)
124
+ })
125
+ }
126
+
127
+ fun shootVideoThumbInBackground(
128
+ mediaMetadataRetriever: MediaMetadataRetriever,
129
+ totalThumbsCount: Int,
130
+ startPosition: Long,
131
+ endPosition: Long,
132
+ callback: SingleCallback<Bitmap, Int>
133
+ ) {
134
+ BackgroundExecutor.execute(object : BackgroundExecutor.Task("", 0L, "") {
135
+ override fun execute() {
136
+ try {
137
+ val interval = (endPosition - startPosition) / (totalThumbsCount - 1)
138
+ for (i in 0 until totalThumbsCount) {
139
+ val frameTime = startPosition + interval * i
140
+
141
+ val bitmap: Bitmap? = try {
142
+ mediaMetadataRetriever.getFrameAtTime(frameTime * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
143
+ } catch (t: Throwable) {
144
+ t.printStackTrace()
145
+ break
146
+ }
147
+
148
+ if (bitmap == null) continue
149
+ val scaledBitmap = try {
150
+ Bitmap.createScaledBitmap(bitmap, mThumbWidth * THUMB_RESOLUTION_RES, THUMB_HEIGHT * THUMB_RESOLUTION_RES, false)
151
+ } catch (t: Throwable) {
152
+ t.printStackTrace()
153
+ bitmap
154
+ }
155
+ callback.onSingleCallback(scaledBitmap, interval.toInt())
156
+ }
157
+ } catch (e: Throwable) {
158
+ Thread.getDefaultUncaughtExceptionHandler()?.uncaughtException(Thread.currentThread(), e)
159
+ }
160
+ }
161
+ })
162
+ }
163
+ }