react-native-compressor 1.8.0 → 1.8.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.
Files changed (47) hide show
  1. package/README.md +22 -1
  2. package/android/build.gradle +6 -2
  3. package/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt +2 -2
  4. package/android/src/main/java/com/reactnativecompressor/CompressorModule.kt +12 -0
  5. package/android/src/main/java/com/reactnativecompressor/Utils/Uploader.kt +11 -11
  6. package/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt +42 -36
  7. package/android/src/main/java/com/reactnativecompressor/Utils/createVideoThumbnail.kt +158 -0
  8. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/CompressionInterface.kt +73 -0
  9. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/VideoCompressorClass.kt +148 -0
  10. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt +562 -0
  11. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/Atoms.kt +55 -0
  12. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt +196 -0
  13. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/NumbersUtils.kt +47 -0
  14. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/StreamableVideo.kt +209 -0
  15. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/InputSurface.kt +128 -0
  16. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/MP4Builder.kt +388 -0
  17. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Mdat.kt +74 -0
  18. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Mp4Movie.kt +54 -0
  19. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/OutputSurface.kt +102 -0
  20. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Result.kt +9 -0
  21. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Sample.kt +3 -0
  22. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/TextureRenderer.kt +214 -0
  23. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Track.kt +286 -0
  24. package/android/src/oldarch/CompressorSpec.kt +2 -0
  25. package/ios/Compressor.mm +9 -0
  26. package/ios/CompressorManager.swift +11 -0
  27. package/ios/Utils/CreateVideoThumbnail.swift +111 -0
  28. package/lib/commonjs/Spec/NativeCompressor.js.map +1 -1
  29. package/lib/commonjs/index.js +14 -0
  30. package/lib/commonjs/index.js.map +1 -1
  31. package/lib/commonjs/utils/index.js +12 -1
  32. package/lib/commonjs/utils/index.js.map +1 -1
  33. package/lib/module/Spec/NativeCompressor.js.map +1 -1
  34. package/lib/module/index.js +4 -2
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/module/utils/index.js +7 -0
  37. package/lib/module/utils/index.js.map +1 -1
  38. package/lib/typescript/Spec/NativeCompressor.d.ts +8 -0
  39. package/lib/typescript/Spec/NativeCompressor.d.ts.map +1 -1
  40. package/lib/typescript/index.d.ts +15 -3
  41. package/lib/typescript/index.d.ts.map +1 -1
  42. package/lib/typescript/utils/index.d.ts +16 -1
  43. package/lib/typescript/utils/index.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/Spec/NativeCompressor.ts +11 -0
  46. package/src/index.tsx +6 -0
  47. package/src/utils/index.tsx +28 -1
