Skip to content

Commit 2a37aaa

Browse files
authored
Merge branch 'main' into copilot/update-docs-and-squash-commits
2 parents 156fe92 + 4b1b288 commit 2a37aaa

13 files changed

Lines changed: 981 additions & 141 deletions

File tree

‎android/src/main/java/com/reactnativecompressor/Image/ImageCompressor.kt‎

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import android.graphics.Paint
99
import android.media.ExifInterface
1010
import android.net.Uri
1111
import android.util.Base64
12+
import android.util.Log
1213
import com.facebook.react.bridge.ReactApplicationContext
1314
import com.reactnativecompressor.Utils.MediaCache
1415
import com.reactnativecompressor.Utils.Utils.exifAttributes
@@ -21,6 +22,8 @@ import java.io.IOException
2122
import java.net.MalformedURLException
2223

2324
object ImageCompressor {
25+
private const val TAG = "ImageCompressor"
26+
2427
fun getRNFileUrl(filePath: String?): String? {
2528
var filePath = filePath
2629
val returnAbleFile = File(filePath)
@@ -56,25 +59,38 @@ object ImageCompressor {
5659
return BitmapFactory.decodeFile(filePath)
5760
}
5861

59-
fun copyExifInfo(imagePath:String, outputUri:String){
60-
try {
61-
// for copy exif info
62-
val sourceExif = ExifInterface(imagePath)
63-
val compressedExif = ExifInterface(outputUri)
64-
for (tag in exifAttributes) {
65-
val compressedValue = compressedExif.getAttribute(tag)
66-
if(compressedValue==null)
67-
{
68-
val sourceValue = sourceExif.getAttribute(tag)
69-
if (sourceValue != null) {
70-
compressedExif.setAttribute(tag, sourceValue)
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+
}
7188
}
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)
7393
}
74-
compressedExif.saveAttributes()
75-
} catch (e: Exception) {
76-
e.printStackTrace()
77-
}
7894
}
7995

8096
fun encodeImage(imageDataByteArrayOutputStream: ByteArrayOutputStream, isBase64: Boolean, outputExtension: String?,imagePath: String?, reactContext: ReactApplicationContext?): String? {
@@ -84,10 +100,14 @@ object ImageCompressor {
84100
} else {
85101
val outputUri = generateCacheFilePath(outputExtension!!, reactContext!!)
86102
try {
87-
val fos = FileOutputStream(outputUri)
88-
imageDataByteArrayOutputStream.writeTo(fos)
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+
}
89109

90-
copyExifInfo(imagePath!!, outputUri)
110+
copyExifInfo(imagePath!!, outputUri)
91111

92112
return getRNFileUrl(outputUri)
93113
} catch (e: Exception) {
@@ -262,7 +282,7 @@ object ImageCompressor {
262282
if (bitmap == null || imagePath == null) return bitmap
263283

264284
return try {
265-
val exif = ExifInterface(imagePath)
285+
val exif = ExifInterface(normalizeToFilePath(imagePath))
266286
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
267287
val matrix = Matrix()
268288

‎android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ object AutoVideoCompression {
2424
val actualHeight = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
2525
val actualWidth = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
2626
val bitrate = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_BITRATE)
27-
val frameRate = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE)
27+
val frameRate = VideoCompressorHelper.getSourceFrameRate(metaRetriever)
2828
if (actualHeight <= 0 || actualWidth <= 0) {
2929
promise.reject(Throwable("Failed to read the input video dimensions"))
3030
return

‎android/src/main/java/com/reactnativecompressor/Video/VideoCompressionProfile.kt‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ data class VideoCompressionProfile(
1212
)
1313

1414
object VideoCompressionProfileFactory {
15+
// Fallback when the source frame rate cannot be detected.
1516
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
1621

1722
fun createAuto(
1823
sourceWidth: Int,
@@ -99,7 +104,7 @@ object VideoCompressionProfileFactory {
99104
return DEFAULT_FRAME_RATE
100105
}
101106

102-
return sourceFrameRate.coerceIn(1, DEFAULT_FRAME_RATE)
107+
return sourceFrameRate.coerceIn(1, MAX_FRAME_RATE)
103108
}
104109

105110
private fun estimateBitrate(
@@ -111,20 +116,25 @@ object VideoCompressionProfileFactory {
111116
targetHeight: Int,
112117
targetFrameRate: Int,
113118
): 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.
114124
val targetLongSide = max(targetWidth, targetHeight)
115125
val floor = when {
116-
targetLongSide >= 1920 -> 4_000_000
117-
targetLongSide >= 1280 -> 2_200_000
118-
targetLongSide >= 960 -> 1_600_000
119-
targetLongSide >= 720 -> 1_200_000
120-
else -> 850_000
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
121131
}
122132
val ceiling = when {
123-
targetLongSide >= 1920 -> 8_000_000
124-
targetLongSide >= 1280 -> 5_000_000
125-
targetLongSide >= 960 -> 3_500_000
126-
targetLongSide >= 720 -> 2_500_000
127-
else -> 1_500_000
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
128138
}
129139

130140
if (sourceBitrate <= 0) {

0 commit comments

Comments
 (0)