Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import android.graphics.Paint
import android.media.ExifInterface
import android.net.Uri
import android.util.Base64
import android.util.Log
import com.facebook.react.bridge.ReactApplicationContext
import com.reactnativecompressor.Utils.MediaCache
import com.reactnativecompressor.Utils.Utils.exifAttributes
Expand All @@ -21,6 +22,8 @@ import java.io.IOException
import java.net.MalformedURLException

object ImageCompressor {
private const val TAG = "ImageCompressor"

fun getRNFileUrl(filePath: String?): String? {
var filePath = filePath
val returnAbleFile = File(filePath)
Expand Down Expand Up @@ -56,25 +59,38 @@ object ImageCompressor {
return BitmapFactory.decodeFile(filePath)
}

fun copyExifInfo(imagePath:String, outputUri:String){
try {
// for copy exif info
val sourceExif = ExifInterface(imagePath)
val compressedExif = ExifInterface(outputUri)
for (tag in exifAttributes) {
val compressedValue = compressedExif.getAttribute(tag)
if(compressedValue==null)
{
val sourceValue = sourceExif.getAttribute(tag)
if (sourceValue != null) {
compressedExif.setAttribute(tag, sourceValue)
/**
* Strip "file://" / "content://" scheme so legacy ExifInterface can open
* the underlying JPEG. ExifInterface(String) only accepts raw filesystem
* paths — passing a URI string makes it fail silently inside the
* try/catch and drops every EXIF tag, including GPS.
*/
private fun normalizeToFilePath(input: String): String {
if (input.startsWith("file://") || input.startsWith("content://")) {
return Uri.parse(input).path ?: input
}
return input
}

fun copyExifInfo(imagePath: String, outputUri: String) {
try {
val sourcePath = normalizeToFilePath(imagePath)
val outPath = normalizeToFilePath(outputUri)
val sourceExif = ExifInterface(sourcePath)
val compressedExif = ExifInterface(outPath)
var copied = 0
for (tag in exifAttributes) {
val sourceValue = sourceExif.getAttribute(tag) ?: continue
if (compressedExif.getAttribute(tag) == null) {
compressedExif.setAttribute(tag, sourceValue)
copied++
}
}
}
compressedExif.saveAttributes()
Log.i(TAG, "copyExifInfo copied $copied tags from $sourcePath -> $outPath")
} catch (e: Exception) {
Log.w(TAG, "copyExifInfo failed for $imagePath", e)
}
compressedExif.saveAttributes()
} catch (e: Exception) {
e.printStackTrace()
}
}

fun encodeImage(imageDataByteArrayOutputStream: ByteArrayOutputStream, isBase64: Boolean, outputExtension: String?,imagePath: String?, reactContext: ReactApplicationContext?): String? {
Expand All @@ -84,10 +100,14 @@ object ImageCompressor {
} else {
val outputUri = generateCacheFilePath(outputExtension!!, reactContext!!)
try {
val fos = FileOutputStream(outputUri)
imageDataByteArrayOutputStream.writeTo(fos)
// Close the stream before ExifInterface re-opens the file so
// the JPEG bytes are fully flushed; otherwise saveAttributes()
// may truncate the in-flight write.
FileOutputStream(outputUri).use { fos ->
imageDataByteArrayOutputStream.writeTo(fos)
}

copyExifInfo(imagePath!!, outputUri)
copyExifInfo(imagePath!!, outputUri)

return getRNFileUrl(outputUri)
} catch (e: Exception) {
Expand Down Expand Up @@ -262,7 +282,7 @@ object ImageCompressor {
if (bitmap == null || imagePath == null) return bitmap

return try {
val exif = ExifInterface(imagePath)
val exif = ExifInterface(normalizeToFilePath(imagePath))
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
val matrix = Matrix()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ object AutoVideoCompression {
val actualHeight = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
val actualWidth = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
val bitrate = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_BITRATE)
val frameRate = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE)
val frameRate = VideoCompressorHelper.getSourceFrameRate(metaRetriever)
if (actualHeight <= 0 || actualWidth <= 0) {
promise.reject(Throwable("Failed to read the input video dimensions"))
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ data class VideoCompressionProfile(
)

object VideoCompressionProfileFactory {
// Fallback when the source frame rate cannot be detected.
private const val DEFAULT_FRAME_RATE = 30
// Hard upper bound. 60 fps covers every modern phone capture (24/25/30/
// 50/60). Capping at 30 — the previous behaviour — silently halved 60
// fps recordings and made the output look choppy.
private const val MAX_FRAME_RATE = 60

fun createAuto(
sourceWidth: Int,
Expand Down Expand Up @@ -99,7 +104,7 @@ object VideoCompressionProfileFactory {
return DEFAULT_FRAME_RATE
}

return sourceFrameRate.coerceIn(1, DEFAULT_FRAME_RATE)
return sourceFrameRate.coerceIn(1, MAX_FRAME_RATE)
}

private fun estimateBitrate(
Expand All @@ -111,20 +116,25 @@ object VideoCompressionProfileFactory {
targetHeight: Int,
targetFrameRate: Int,
): Int {
// WhatsApp-style bitrate envelope. The previous floors/ceilings
// were ~2-3x larger and produced "compressed" outputs that were
// still 20-40 MB for short clips. These bands target ~1.5 Mbps at
// 720p, which matches WhatsApp's typical output size while keeping
// visual quality acceptable for chat playback.
val targetLongSide = max(targetWidth, targetHeight)
val floor = when {
targetLongSide >= 1920 -> 4_000_000
targetLongSide >= 1280 -> 2_200_000
targetLongSide >= 960 -> 1_600_000
targetLongSide >= 720 -> 1_200_000
else -> 850_000
targetLongSide >= 1920 -> 2_000_000
targetLongSide >= 1280 -> 1_200_000
targetLongSide >= 960 -> 900_000
targetLongSide >= 720 -> 700_000
else -> 500_000
}
val ceiling = when {
targetLongSide >= 1920 -> 8_000_000
targetLongSide >= 1280 -> 5_000_000
targetLongSide >= 960 -> 3_500_000
targetLongSide >= 720 -> 2_500_000
else -> 1_500_000
targetLongSide >= 1920 -> 3_500_000
targetLongSide >= 1280 -> 2_000_000
targetLongSide >= 960 -> 1_500_000
targetLongSide >= 720 -> 1_200_000
else -> 900_000
}

if (sourceBitrate <= 0) {
Expand Down
Loading