package/README.md CHANGED
@@ -68,6 +68,7 @@ We should use **react-native-compressor** instead of **FFmpeg** because **react-
68
68
  - [Audio](#audio)
69
69
  - [Background Upload](#background-upload)
70
70
  - [Download File](#download)
71
+ - [Create Video Thumbnail and Clear Cache](#create-video-thumbnail-and-clear-cache)
71
72
 
72
73
  * [Other Utilities](#api)
73
74
  - [Background Upload](#background-upload-1)
@@ -84,7 +85,7 @@ yarn add react-native-compressor
84
85
 
85
86
  ### [New Architecture (Turbo Module)](https://reactnative.dev/docs/new-architecture-intro) Supported
86
87
 
87
- you can give feedback on [Discord channel](https://discord.gg/dtYzk8sp)
88
+ you can give feedback on [Discord channel](https://discord.gg/6Wx8Em8KAN)
88
89
 
89
90
  ### Managed Expo
90
91
 
@@ -299,6 +300,16 @@ const downloadFileUrl = await download(url, (progress) => {
299
300
  });
300
301
  ```
301
302
 
303
+ ### Video Thumbnail
304
+
305
+ ```js
306
+ import { createVideoThumbnail, clearCache } from 'react-native-compressor';
307
+
308
+ const thumbnail = await createVideoThumbnail(videoUri);
309
+
310
+ await clearCache(); // this will clear cache of thumbnails cache directory
311
+ ```
312
+
302
313
  # API
303
314
 
304
315
  ## Image
@@ -421,6 +432,16 @@ type FileSystemUploadOptions = (
421
432
 
422
433
  - ##### download: ( fileUrl: string, downloadProgress?: (progress: number) => void, progressDivider?: number ) => Promise< string >
423
434
 
435
+ ### Create Video Thumbnail and Clear Cache
436
+
437
+ - #### createVideoThumbnail( fileUrl: string, options: {header:Object} ): Promise<{ path: string;size: number; mime: string; width: number; height: number; }>
438
+
439
+ it will save the thumbnail of the video into the cache directory and return the thumbnail URI which you can display
440
+
441
+ - #### clearCache(cacheDir?: string): Promise< string >
442
+
443
+ it will clear the cache that was created from createVideoThumbnail, in future this clear cache will be totally customized
444
+
424
445
  ### Get Metadata Of Video
425
446
 
426
447
  if you want to get metadata of video than you can use this function
@@ -1,7 +1,7 @@
1
1
  buildscript {
2
2
  // Buildscript is evaluated before everything else so we can't use getExtOrDefault
3
3
  def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["Compressor_kotlinVersion"]
4
-
4
+
5
5
  repositories {
6
6
  google()
7
7
  mavenCentral()
@@ -110,8 +110,12 @@ dependencies {
110
110
  //noinspection GradleDynamicVersion
111
111
  implementation "com.facebook.react:react-native:+"
112
112
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
113
+
114
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
115
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
116
+ implementation "com.googlecode.mp4parser:isoparser:1.0.6"
117
+
113
118
  implementation 'io.github.lizhangqu:coreprogress:1.0.2'
114
- implementation 'com.github.numandev1:VideoCompressor:1a262bba37'
115
119
  }
116
120
 
117
121
  if (isNewArchitectureEnabled()) {
@@ -11,7 +11,7 @@ import android.media.MediaMuxer
11
11
  import android.os.Build
12
12
  import android.util.Log
13
13
  import androidx.annotation.RequiresApi
14
- import numan.dev.videocompressor.codecinputsurface.CodecInputSurface
14
+ import com.reactnativecompressor.Video.VideoCompressor.video.OutputSurface
15
15
  import java.io.File
16
16
  import java.io.IOException
17
17
  import java.nio.ByteBuffer
@@ -24,7 +24,7 @@ class AudioCompressor {
24
24
  private var mEncoder: MediaCodec? = null
25
25
  private var mDecoder: MediaCodec? = null
26
26
  private var mTrackIndex = 0
27
- private var mInputSurface: CodecInputSurface? = null
27
+ private var mInputSurface: OutputSurface? = null
28
28
 
29
29
  // bit rate, in bits per second
30
30
  private var mBitRate = -1
@@ -8,6 +8,7 @@ import com.facebook.react.bridge.ReactMethod
8
8
  import com.facebook.react.bridge.ReadableMap
9
9
  import com.reactnativecompressor.Audio.AudioMain
10
10
  import com.reactnativecompressor.Image.ImageMain
11
+ import com.reactnativecompressor.Utils.CreateVideoThumbnailClass
11
12
  import com.reactnativecompressor.Utils.Downloader
12
13
  import com.reactnativecompressor.Utils.EventEmitterHandler
13
14
  import com.reactnativecompressor.Utils.Uploader
@@ -20,6 +21,7 @@ class CompressorModule(private val reactContext: ReactApplicationContext) : Comp
20
21
  private val imageMain: ImageMain = ImageMain(reactContext)
21
22
  private val videoMain: VideoMain = VideoMain(reactContext)
22
23
  private val audioMain: AudioMain = AudioMain(reactContext)
24
+ private val videoThumbnail: CreateVideoThumbnailClass = CreateVideoThumbnailClass(reactContext)
23
25
 
24
26
  override fun initialize() {
25
27
  super.initialize()
@@ -147,6 +149,16 @@ class CompressorModule(private val reactContext: ReactApplicationContext) : Comp
147
149
  }
148
150
  }
149
151
 
152
+ @ReactMethod
153
+ override fun createVideoThumbnail(fileUrl:String, options:ReadableMap, promise:Promise) {
154
+ videoThumbnail.create(fileUrl,options,promise)
155
+ }
156
+
157
+ @ReactMethod
158
+ override fun clearCache(cacheDir:String?, promise:Promise) {
159
+ CreateVideoThumbnailClass.clearCache(cacheDir, promise, reactContext)
160
+ }
161
+
150
162
  @ReactMethod
151
163
  override fun addListener(eventName: String) {
152
164
  }
@@ -32,17 +32,17 @@ object Uploader {
32
32
  if (url != null) {
33
33
  builder.url(url)
34
34
  }
35
- val headerIterator = options!!.headers!!.keySetIterator()
36
- while (headerIterator.hasNextKey()) {
37
- val key = headerIterator.nextKey()
38
- val value = options.headers!!.getString(key)
39
- Log.d(TAG, "$key value: $value")
40
- builder.addHeader(key, value.toString())
41
- if (key.lowercase(Locale.getDefault()) === "content-type:") {
42
- contentType = value
43
- }
35
+ val headerIterator = options?.headers?.keySetIterator()
36
+ while (headerIterator?.hasNextKey() == true) {
37
+ val key = headerIterator.nextKey()
38
+ val value = options.headers?.getString(key)
39
+ Log.d(TAG, "$key value: $value")
40
+ builder.addHeader(key, value.toString())
41
+ if (key.lowercase(Locale.getDefault()) == "content-type:") {
42
+ contentType = value
43
+ }
44
44
  }
45
- val mediaType: MediaType = contentType?.toMediaTypeOrNull()!!;
45
+ val mediaType: MediaType? = contentType?.toMediaTypeOrNull();
46
46
  val body = RequestBody.create(mediaType, uploadableFile)
47
47
  val requestBody = ProgressHelper.withProgress(body, object : ProgressUIListener() {
48
48
  //if you don't need this method, don't override this methd. It isn't an abstract method, just an empty method.
@@ -52,7 +52,7 @@ object Uploader {
52
52
  }
53
53
 
54
54
  override fun onUIProgressChanged(numBytes: Long, totalBytes: Long, percent: Float, speed: Float) {
55
- EventEmitterHandler.sendUploadProgressEvent(numBytes,totalBytes,options.uuid)
55
+ EventEmitterHandler.sendUploadProgressEvent(numBytes,totalBytes,options?.uuid)
56
56
  Log.d(TAG, "=============start===============")
57
57
  Log.d(TAG, "numBytes:$numBytes")
58
58
  Log.d(TAG, "totalBytes:$totalBytes")
@@ -7,8 +7,8 @@ import android.provider.OpenableColumns
7
7
  import android.util.Log
8
8
  import com.facebook.react.bridge.Promise
9
9
  import com.facebook.react.bridge.ReactApplicationContext
10
- import numan.dev.videocompressor.VideoCompressTask
11
- import numan.dev.videocompressor.VideoCompressor
10
+ import com.reactnativecompressor.Video.VideoCompressor.CompressionListener
11
+ import com.reactnativecompressor.Video.VideoCompressor.VideoCompressorClass
12
12
  import java.io.FileNotFoundException
13
13
  import java.io.IOException
14
14
  import java.net.HttpURLConnection
@@ -17,7 +17,7 @@ import java.util.UUID
17
17
 
18
18
  object Utils {
19
19
  private const val TAG = "react-native-compessor"
20
- var compressorExports: MutableMap<String, VideoCompressTask> = HashMap()
20
+ var compressorExports: MutableMap<String, VideoCompressorClass?> = HashMap()
21
21
  @JvmStatic
22
22
  fun generateCacheFilePath(extension: String, reactContext: ReactApplicationContext): String {
23
23
  val outputDir = reactContext.cacheDir
@@ -26,48 +26,54 @@ object Utils {
26
26
 
27
27
  @JvmStatic
28
28
  fun compressVideo(srcPath: String, destinationPath: String, resultWidth: Int, resultHeight: Int, videoBitRate: Float, uuid: String,progressDivider:Int, promise: Promise, reactContext: ReactApplicationContext) {
29
- val currentVideoCompression = intArrayOf(0)
30
- try {
31
- val export = VideoCompressor.convertVideo(srcPath, destinationPath, resultWidth, resultHeight, videoBitRate.toInt(), object : VideoCompressor.ProgressListener {
32
- override fun onStart() {
33
- //convert start
29
+ val currentVideoCompression = intArrayOf(0)
30
+ val videoCompressorClass: VideoCompressorClass? = VideoCompressorClass(reactContext);
31
+ compressorExports[uuid] = videoCompressorClass
32
+ videoCompressorClass?.start(srcPath, destinationPath, resultWidth, resultHeight, videoBitRate.toInt(),
33
+ listener = object : CompressionListener {
34
+ override fun onProgress(index: Int, percent: Float) {
35
+ if (percent <= 100)
36
+ {
37
+ val roundProgress = Math.round(percent)
38
+ if (progressDivider==0||(roundProgress % progressDivider == 0 && roundProgress > currentVideoCompression[0])) {
39
+ EventEmitterHandler.emitVideoCompressProgress((percent / 100).toDouble(),uuid)
40
+ currentVideoCompression[0] = roundProgress
34
41
  }
42
+ }
43
+ }
35
44
 
36
- override fun onFinish(result: Boolean) {
37
- val fileUrl = "file://$destinationPath"
38
- //convert finish,result(true is success,false is fail)
39
- promise.resolve(fileUrl)
40
- MediaCache.removeCompletedImagePath(fileUrl)
41
- }
45
+ override fun onStart(index: Int) {
42
46
 
43
- override fun onError(errorMessage: String) {
44
- if (errorMessage == "class java.lang.AssertionError") {
45
- promise.resolve("file://$srcPath")
46
- } else {
47
- promise.reject("Compression has canncelled")
48
- }
49
- }
47
+ }
50
48
 
51
- override fun onProgress(percent: Float) {
52
- val roundProgress = Math.round(percent)
53
- if (progressDivider==0||(roundProgress % progressDivider == 0 && roundProgress > currentVideoCompression[0])) {
54
- EventEmitterHandler.emitVideoCompressProgress((percent / 100).toDouble(),uuid)
55
- currentVideoCompression[0] = roundProgress
56
- }
57
- }
58
- })
59
- compressorExports[uuid] = export
60
- } catch (ex: Exception) {
61
- promise.reject(ex)
62
- } finally {
63
- currentVideoCompression[0] = 0
64
- }
49
+ override fun onSuccess(index: Int, size: Long, path: String?) {
50
+ val fileUrl = "file://$destinationPath"
51
+ //convert finish,result(true is success,false is fail)
52
+ promise.resolve(fileUrl)
53
+ MediaCache.removeCompletedImagePath(fileUrl)
54
+ currentVideoCompression[0] = 0
55
+ compressorExports[uuid]=null
56
+ }
57
+
58
+ override fun onFailure(index: Int, failureMessage: String) {
59
+ Log.wtf("failureMessage", failureMessage)
60
+ currentVideoCompression[0] = 0
61
+ }
62
+
63
+ override fun onCancelled(index: Int) {
64
+ Log.wtf("TAG", "compression has been cancelled")
65
+ // make UI changes, cleanup, etc
66
+ currentVideoCompression[0] = 0
67
+ }
68
+ },
69
+ )
65
70
  }
66
71
 
67
72
  fun cancelCompressionHelper(uuid: String) {
68
73
  try {
69
74
  val export = compressorExports[uuid]
70
- export!!.cancel(true)
75
+ export?.cancel()
76
+ compressorExports[uuid]=null
71
77
  } catch (ex: Exception) {
72
78
  }
73
79
  }
@@ -0,0 +1,158 @@
1
+ package com.reactnativecompressor.Utils
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.graphics.BitmapFactory
6
+ import android.media.MediaMetadataRetriever
7
+ import android.net.Uri
8
+ import android.os.Build
9
+ import android.text.TextUtils
10
+ import android.webkit.URLUtil
11
+ import androidx.annotation.RequiresApi
12
+ import com.facebook.react.bridge.Arguments
13
+ import com.facebook.react.bridge.GuardedResultAsyncTask
14
+ import com.facebook.react.bridge.Promise
15
+ import com.facebook.react.bridge.ReactApplicationContext
16
+ import com.facebook.react.bridge.ReactContext
17
+ import com.facebook.react.bridge.ReactMethod
18
+ import com.facebook.react.bridge.ReadableMap
19
+ import java.io.File
20
+ import java.io.FileOutputStream
21
+ import java.io.IOException
22
+ import java.io.OutputStream
23
+ import java.io.UnsupportedEncodingException
24
+ import java.lang.ref.WeakReference
25
+ import java.net.URLDecoder
26
+ import java.util.UUID
27
+
28
+
29
+ class CreateVideoThumbnailClass(private val reactContext: ReactApplicationContext) {
30
+ @ReactMethod
31
+ fun create(fileUrl:String,options: ReadableMap, promise: Promise) {
32
+ ProcessDataTask(reactContext,fileUrl, promise, options).execute()
33
+ }
34
+
35
+ private class ProcessDataTask(reactContext: ReactContext,private val filePath:String, private val promise: Promise, private val options: ReadableMap) : GuardedResultAsyncTask<ReadableMap?>(reactContext.exceptionHandler) {
36
+ private val weakContext: WeakReference<Context>
37
+
38
+ init {
39
+ weakContext = WeakReference(reactContext.applicationContext)
40
+ }
41
+
42
+ @RequiresApi(Build.VERSION_CODES.N)
43
+ override fun doInBackgroundGuarded(): ReadableMap? {
44
+ val format = "jpeg"
45
+ val cacheName = if (options.hasKey("cacheName")) options.getString("cacheName") else ""
46
+ val thumbnailDir = weakContext.get()!!.applicationContext.cacheDir.absolutePath + "/thumbnails"
47
+ val cacheDir = createDirIfNotExists(thumbnailDir)
48
+ if (!TextUtils.isEmpty(cacheName)) {
49
+ val file = File(thumbnailDir, "$cacheName.$format")
50
+ if (file.exists()) {
51
+ val map = Arguments.createMap()
52
+ map.putString("path", "file://" + file.absolutePath)
53
+ val image = BitmapFactory.decodeFile(file.absolutePath)
54
+ map.putDouble("size", image.byteCount.toDouble())
55
+ map.putString("mime", "image/$format")
56
+ map.putDouble("width", image.width.toDouble())
57
+ map.putDouble("height", image.height.toDouble())
58
+ return map
59
+ }
60
+ }
61
+ val headers: Map<String, String> = if (options.hasKey("headers")) options.getMap("headers")!!.toHashMap() as Map<String, String> else HashMap<String, String>()
62
+ val fileName = if (TextUtils.isEmpty(cacheName)) "thumb-" + UUID.randomUUID().toString() else "$cacheName.$format"
63
+ var fOut: OutputStream? = null
64
+ try {
65
+ val file = File(cacheDir, fileName)
66
+ val context = weakContext.get()
67
+ val image = getBitmapAtTime(context, filePath, 0, headers)
68
+ file.createNewFile()
69
+ fOut = FileOutputStream(file)
70
+
71
+ // 100 means no compression, the lower you go, the stronger the compression
72
+ image.compress(Bitmap.CompressFormat.JPEG, 90, fOut)
73
+ fOut.flush()
74
+ fOut.close()
75
+
76
+ val map = Arguments.createMap()
77
+ map.putString("path", "file://" + file.absolutePath)
78
+ map.putDouble("size", image.byteCount.toDouble())
79
+ map.putString("mime", "image/$format")
80
+ map.putDouble("width", image.width.toDouble())
81
+ map.putDouble("height", image.height.toDouble())
82
+ return map
83
+ } catch (e: Exception) {
84
+ promise.reject("CreateVideoThumbnail_ERROR", e)
85
+ }
86
+ return null
87
+ }
88
+
89
+ override fun onPostExecuteGuarded(readableArray: ReadableMap?) {
90
+ promise.resolve(readableArray)
91
+ }
92
+ }
93
+
94
+ companion object {
95
+ // delete previously added files one by one untill requred space is available
96
+ fun clearCache(cacheDir: String?,promise:Promise, reactContext: ReactApplicationContext) {
97
+ val cacheDirectory=cacheDir?.takeIf { it.isNotEmpty() } ?:"/thumbnails"
98
+ val thumbnailDir: String = reactContext.getApplicationContext().getCacheDir().getAbsolutePath() + cacheDirectory
99
+ val thumbnailDirFile = createDirIfNotExists(thumbnailDir)
100
+
101
+ if (thumbnailDirFile != null) {
102
+ val files = thumbnailDirFile.listFiles()
103
+
104
+ // Loop through the files and delete them
105
+ if (files != null) {
106
+ for (file in files) {
107
+ if (file.isFile) {
108
+ file.delete()
109
+ }
110
+ }
111
+ }
112
+ }
113
+ promise.resolve("done")
114
+ }
115
+
116
+ private fun createDirIfNotExists(path: String): File {
117
+ val dir = File(path)
118
+ if (dir.exists()) {
119
+ return dir
120
+ }
121
+ try {
122
+ dir.mkdirs()
123
+ // Add .nomedia to hide the thumbnail directory from gallery
124
+ val noMedia = File(path, ".nomedia")
125
+ noMedia.createNewFile()
126
+ } catch (e: IOException) {
127
+ e.printStackTrace()
128
+ }
129
+ return dir
130
+ }
131
+
132
+ private fun getBitmapAtTime(context: Context?, filePath: String?, time: Int, headers: Map<String, String>): Bitmap {
133
+ val retriever = MediaMetadataRetriever()
134
+ if (URLUtil.isFileUrl(filePath)) {
135
+ val decodedPath: String?
136
+ decodedPath = try {
137
+ URLDecoder.decode(filePath, "UTF-8")
138
+ } catch (e: UnsupportedEncodingException) {
139
+ filePath
140
+ }
141
+ retriever.setDataSource(decodedPath!!.replace("file://", ""))
142
+ } else if (filePath!!.contains("content://")) {
143
+ retriever.setDataSource(context, Uri.parse(filePath))
144
+ } else {
145
+ check(Build.VERSION.SDK_INT >= 14) { "Remote videos aren't supported on sdk_version < 14" }
146
+ retriever.setDataSource(filePath, headers)
147
+ }
148
+ val image = retriever.getFrameAtTime((time * 1000).toLong(), MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
149
+ try {
150
+ retriever.release()
151
+ } catch (e: IOException) {
152
+ throw RuntimeException(e)
153
+ }
154
+ checkNotNull(image) { "File doesn't exist or not supported" }
155
+ return image
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,73 @@
1
+ package com.reactnativecompressor.Video.VideoCompressor
2
+
3
+ import androidx.annotation.MainThread
4
+ import androidx.annotation.WorkerThread
5
+
6
+ /**
7
+ * Interface for listening to video compression events.
8
+ */
9
+ interface CompressionListener {
10
+ /**
11
+ * Called when video compression starts.
12
+ *
13
+ * @param index The index of the video being compressed.
14
+ */
15
+ @MainThread
16
+ fun onStart(index: Int)
17
+
18
+ /**
19
+ * Called when video compression succeeds.
20
+ *
21
+ * @param index The index of the video that was compressed.
22
+ * @param size The size of the compressed video.
23
+ * @param path The path to the compressed video file (nullable).
24
+ */
25
+ @MainThread
26
+ fun onSuccess(index: Int, size: Long, path: String?)
27
+
28
+ /**
29
+ * Called when video compression fails.
30
+ *
31
+ * @param index The index of the video that failed to compress.
32
+ * @param failureMessage A message describing the reason for the failure.
33
+ */
34
+ @MainThread
35
+ fun onFailure(index: Int, failureMessage: String)
36
+
37
+ /**
38
+ * Called to report the progress of video compression.
39
+ *
40
+ * @param index The index of the video being compressed.
41
+ * @param percent The progress percentage (0.0 to 100.0).
42
+ */
43
+ @WorkerThread
44
+ fun onProgress(index: Int, percent: Float)
45
+
46
+ /**
47
+ * Called when video compression is canceled.
48
+ *
49
+ * @param index The index of the video that was canceled.
50
+ */
51
+ @WorkerThread
52
+ fun onCancelled(index: Int)
53
+ }
54
+
55
+ /**
56
+ * Interface for tracking the progress of video compression.
57
+ */
58
+ interface CompressionProgressListener {
59
+ /**
60
+ * Called when the progress of video compression changes.
61
+ *
62
+ * @param index The index of the video being compressed.
63
+ * @param percent The current progress percentage (0.0 to 100.0).
64
+ */
65
+ fun onProgressChanged(index: Int, percent: Float)
66
+
67
+ /**
68
+ * Called when the compression progress is canceled.
69
+ *
70
+ * @param index The index of the video compression that was canceled.
71
+ */
72
+ fun onProgressCancelled(index: Int)
73
+ }
@@ -0,0 +1,148 @@
1
+ package com.reactnativecompressor.Video.VideoCompressor
2
+
3
+ import android.database.Cursor
4
+ import android.net.Uri
5
+ import android.provider.MediaStore
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import com.reactnativecompressor.Video.VideoCompressor.compressor.Compressor.compressVideo
8
+ import com.reactnativecompressor.Video.VideoCompressor.compressor.Compressor.isRunning
9
+ import com.reactnativecompressor.Video.VideoCompressor.video.Result
10
+ import kotlinx.coroutines.CoroutineScope
11
+ import kotlinx.coroutines.Dispatchers
12
+ import kotlinx.coroutines.Job
13
+ import kotlinx.coroutines.launch
14
+ import kotlinx.coroutines.withContext
15
+ import java.io.File
16
+
17
+ class VideoCompressorClass(private val context: ReactApplicationContext) {
18
+ private var job: Job? = null
19
+
20
+ @JvmOverloads
21
+ fun start(
22
+ srcPath: String,
23
+ destPath: String,
24
+ outputWidth: Int,
25
+ outputHeight: Int,
26
+ bitrate: Int,
27
+ listener: CompressionListener,
28
+ ) {
29
+ val uris = mutableListOf<Uri>()
30
+ val uri = Uri.parse(srcPath)
31
+ uris.add(uri)
32
+ val isStreamable: Boolean = false
33
+ doVideoCompression(
34
+ uris,
35
+ isStreamable,
36
+ outputWidth,
37
+ outputHeight,
38
+ bitrate,
39
+ listener,
40
+ destPath
41
+ )
42
+ }
43
+
44
+ fun cancel() {
45
+ job?.cancel()
46
+ isRunning = false
47
+ }
48
+
49
+ private fun doVideoCompression(
50
+ uris: List<Uri>,
51
+ isStreamable: Boolean,
52
+ outputWidth: Int,
53
+ outputHeight: Int,
54
+ bitrate: Int,
55
+ listener: CompressionListener,
56
+ destPath: String
57
+ ) {
58
+ var streamableFile: File? = null
59
+ for (i in uris.indices) {
60
+ job = CoroutineScope(Dispatchers.Main).launch {
61
+ val desFile = File(destPath)
62
+
63
+ if (isStreamable)
64
+ streamableFile = File(destPath)
65
+
66
+ desFile?.let {
67
+ isRunning = true
68
+ listener.onStart(i)
69
+ val result = startCompression(
70
+ i,
71
+ uris[i],
72
+ desFile.path,
73
+ streamableFile?.path,
74
+ outputWidth,
75
+ outputHeight,
76
+ bitrate,
77
+ listener,
78
+ )
79
+
80
+ // Runs in Main(UI) Thread
81
+ if (result.success) {
82
+ listener.onSuccess(i, result.size, result.path)
83
+ } else {
84
+ listener.onFailure(i, result.failureMessage ?: "An error has occurred!")
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ private suspend fun startCompression(
92
+ index: Int,
93
+ srcUri: Uri,
94
+ destPath: String,
95
+ streamableFile: String? = null,
96
+ outputWidth: Int,
97
+ outputHeight: Int,
98
+ bitrate: Int,
99
+ listener: CompressionListener,
100
+ ): Result = withContext(Dispatchers.Default) {
101
+ return@withContext compressVideo(
102
+ index,
103
+ context,
104
+ srcUri,
105
+ destPath,
106
+ streamableFile,
107
+ outputWidth,
108
+ outputHeight,
109
+ bitrate,
110
+ object : CompressionProgressListener {
111
+ override fun onProgressChanged(index: Int, percent: Float) {
112
+ listener.onProgress(index, percent)
113
+ }
114
+
115
+ override fun onProgressCancelled(index: Int) {
116
+ listener.onCancelled(index)
117
+ }
118
+ },
119
+ )
120
+ }
121
+
122
+ private fun getMediaPath(uri: Uri): String {
123
+ val scheme = uri.scheme
124
+ if (scheme == null || scheme == "file") {
125
+ // The URI is a file URI, so use the path directly.
126
+ return uri.path ?: ""
127
+ } else {
128
+ // The URI is not a file URI, so try to copy the content to a file.
129
+ val resolver = context.contentResolver
130
+ val projection = arrayOf(MediaStore.Video.Media.DATA)
131
+ var cursor: Cursor? = null
132
+ try {
133
+ cursor = resolver.query(uri, projection, null, null, null)
134
+ if (cursor != null && cursor.moveToFirst()) {
135
+ val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)
136
+ return cursor.getString(columnIndex)
137
+ }
138
+ } catch (e: Exception) {
139
+ // Handle the exception as needed.
140
+ } finally {
141
+ cursor?.close()
142
+ }
143
+ }
144
+
145
+ // If all else fails, return an empty string or handle the error accordingly.
146
+ return ""
147
+ }
148
+ }