react-native-compressor 1.18.1 → 1.19.0
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/android/src/main/java/com/reactnativecompressor/Image/ImageCompressor.kt +44 -23
- package/android/src/main/java/com/reactnativecompressor/Utils/createVideoThumbnail.kt +42 -21
- package/android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt +1 -1
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressionProfile.kt +21 -11
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt +369 -87
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt +24 -5
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/LocationExtractor.kt +397 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/LocationBox.kt +47 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/MP4Builder.kt +39 -7
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Mp4Movie.kt +7 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/OutputSurface.kt +30 -4
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt +24 -1
- package/ios/Image/ImageCompressor.swift +3 -2
- package/ios/Utils/CreateVideoThumbnail.swift +40 -21
- package/ios/Utils/Uploader.swift +14 -7
- package/ios/Video/VideoMain.swift +27 -11
- package/lib/commonjs/index.js +1 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/index.d.ts +1 -0
- package/lib/typescript/src/utils/index.d.ts.map +1 -1
- package/package.json +7 -1
- package/src/index.tsx +1 -0
- package/src/utils/index.tsx +1 -0
|
@@ -9,6 +9,7 @@ import android.graphics.Paint
|
|
|
9
9
|
import android.media.ExifInterface
|
|
10
10
|
import android.net.Uri
|
|
11
11
|
import android.util.Base64
|
|
12
|
+
import android.util.Log
|
|
12
13
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
13
14
|
import com.reactnativecompressor.Utils.MediaCache
|
|
14
15
|
import com.reactnativecompressor.Utils.Utils.exifAttributes
|
|
@@ -21,6 +22,8 @@ import java.io.IOException
|
|
|
21
22
|
import java.net.MalformedURLException
|
|
22
23
|
|
|
23
24
|
object ImageCompressor {
|
|
25
|
+
private const val TAG = "ImageCompressor"
|
|
26
|
+
|
|
24
27
|
fun getRNFileUrl(filePath: String?): String? {
|
|
25
28
|
var filePath = filePath
|
|
26
29
|
val returnAbleFile = File(filePath)
|
|
@@ -56,25 +59,38 @@ object ImageCompressor {
|
|
|
56
59
|
return BitmapFactory.decodeFile(filePath)
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Strip "file://" / "content://" scheme so legacy ExifInterface can open
|
|
64
|
+
* the underlying JPEG. ExifInterface(String) only accepts raw filesystem
|
|
65
|
+
* paths — passing a URI string makes it fail silently inside the
|
|
66
|
+
* try/catch and drops every EXIF tag, including GPS.
|
|
67
|
+
*/
|
|
68
|
+
private fun normalizeToFilePath(input: String): String {
|
|
69
|
+
if (input.startsWith("file://") || input.startsWith("content://")) {
|
|
70
|
+
return Uri.parse(input).path ?: input
|
|
71
|
+
}
|
|
72
|
+
return input
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fun copyExifInfo(imagePath: String, outputUri: String) {
|
|
76
|
+
try {
|
|
77
|
+
val sourcePath = normalizeToFilePath(imagePath)
|
|
78
|
+
val outPath = normalizeToFilePath(outputUri)
|
|
79
|
+
val sourceExif = ExifInterface(sourcePath)
|
|
80
|
+
val compressedExif = ExifInterface(outPath)
|
|
81
|
+
var copied = 0
|
|
82
|
+
for (tag in exifAttributes) {
|
|
83
|
+
val sourceValue = sourceExif.getAttribute(tag) ?: continue
|
|
84
|
+
if (compressedExif.getAttribute(tag) == null) {
|
|
85
|
+
compressedExif.setAttribute(tag, sourceValue)
|
|
86
|
+
copied++
|
|
87
|
+
}
|
|
71
88
|
}
|
|
72
|
-
|
|
89
|
+
compressedExif.saveAttributes()
|
|
90
|
+
Log.i(TAG, "copyExifInfo copied $copied tags from $sourcePath -> $outPath")
|
|
91
|
+
} catch (e: Exception) {
|
|
92
|
+
Log.w(TAG, "copyExifInfo failed for $imagePath", e)
|
|
73
93
|
}
|
|
74
|
-
compressedExif.saveAttributes()
|
|
75
|
-
} catch (e: Exception) {
|
|
76
|
-
e.printStackTrace()
|
|
77
|
-
}
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
fun encodeImage(imageDataByteArrayOutputStream: ByteArrayOutputStream, isBase64: Boolean, outputExtension: String?,imagePath: String?, reactContext: ReactApplicationContext?): String? {
|
|
@@ -84,10 +100,14 @@ object ImageCompressor {
|
|
|
84
100
|
} else {
|
|
85
101
|
val outputUri = generateCacheFilePath(outputExtension!!, reactContext!!)
|
|
86
102
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
// Close the stream before ExifInterface re-opens the file so
|
|
104
|
+
// the JPEG bytes are fully flushed; otherwise saveAttributes()
|
|
105
|
+
// may truncate the in-flight write.
|
|
106
|
+
FileOutputStream(outputUri).use { fos ->
|
|
107
|
+
imageDataByteArrayOutputStream.writeTo(fos)
|
|
108
|
+
}
|
|
89
109
|
|
|
90
|
-
|
|
110
|
+
copyExifInfo(imagePath!!, outputUri)
|
|
91
111
|
|
|
92
112
|
return getRNFileUrl(outputUri)
|
|
93
113
|
} catch (e: Exception) {
|
|
@@ -113,16 +133,17 @@ object ImageCompressor {
|
|
|
113
133
|
|
|
114
134
|
fun compress(image: Bitmap?, output: ImageCompressorOptions.OutputType, quality: Float,disablePngTransparency:Boolean): ByteArrayOutputStream {
|
|
115
135
|
var stream = ByteArrayOutputStream()
|
|
136
|
+
val normalizedQuality = Math.round(100 * quality.coerceIn(0f, 1f))
|
|
116
137
|
if (output === ImageCompressorOptions.OutputType.jpg)
|
|
117
138
|
{
|
|
118
|
-
image!!.compress(CompressFormat.JPEG,
|
|
139
|
+
image!!.compress(CompressFormat.JPEG, normalizedQuality, stream)
|
|
119
140
|
}
|
|
120
141
|
else
|
|
121
142
|
{
|
|
122
143
|
var bitmap = image
|
|
123
144
|
if(disablePngTransparency)
|
|
124
145
|
{
|
|
125
|
-
image!!.compress(CompressFormat.JPEG,
|
|
146
|
+
image!!.compress(CompressFormat.JPEG, normalizedQuality, stream)
|
|
126
147
|
val byteArray: ByteArray = stream.toByteArray()
|
|
127
148
|
stream=ByteArrayOutputStream()
|
|
128
149
|
bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
|
|
@@ -261,7 +282,7 @@ object ImageCompressor {
|
|
|
261
282
|
if (bitmap == null || imagePath == null) return bitmap
|
|
262
283
|
|
|
263
284
|
return try {
|
|
264
|
-
val exif = ExifInterface(imagePath)
|
|
285
|
+
val exif = ExifInterface(normalizeToFilePath(imagePath))
|
|
265
286
|
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
|
266
287
|
val matrix = Matrix()
|
|
267
288
|
|
|
@@ -62,6 +62,7 @@ class CreateVideoThumbnailClass(private val reactContext: ReactApplicationContex
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
val headers: Map<String, String> = if (options.hasKey("headers")) options.getMap("headers")!!.toHashMap() as Map<String, String> else HashMap<String, String>()
|
|
65
|
+
val quality = if (options.hasKey("quality")) (options.getDouble("quality") * 100).toInt().coerceIn(0, 100) else DEFAULT_THUMBNAIL_QUALITY
|
|
65
66
|
val fileName = if (TextUtils.isEmpty(cacheName)) "thumb-" + UUID.randomUUID().toString() else "$cacheName.$format"
|
|
66
67
|
var fOut: OutputStream? = null
|
|
67
68
|
|
|
@@ -73,7 +74,7 @@ class CreateVideoThumbnailClass(private val reactContext: ReactApplicationContex
|
|
|
73
74
|
fOut = FileOutputStream(file)
|
|
74
75
|
|
|
75
76
|
// 100 means no compression, the lower you go, the stronger the compression
|
|
76
|
-
image.compress(Bitmap.CompressFormat.JPEG,
|
|
77
|
+
image.compress(Bitmap.CompressFormat.JPEG, quality, fOut)
|
|
77
78
|
fOut.flush()
|
|
78
79
|
fOut.close()
|
|
79
80
|
|
|
@@ -96,6 +97,8 @@ class CreateVideoThumbnailClass(private val reactContext: ReactApplicationContex
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
companion object {
|
|
100
|
+
private const val DEFAULT_THUMBNAIL_QUALITY = 90
|
|
101
|
+
|
|
99
102
|
// delete previously added files one by one untill requred space is available
|
|
100
103
|
fun clearCache(cacheDir: String?,promise:Promise, reactContext: ReactApplicationContext) {
|
|
101
104
|
val cacheDirectory=cacheDir?.takeIf { it.isNotEmpty() } ?:"/thumbnails"
|
|
@@ -134,29 +137,47 @@ class CreateVideoThumbnailClass(private val reactContext: ReactApplicationContex
|
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
private fun getBitmapAtTime(context: Context?, filePath: String?, time: Int, headers: Map<String, String>): Bitmap {
|
|
140
|
+
check(!filePath.isNullOrEmpty()) { "Video file path cannot be null or empty" }
|
|
137
141
|
val retriever = MediaMetadataRetriever()
|
|
138
|
-
if (URLUtil.isFileUrl(filePath)) {
|
|
139
|
-
val decodedPath: String?
|
|
140
|
-
decodedPath = try {
|
|
141
|
-
URLDecoder.decode(filePath, "UTF-8")
|
|
142
|
-
} catch (e: UnsupportedEncodingException) {
|
|
143
|
-
filePath
|
|
144
|
-
}
|
|
145
|
-
retriever.setDataSource(decodedPath!!.replace("file://", ""))
|
|
146
|
-
} else if (filePath!!.contains("content://")) {
|
|
147
|
-
retriever.setDataSource(context, Uri.parse(filePath))
|
|
148
|
-
} else {
|
|
149
|
-
check(Build.VERSION.SDK_INT >= 14) { "Remote videos aren't supported on sdk_version < 14" }
|
|
150
|
-
retriever.setDataSource(filePath, headers)
|
|
151
|
-
}
|
|
152
|
-
val image = retriever.getFrameAtTime((time * 1000).toLong(), MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
|
153
142
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
143
|
+
if (URLUtil.isFileUrl(filePath)) {
|
|
144
|
+
val decodedPath: String? = try {
|
|
145
|
+
URLDecoder.decode(filePath, "UTF-8")
|
|
146
|
+
} catch (e: UnsupportedEncodingException) {
|
|
147
|
+
filePath
|
|
148
|
+
}
|
|
149
|
+
retriever.setDataSource(decodedPath!!.replace("file://", ""))
|
|
150
|
+
} else if (filePath.contains("content://")) {
|
|
151
|
+
retriever.setDataSource(context, Uri.parse(filePath))
|
|
152
|
+
} else {
|
|
153
|
+
check(Build.VERSION.SDK_INT >= 14) { "Remote videos aren't supported on sdk_version < 14" }
|
|
154
|
+
retriever.setDataSource(filePath, headers)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
val requestedTimeUs = (time * 1000).toLong()
|
|
158
|
+
val frameAttempts = arrayOf(
|
|
159
|
+
Pair(requestedTimeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC),
|
|
160
|
+
Pair(requestedTimeUs, MediaMetadataRetriever.OPTION_CLOSEST),
|
|
161
|
+
Pair(1_000_000L, MediaMetadataRetriever.OPTION_CLOSEST_SYNC),
|
|
162
|
+
)
|
|
163
|
+
for ((timeUs, option) in frameAttempts) {
|
|
164
|
+
val image = try {
|
|
165
|
+
retriever.getFrameAtTime(timeUs, option)
|
|
166
|
+
} catch (e: RuntimeException) {
|
|
167
|
+
null
|
|
168
|
+
}
|
|
169
|
+
if (image != null) {
|
|
170
|
+
return image
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
error("Unable to extract video frame from file")
|
|
174
|
+
} finally {
|
|
175
|
+
try {
|
|
176
|
+
retriever.release()
|
|
177
|
+
} catch (e: IOException) {
|
|
178
|
+
// Ignore
|
|
179
|
+
}
|
|
157
180
|
}
|
|
158
|
-
checkNotNull(image) { "File doesn't exist or not supported" }
|
|
159
|
-
return image
|
|
160
181
|
}
|
|
161
182
|
}
|
|
162
183
|
}
|
|
@@ -24,7 +24,7 @@ object AutoVideoCompression {
|
|
|
24
24
|
val actualHeight = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
|
|
25
25
|
val actualWidth = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
|
|
26
26
|
val bitrate = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_BITRATE)
|
|
27
|
-
val frameRate = VideoCompressorHelper.
|
|
27
|
+
val frameRate = VideoCompressorHelper.getSourceFrameRate(metaRetriever)
|
|
28
28
|
if (actualHeight <= 0 || actualWidth <= 0) {
|
|
29
29
|
promise.reject(Throwable("Failed to read the input video dimensions"))
|
|
30
30
|
return
|
|
@@ -12,7 +12,12 @@ data class VideoCompressionProfile(
|
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
object VideoCompressionProfileFactory {
|
|
15
|
+
// Fallback when the source frame rate cannot be detected.
|
|
15
16
|
private const val DEFAULT_FRAME_RATE = 30
|
|
17
|
+
// Hard upper bound. 60 fps covers every modern phone capture (24/25/30/
|
|
18
|
+
// 50/60). Capping at 30 — the previous behaviour — silently halved 60
|
|
19
|
+
// fps recordings and made the output look choppy.
|
|
20
|
+
private const val MAX_FRAME_RATE = 60
|
|
16
21
|
|
|
17
22
|
fun createAuto(
|
|
18
23
|
sourceWidth: Int,
|
|
@@ -99,7 +104,7 @@ object VideoCompressionProfileFactory {
|
|
|
99
104
|
return DEFAULT_FRAME_RATE
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
return sourceFrameRate.coerceIn(1,
|
|
107
|
+
return sourceFrameRate.coerceIn(1, MAX_FRAME_RATE)
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
private fun estimateBitrate(
|
|
@@ -111,20 +116,25 @@ object VideoCompressionProfileFactory {
|
|
|
111
116
|
targetHeight: Int,
|
|
112
117
|
targetFrameRate: Int,
|
|
113
118
|
): Int {
|
|
119
|
+
// WhatsApp-style bitrate envelope. The previous floors/ceilings
|
|
120
|
+
// were ~2-3x larger and produced "compressed" outputs that were
|
|
121
|
+
// still 20-40 MB for short clips. These bands target ~1.5 Mbps at
|
|
122
|
+
// 720p, which matches WhatsApp's typical output size while keeping
|
|
123
|
+
// visual quality acceptable for chat playback.
|
|
114
124
|
val targetLongSide = max(targetWidth, targetHeight)
|
|
115
125
|
val floor = when {
|
|
116
|
-
targetLongSide >= 1920 ->
|
|
117
|
-
targetLongSide >= 1280 ->
|
|
118
|
-
targetLongSide >= 960 ->
|
|
119
|
-
targetLongSide >= 720 ->
|
|
120
|
-
else ->
|
|
126
|
+
targetLongSide >= 1920 -> 2_000_000
|
|
127
|
+
targetLongSide >= 1280 -> 1_200_000
|
|
128
|
+
targetLongSide >= 960 -> 900_000
|
|
129
|
+
targetLongSide >= 720 -> 700_000
|
|
130
|
+
else -> 500_000
|
|
121
131
|
}
|
|
122
132
|
val ceiling = when {
|
|
123
|
-
targetLongSide >= 1920 ->
|
|
124
|
-
targetLongSide >= 1280 ->
|
|
125
|
-
targetLongSide >= 960 ->
|
|
126
|
-
targetLongSide >= 720 ->
|
|
127
|
-
else ->
|
|
133
|
+
targetLongSide >= 1920 -> 3_500_000
|
|
134
|
+
targetLongSide >= 1280 -> 2_000_000
|
|
135
|
+
targetLongSide >= 960 -> 1_500_000
|
|
136
|
+
targetLongSide >= 720 -> 1_200_000
|
|
137
|
+
else -> 900_000
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
if (sourceBitrate <= 0) {
|