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.
- package/README.md +22 -1
- package/android/build.gradle +6 -2
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt +2 -2
- package/android/src/main/java/com/reactnativecompressor/CompressorModule.kt +12 -0
- package/android/src/main/java/com/reactnativecompressor/Utils/Uploader.kt +11 -11
- package/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt +42 -36
- package/android/src/main/java/com/reactnativecompressor/Utils/createVideoThumbnail.kt +158 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/CompressionInterface.kt +73 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/VideoCompressorClass.kt +148 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt +562 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/Atoms.kt +55 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt +196 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/NumbersUtils.kt +47 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/StreamableVideo.kt +209 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/InputSurface.kt +128 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/MP4Builder.kt +388 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Mdat.kt +74 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Mp4Movie.kt +54 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/OutputSurface.kt +102 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Result.kt +9 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Sample.kt +3 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/TextureRenderer.kt +214 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Track.kt +286 -0
- package/android/src/oldarch/CompressorSpec.kt +2 -0
- package/ios/Compressor.mm +9 -0
- package/ios/CompressorManager.swift +11 -0
- package/ios/Utils/CreateVideoThumbnail.swift +111 -0
- package/lib/commonjs/Spec/NativeCompressor.js.map +1 -1
- package/lib/commonjs/index.js +14 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/utils/index.js +12 -1
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/module/Spec/NativeCompressor.js.map +1 -1
- package/lib/module/index.js +4 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/index.js +7 -0
- package/lib/module/utils/index.js.map +1 -1
- package/lib/typescript/Spec/NativeCompressor.d.ts +8 -0
- package/lib/typescript/Spec/NativeCompressor.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +15 -3
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/utils/index.d.ts +16 -1
- package/lib/typescript/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Spec/NativeCompressor.ts +11 -0
- package/src/index.tsx +6 -0
- 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/
|
|
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
|
package/android/build.gradle
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
36
|
-
while (headerIterator
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
|
11
|
-
import
|
|
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,
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
+
}
|