react-native-compressor 1.16.2 → 1.17.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.
@@ -67,6 +67,9 @@ android {
67
67
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
68
68
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
69
69
  }
70
+ buildFeatures {
71
+ buildConfig true
72
+ }
70
73
  buildTypes {
71
74
  release {
72
75
  minifyEnabled false
@@ -29,12 +29,12 @@ object Utils {
29
29
  }
30
30
 
31
31
  @JvmStatic
32
- fun compressVideo(srcPath: String, destinationPath: String, resultWidth: Int, resultHeight: Int, videoBitRate: Float, uuid: String,progressDivider:Int, promise: Promise, reactContext: ReactApplicationContext) {
32
+ fun compressVideo(srcPath: String, destinationPath: String, resultWidth: Int, resultHeight: Int, videoBitRate: Float, frameRate: Int, uuid: String,progressDivider:Int, promise: Promise, reactContext: ReactApplicationContext) {
33
33
  val currentVideoCompression = intArrayOf(0)
34
34
  val videoCompressorClass: VideoCompressorClass? = VideoCompressorClass(reactContext);
35
35
  compressorExports[uuid] = videoCompressorClass
36
36
  videoCompressorClass?.start(
37
- srcPath, destinationPath, resultWidth, resultHeight, videoBitRate.toInt(),
37
+ srcPath, destinationPath, resultWidth, resultHeight, videoBitRate.toInt(), frameRate,
38
38
  listener = object : CompressionListener {
39
39
  override fun onProgress(index: Int, percent: Float) {
40
40
  if (percent <= 100) {
@@ -10,7 +10,6 @@ import java.io.File
10
10
 
11
11
  object AutoVideoCompression {
12
12
  fun createCompressionSettings(fileUrl: String?, options: VideoCompressorHelper, promise: Promise, reactContext: ReactApplicationContext?) {
13
- val maxSize = options.maxSize
14
13
  val minimumFileSizeForCompress = options.minimumFileSizeForCompress
15
14
  try {
16
15
  val uri = Uri.parse(fileUrl)
@@ -22,18 +21,33 @@ object AutoVideoCompression {
22
21
  val sizeInMb = sizeInBytes / (1024 * 1024)
23
22
  if (sizeInMb > minimumFileSizeForCompress) {
24
23
  val destinationPath = generateCacheFilePath("mp4", reactContext!!)
25
- val actualHeight = metaRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)!!.toInt()
26
- val actualWidth = metaRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)!!.toInt()
27
- val bitrate = metaRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)!!.toInt()
28
- val scale = if (actualWidth > actualHeight) maxSize / actualWidth else maxSize / actualHeight
29
- val resultWidth = Math.round(actualWidth * Math.min(scale, 1f) / 2) * 2
30
- val resultHeight = Math.round(actualHeight * Math.min(scale, 1f) / 2) * 2
31
- val videoBitRate = makeVideoBitrate(
32
- actualHeight, actualWidth,
33
- bitrate,
34
- resultHeight, resultWidth
35
- ).toFloat()
36
- compressVideo(srcPath!!, destinationPath, resultWidth, resultHeight, videoBitRate, options.uuid!!,options.progressDivider!!, promise, reactContext)
24
+ val actualHeight = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
25
+ val actualWidth = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
26
+ val bitrate = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_BITRATE)
27
+ val frameRate = VideoCompressorHelper.getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE)
28
+ if (actualHeight <= 0 || actualWidth <= 0) {
29
+ promise.reject(Throwable("Failed to read the input video dimensions"))
30
+ return
31
+ }
32
+ val profile = VideoCompressionProfileFactory.createAuto(
33
+ sourceWidth = actualWidth,
34
+ sourceHeight = actualHeight,
35
+ sourceBitrate = bitrate,
36
+ sourceFrameRate = frameRate,
37
+ maxSize = options.maxSize,
38
+ )
39
+ compressVideo(
40
+ srcPath!!,
41
+ destinationPath,
42
+ profile.width,
43
+ profile.height,
44
+ profile.bitrate.toFloat(),
45
+ profile.frameRate,
46
+ options.uuid!!,
47
+ options.progressDivider!!,
48
+ promise,
49
+ reactContext,
50
+ )
37
51
  } else {
38
52
  promise.resolve(fileUrl)
39
53
  }
@@ -41,23 +55,4 @@ object AutoVideoCompression {
41
55
  promise.reject(ex)
42
56
  }
43
57
  }
44
-
45
- fun makeVideoBitrate(originalHeight: Int, originalWidth: Int, originalBitrate: Int, height: Int, width: Int): Int {
46
- val compressFactor = 0.8f
47
- val minCompressFactor = 0.8f
48
- val maxBitrate = 1669000
49
- var remeasuredBitrate = (originalBitrate / Math.min(originalHeight / height.toFloat(), originalWidth / width.toFloat())).toInt()
50
- remeasuredBitrate = (remeasuredBitrate * compressFactor).toInt()
51
- val minBitrate = (getVideoBitrateWithFactor(minCompressFactor) / (1280f * 720f / (width * height))).toInt()
52
- if (originalBitrate < minBitrate) {
53
- return remeasuredBitrate
54
- }
55
- return if (remeasuredBitrate > maxBitrate) {
56
- maxBitrate
57
- } else Math.max(remeasuredBitrate, minBitrate)
58
- }
59
-
60
- private fun getVideoBitrateWithFactor(f: Float): Int {
61
- return (f * 2000f * 1000f * 1.13f).toInt()
62
- }
63
58
  }
@@ -0,0 +1,144 @@
1
+ package com.reactnativecompressor.Video
2
+
3
+ import kotlin.math.max
4
+ import kotlin.math.min
5
+ import kotlin.math.roundToInt
6
+
7
+ data class VideoCompressionProfile(
8
+ val width: Int,
9
+ val height: Int,
10
+ val bitrate: Int,
11
+ val frameRate: Int,
12
+ )
13
+
14
+ object VideoCompressionProfileFactory {
15
+ private const val DEFAULT_FRAME_RATE = 30
16
+
17
+ fun createAuto(
18
+ sourceWidth: Int,
19
+ sourceHeight: Int,
20
+ sourceBitrate: Int,
21
+ sourceFrameRate: Int,
22
+ maxSize: Float,
23
+ ): VideoCompressionProfile {
24
+ val dimensions = scaleWithin(sourceWidth, sourceHeight, maxSize.roundToInt())
25
+ val frameRate = normalizeFrameRate(sourceFrameRate)
26
+ val bitrate = estimateBitrate(
27
+ sourceWidth = sourceWidth,
28
+ sourceHeight = sourceHeight,
29
+ sourceBitrate = sourceBitrate,
30
+ sourceFrameRate = sourceFrameRate,
31
+ targetWidth = dimensions.first,
32
+ targetHeight = dimensions.second,
33
+ targetFrameRate = frameRate,
34
+ )
35
+
36
+ return VideoCompressionProfile(
37
+ width = dimensions.first,
38
+ height = dimensions.second,
39
+ bitrate = bitrate,
40
+ frameRate = frameRate,
41
+ )
42
+ }
43
+
44
+ fun createManual(
45
+ sourceWidth: Int,
46
+ sourceHeight: Int,
47
+ sourceBitrate: Int,
48
+ sourceFrameRate: Int,
49
+ maxSize: Float,
50
+ requestedBitrate: Float,
51
+ ): VideoCompressionProfile {
52
+ val dimensions = scaleWithin(sourceWidth, sourceHeight, maxSize.roundToInt())
53
+ val frameRate = normalizeFrameRate(sourceFrameRate)
54
+ val bitrate = if (requestedBitrate > 0f) {
55
+ requestedBitrate.roundToInt().coerceAtLeast(1)
56
+ } else {
57
+ estimateBitrate(
58
+ sourceWidth = sourceWidth,
59
+ sourceHeight = sourceHeight,
60
+ sourceBitrate = sourceBitrate,
61
+ sourceFrameRate = sourceFrameRate,
62
+ targetWidth = dimensions.first,
63
+ targetHeight = dimensions.second,
64
+ targetFrameRate = frameRate,
65
+ )
66
+ }
67
+
68
+ return VideoCompressionProfile(
69
+ width = dimensions.first,
70
+ height = dimensions.second,
71
+ bitrate = bitrate,
72
+ frameRate = frameRate,
73
+ )
74
+ }
75
+
76
+ fun normalizeDimension(value: Int): Int {
77
+ val positive = value.coerceAtLeast(2)
78
+ return if (positive % 2 == 0) positive else positive - 1
79
+ }
80
+
81
+ private fun scaleWithin(sourceWidth: Int, sourceHeight: Int, requestedMaxSize: Int): Pair<Int, Int> {
82
+ val safeWidth = normalizeDimension(sourceWidth)
83
+ val safeHeight = normalizeDimension(sourceHeight)
84
+ val longSide = max(safeWidth, safeHeight)
85
+ val boundedMaxSize = requestedMaxSize.coerceAtLeast(2)
86
+
87
+ if (longSide <= boundedMaxSize) {
88
+ return Pair(safeWidth, safeHeight)
89
+ }
90
+
91
+ val scale = boundedMaxSize.toFloat() / longSide.toFloat()
92
+ val width = normalizeDimension((safeWidth * scale).roundToInt())
93
+ val height = normalizeDimension((safeHeight * scale).roundToInt())
94
+ return Pair(width, height)
95
+ }
96
+
97
+ private fun normalizeFrameRate(sourceFrameRate: Int): Int {
98
+ if (sourceFrameRate <= 0) {
99
+ return DEFAULT_FRAME_RATE
100
+ }
101
+
102
+ return sourceFrameRate.coerceIn(1, DEFAULT_FRAME_RATE)
103
+ }
104
+
105
+ private fun estimateBitrate(
106
+ sourceWidth: Int,
107
+ sourceHeight: Int,
108
+ sourceBitrate: Int,
109
+ sourceFrameRate: Int,
110
+ targetWidth: Int,
111
+ targetHeight: Int,
112
+ targetFrameRate: Int,
113
+ ): Int {
114
+ val targetLongSide = max(targetWidth, targetHeight)
115
+ 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
121
+ }
122
+ 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
128
+ }
129
+
130
+ if (sourceBitrate <= 0) {
131
+ return floor
132
+ }
133
+
134
+ val sourcePixels = sourceWidth.toLong() * sourceHeight.toLong()
135
+ val targetPixels = targetWidth.toLong() * targetHeight.toLong()
136
+ val pixelRatio = if (sourcePixels == 0L) 1.0 else targetPixels.toDouble() / sourcePixels.toDouble()
137
+ val sourceFps = max(sourceFrameRate, 1)
138
+ val frameRateRatio = targetFrameRate.toDouble() / sourceFps.toDouble()
139
+ val scaledBitrate = (sourceBitrate * pixelRatio * max(frameRateRatio, 0.85)).roundToInt()
140
+ val sourceCap = (sourceBitrate * 0.95).roundToInt().coerceAtLeast(floor)
141
+
142
+ return scaledBitrate.coerceAtLeast(floor).coerceAtMost(min(ceiling, sourceCap))
143
+ }
144
+ }
@@ -24,6 +24,7 @@ class VideoCompressorClass(private val context: ReactApplicationContext) {
24
24
  outputWidth: Int,
25
25
  outputHeight: Int,
26
26
  bitrate: Int,
27
+ frameRate: Int,
27
28
  listener: CompressionListener,
28
29
  ) {
29
30
  val uris = mutableListOf<Uri>()
@@ -36,6 +37,7 @@ class VideoCompressorClass(private val context: ReactApplicationContext) {
36
37
  outputWidth,
37
38
  outputHeight,
38
39
  bitrate,
40
+ frameRate,
39
41
  listener,
40
42
  destPath
41
43
  )
@@ -52,6 +54,7 @@ class VideoCompressorClass(private val context: ReactApplicationContext) {
52
54
  outputWidth: Int,
53
55
  outputHeight: Int,
54
56
  bitrate: Int,
57
+ frameRate: Int,
55
58
  listener: CompressionListener,
56
59
  destPath: String
57
60
  ) {
@@ -74,6 +77,7 @@ class VideoCompressorClass(private val context: ReactApplicationContext) {
74
77
  outputWidth,
75
78
  outputHeight,
76
79
  bitrate,
80
+ frameRate,
77
81
  listener,
78
82
  )
79
83
 
@@ -96,6 +100,7 @@ class VideoCompressorClass(private val context: ReactApplicationContext) {
96
100
  outputWidth: Int,
97
101
  outputHeight: Int,
98
102
  bitrate: Int,
103
+ frameRate: Int,
99
104
  listener: CompressionListener,
100
105
  ): Result = withContext(Dispatchers.Default) {
101
106
  return@withContext compressVideo(
@@ -107,6 +112,7 @@ class VideoCompressorClass(private val context: ReactApplicationContext) {
107
112
  outputWidth,
108
113
  outputHeight,
109
114
  bitrate,
115
+ frameRate,
110
116
  object : CompressionProgressListener {
111
117
  override fun onProgressChanged(index: Int, percent: Float) {
112
118
  listener.onProgress(index, percent)
@@ -54,6 +54,7 @@ object Compressor {
54
54
  outputWidth: Int,
55
55
  outputHeight: Int,
56
56
  outputBitrate: Int,
57
+ outputFrameRate: Int,
57
58
  listener: CompressionProgressListener,
58
59
  ): Result = withContext(Dispatchers.Default) {
59
60
 
@@ -123,6 +124,7 @@ object Compressor {
123
124
  newHeight!!,
124
125
  destination,
125
126
  newBitrate,
127
+ outputFrameRate,
126
128
  streamableFile,
127
129
  false,
128
130
  extractor,
@@ -140,6 +142,7 @@ object Compressor {
140
142
  newHeight: Int,
141
143
  destination: String,
142
144
  newBitrate: Int,
145
+ outputFrameRate: Int,
143
146
  streamableFile: String?,
144
147
  disableAudio: Boolean,
145
148
  extractor: MediaExtractor,
@@ -178,6 +181,7 @@ object Compressor {
178
181
  inputFormat,
179
182
  outputFormat,
180
183
  newBitrate,
184
+ outputFrameRate,
181
185
  )
182
186
 
183
187
  val decoder: MediaCodec
@@ -374,7 +378,7 @@ object Compressor {
374
378
  }
375
379
  }
376
380
 
377
- } catch (exception: Exception) {
381
+ } catch (exception: Throwable) {
378
382
  printException(exception)
379
383
  return Result(id, success = false, failureMessage = exception.message)
380
384
  }
@@ -400,12 +404,14 @@ object Compressor {
400
404
  extractor.release()
401
405
  try {
402
406
  mediaMuxer.finishMovie()
403
- } catch (e: Exception) {
407
+ } catch (e: Throwable) {
404
408
  printException(e)
409
+ return Result(id, success = false, failureMessage = e.message ?: "Failed to finalize compressed video")
405
410
  }
406
411
 
407
- } catch (exception: Exception) {
412
+ } catch (exception: Throwable) {
408
413
  printException(exception)
414
+ return Result(id, success = false, failureMessage = exception.message)
409
415
  }
410
416
 
411
417
  var resultFile = cacheFile
@@ -423,6 +429,13 @@ object Compressor {
423
429
  printException(e)
424
430
  }
425
431
  }
432
+ if (!resultFile.exists() || resultFile.length() <= 32) {
433
+ return Result(
434
+ id,
435
+ success = false,
436
+ failureMessage = "Compressed video output is invalid"
437
+ )
438
+ }
426
439
  return Result(
427
440
  id,
428
441
  success = true,
@@ -70,8 +70,9 @@ object CompressorUtils {
70
70
  inputFormat: MediaFormat,
71
71
  outputFormat: MediaFormat,
72
72
  newBitrate: Int,
73
+ targetFrameRate: Int,
73
74
  ) {
74
- val newFrameRate = getFrameRate(inputFormat)
75
+ val newFrameRate = targetFrameRate.coerceAtLeast(1)
75
76
  val iFrameInterval = getIFrameIntervalRate(inputFormat)
76
77
  outputFormat.apply {
77
78
  setInteger(
@@ -107,12 +108,6 @@ object CompressorUtils {
107
108
  }
108
109
  }
109
110
 
110
- // Get the frame rate from the input format or use a default value
111
- private fun getFrameRate(format: MediaFormat): Int {
112
- return if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) format.getInteger(MediaFormat.KEY_FRAME_RATE)
113
- else 30
114
- }
115
-
116
111
  // Get the I-frame (keyframe) interval from the input format or use a default value
117
112
  private fun getIFrameIntervalRate(format: MediaFormat): Int {
118
113
  return if (format.containsKey(MediaFormat.KEY_I_FRAME_INTERVAL)) format.getInteger(
@@ -172,7 +167,7 @@ object CompressorUtils {
172
167
  /**
173
168
  * Log an exception with a meaningful message.
174
169
  */
175
- fun printException(exception: Exception) {
170
+ fun printException(exception: Throwable) {
176
171
  var message = "An error has occurred!"
177
172
  exception.localizedMessage?.let {
178
173
  message = it
@@ -88,6 +88,16 @@ class VideoCompressorHelper {
88
88
  }
89
89
  return options
90
90
  }
91
+
92
+ fun getMetadataInt(metaRetriever: MediaMetadataRetriever, key: Int): Int {
93
+ return metaRetriever.extractMetadata(key)
94
+ ?.toDoubleOrNull()
95
+ ?.toLong()
96
+ ?.coerceIn(0L, Int.MAX_VALUE.toLong())
97
+ ?.toInt()
98
+ ?: 0
99
+ }
100
+
91
101
  fun VideoCompressManual(fileUrl: String?, options: VideoCompressorHelper, promise: Promise, reactContext: ReactApplicationContext?) {
92
102
  try {
93
103
  val uri = Uri.parse(fileUrl)
@@ -95,24 +105,34 @@ class VideoCompressorHelper {
95
105
  val destinationPath = Utils.generateCacheFilePath("mp4", reactContext!!)
96
106
  val metaRetriever = MediaMetadataRetriever()
97
107
  metaRetriever.setDataSource(srcPath)
98
- var height = metaRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)!!.toInt()
99
- var width = metaRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)!!.toInt()
100
- val bitrate = metaRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)!!.toInt()
101
- val isPortrait = height > width
102
- val maxSize = options.maxSize.toInt()
103
- if (isPortrait && height > maxSize) {
104
- width = (maxSize.toFloat() / height * width).toInt()
105
- height = maxSize
106
- } else if (width > maxSize) {
107
- height = (maxSize.toFloat() / width * height).toInt()
108
- width = maxSize
109
- } else {
110
- if (options.bitrate == 0f) {
111
- options.bitrate = (bitrate * 0.8).toInt().toFloat()
112
- }
108
+ val height = getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
109
+ val width = getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
110
+ val bitrate = getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_BITRATE)
111
+ val frameRate = getMetadataInt(metaRetriever, MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE)
112
+ if (height <= 0 || width <= 0) {
113
+ promise.reject(Throwable("Failed to read the input video dimensions"))
114
+ return
113
115
  }
114
- val videoBitRate = if (options.bitrate > 0) options.bitrate else (height * width * 1.5).toFloat()
115
- Utils.compressVideo(srcPath!!, destinationPath, width, height, videoBitRate, options.uuid!!,options.progressDivider!!, promise, reactContext)
116
+ val profile = VideoCompressionProfileFactory.createManual(
117
+ sourceWidth = width,
118
+ sourceHeight = height,
119
+ sourceBitrate = bitrate,
120
+ sourceFrameRate = frameRate,
121
+ maxSize = options.maxSize,
122
+ requestedBitrate = options.bitrate,
123
+ )
124
+ Utils.compressVideo(
125
+ srcPath!!,
126
+ destinationPath,
127
+ profile.width,
128
+ profile.height,
129
+ profile.bitrate.toFloat(),
130
+ profile.frameRate,
131
+ options.uuid!!,
132
+ options.progressDivider!!,
133
+ promise,
134
+ reactContext,
135
+ )
116
136
  } catch (ex: Exception) {
117
137
  promise.reject(ex)
118
138
  }
@@ -104,11 +104,11 @@ class VideoCompressor {
104
104
 
105
105
  VideoCompressor.getAbsoluteVideoPath(url.absoluteString, options: options) { absoluteVideoPath in
106
106
  var minimumFileSizeForCompress:Double=0.0;
107
- let videoURL = URL(string: absoluteVideoPath)
107
+ let videoURL = URL(string: absoluteVideoPath)
108
108
  let fileSize=self.getfileSize(forURL: videoURL!);
109
- if((options["minimumFileSizeForCompress"]) != nil)
110
- {0
111
- minimumFileSizeForCompress=options["minimumFileSizeForCompress"] as! Double;
109
+ if((options["minimumFileSizeForCompress"]) != nil)
110
+ {
111
+ minimumFileSizeForCompress=(options["minimumFileSizeForCompress"] as? NSNumber)?.doubleValue ?? 0;
112
112
  }
113
113
  if(fileSize>minimumFileSizeForCompress)
114
114
  {
@@ -146,55 +146,128 @@ class VideoCompressor {
146
146
  }
147
147
 
148
148
 
149
- func makeVideoBitrate(originalHeight:Int,originalWidth:Int,originalBitrate:Int,height:Int,width:Int)->Int {
150
- let compressFactor:Float = 0.8
151
- let minCompressFactor:Float = 0.8
152
- let maxBitrate:Int = 1669000
153
- let minValue:Float=min(Float(originalHeight)/Float(height),Float(originalWidth)/Float(width))
154
- var remeasuredBitrate:Int = Int(Float(originalBitrate) / minValue)
155
- remeasuredBitrate = Int(Float(remeasuredBitrate)*compressFactor)
156
- let minBitrate:Int = Int(Float(self.getVideoBitrateWithFactor(f: minCompressFactor)) / (1280 * 720 / Float(width * height)))
157
- if (originalBitrate < minBitrate) {
158
- return remeasuredBitrate;
149
+ struct CompressionProfile {
150
+ let width: Int
151
+ let height: Int
152
+ let bitrate: Int
153
+ let frameRate: Int
154
+ }
155
+
156
+ func normalizeDimension(_ value: CGFloat) -> Int {
157
+ let rounded = max(Int(value.rounded()), 2)
158
+ return rounded % 2 == 0 ? rounded : rounded - 1
159
+ }
160
+
161
+ func normalizeFrameRate(for track: AVAssetTrack) -> Int {
162
+ let nominalFrameRate = Int(track.nominalFrameRate.rounded())
163
+ if nominalFrameRate <= 0 {
164
+ return 30
159
165
  }
160
- if (remeasuredBitrate > maxBitrate) {
161
- return maxBitrate;
166
+
167
+ return min(max(nominalFrameRate, 1), 30)
168
+ }
169
+
170
+ func scaledDimensions(width: CGFloat, height: CGFloat, maxSize: CGFloat) -> (width: Int, height: Int) {
171
+ let safeWidth = normalizeDimension(width)
172
+ let safeHeight = normalizeDimension(height)
173
+ let longSide = max(safeWidth, safeHeight)
174
+ let safeMaxSize = max(Int(maxSize.rounded()), 2)
175
+
176
+ guard longSide > safeMaxSize else {
177
+ return (safeWidth, safeHeight)
178
+ }
179
+
180
+ let scale = CGFloat(safeMaxSize) / CGFloat(longSide)
181
+ return (
182
+ normalizeDimension(CGFloat(safeWidth) * scale),
183
+ normalizeDimension(CGFloat(safeHeight) * scale)
184
+ )
185
+ }
186
+
187
+ func estimateBitrate(
188
+ originalWidth: Int,
189
+ originalHeight: Int,
190
+ originalBitrate: Int,
191
+ originalFrameRate: Int,
192
+ targetWidth: Int,
193
+ targetHeight: Int,
194
+ targetFrameRate: Int
195
+ ) -> Int {
196
+ let targetLongSide = max(targetWidth, targetHeight)
197
+ let floor: Int
198
+ let ceiling: Int
199
+
200
+ switch targetLongSide {
201
+ case 1920...:
202
+ floor = 4_000_000
203
+ ceiling = 8_000_000
204
+ case 1280...1919:
205
+ floor = 2_200_000
206
+ ceiling = 5_000_000
207
+ case 960...1279:
208
+ floor = 1_600_000
209
+ ceiling = 3_500_000
210
+ case 720...959:
211
+ floor = 1_200_000
212
+ ceiling = 2_500_000
213
+ default:
214
+ floor = 850_000
215
+ ceiling = 1_500_000
162
216
  }
163
- return max(remeasuredBitrate, minBitrate);
164
- }
165
- func getVideoBitrateWithFactor(f:Float)->Int {
166
- return Int(f * 2000 * 1000 * 1.13);
167
- }
217
+
218
+ guard originalBitrate > 0 else {
219
+ return floor
220
+ }
221
+
222
+ let originalPixels = max(originalWidth * originalHeight, 1)
223
+ let targetPixels = max(targetWidth * targetHeight, 1)
224
+ let pixelRatio = Double(targetPixels) / Double(originalPixels)
225
+ let safeOriginalFrameRate = max(originalFrameRate, 1)
226
+ let frameRateRatio = Double(targetFrameRate) / Double(safeOriginalFrameRate)
227
+ let scaledBitrate = Int((Double(originalBitrate) * pixelRatio * max(frameRateRatio, 0.85)).rounded())
228
+ let sourceCap = max(Int((Double(originalBitrate) * 0.95).rounded()), floor)
229
+ return min(max(scaledBitrate, floor), min(ceiling, sourceCap))
230
+ }
231
+
232
+ func createCompressionProfile(track: AVAssetTrack, maxSize: CGFloat, requestedBitrate: Int?) -> CompressionProfile {
233
+ let videoSize = track.naturalSize.applying(track.preferredTransform)
234
+ let actualWidth = abs(videoSize.width)
235
+ let actualHeight = abs(videoSize.height)
236
+ let dimensions = scaledDimensions(width: actualWidth, height: actualHeight, maxSize: maxSize)
237
+ let frameRate = normalizeFrameRate(for: track)
238
+ let sourceFrameRate = Int(track.nominalFrameRate.rounded())
239
+ let bitrate = (requestedBitrate != nil && requestedBitrate! > 0) ? requestedBitrate! : estimateBitrate(
240
+ originalWidth: normalizeDimension(actualWidth),
241
+ originalHeight: normalizeDimension(actualHeight),
242
+ originalBitrate: Int(abs(track.estimatedDataRate.rounded())),
243
+ originalFrameRate: sourceFrameRate,
244
+ targetWidth: dimensions.width,
245
+ targetHeight: dimensions.height,
246
+ targetFrameRate: frameRate
247
+ )
248
+
249
+ return CompressionProfile(
250
+ width: dimensions.width,
251
+ height: dimensions.height,
252
+ bitrate: max(bitrate, 1),
253
+ frameRate: frameRate
254
+ )
255
+ }
168
256
 
169
257
  func autoCompressionHelper(url: URL, options: [String: Any], onProgress: @escaping (Float) -> Void, onCompletion: @escaping (URL) -> Void, onFailure: @escaping (Error) -> Void){
170
- let maxSize:Float = options["maxSize"] as! Float;
258
+ let maxSize = (options["maxSize"] as? NSNumber)?.floatValue ?? 640
171
259
  let uuid:String = options["uuid"] as! String
172
260
  let progressDivider=options["progressDivider"] as? Int ?? 0
173
261
 
174
262
  let asset = AVAsset(url: url)
175
- guard asset.tracks.count >= 1 else {
263
+ guard let track = getVideoTrack(asset: asset) else {
176
264
  let error = CompressionError(message: "Invalid video URL, no track found")
177
265
  onFailure(error)
178
266
  return
179
267
  }
180
- let track = getVideoTrack(asset: asset);
181
-
182
- let videoSize = track.naturalSize.applying(track.preferredTransform);
183
- let actualWidth = Float(abs(videoSize.width))
184
- let actualHeight = Float(abs(videoSize.height))
185
-
186
- let bitrate=Float(abs(track.estimatedDataRate));
187
- let scale:Float = actualWidth > actualHeight ? (Float(maxSize) / actualWidth) : (Float(maxSize) / actualHeight);
188
- let resultWidth:Float = round(actualWidth * min(scale, 1) / 2) * 2;
189
- let resultHeight:Float = round(actualHeight * min(scale, 1) / 2) * 2;
268
+ let profile = createCompressionProfile(track: track, maxSize: CGFloat(maxSize), requestedBitrate: nil)
190
269
 
191
- let videoBitRate:Int = self.makeVideoBitrate(
192
- originalHeight: Int(actualHeight), originalWidth: Int(actualWidth),
193
- originalBitrate: Int(bitrate),
194
- height: Int(resultHeight), width: Int(resultWidth)
195
- );
196
-
197
- exportVideoHelper(url: url, asset: asset, bitRate: videoBitRate, resultWidth: resultWidth, resultHeight: resultHeight,uuid: uuid,progressDivider: progressDivider) { progress in
270
+ exportVideoHelper(url: url, asset: asset, bitRate: profile.bitrate, frameRate: profile.frameRate, resultWidth: profile.width, resultHeight: profile.height,uuid: uuid,progressDivider: progressDivider) { progress in
198
271
  onProgress(progress)
199
272
  } onCompletion: { outputURL in
200
273
  onCompletion(outputURL)
@@ -205,36 +278,18 @@ class VideoCompressor {
205
278
 
206
279
  func manualCompressionHelper(url: URL, options: [String: Any], onProgress: @escaping (Float) -> Void, onCompletion: @escaping (URL) -> Void, onFailure: @escaping (Error) -> Void){
207
280
  let uuid:String = options["uuid"] as! String
208
- var bitRate = (options["bitrate"] as? NSNumber)?.floatValue;
281
+ let bitRate = (options["bitrate"] as? NSNumber)?.intValue
209
282
  let progressDivider=options["progressDivider"] as? Int ?? 0
210
283
  let asset = AVAsset(url: url)
211
- guard asset.tracks.count >= 1 else {
284
+ guard let track = getVideoTrack(asset: asset) else {
212
285
  let error = CompressionError(message: "Invalid video URL, no track found")
213
286
  onFailure(error)
214
287
  return
215
288
  }
216
- let track = getVideoTrack(asset: asset);
217
-
218
- let videoSize = track.naturalSize.applying(track.preferredTransform);
219
- var width = Float(abs(videoSize.width))
220
- var height = Float(abs(videoSize.height))
221
- let isPortrait = height > width
222
- let maxSize = (options["maxSize"] as! Float?) ?? Float(1920);
223
- if(isPortrait && height > maxSize){
224
- width = (maxSize/height)*width
225
- height = maxSize
226
- }else if(width > maxSize){
227
- height = (maxSize/width)*height
228
- width = maxSize
229
- }
230
- else
231
- {
232
- bitRate=bitRate ?? Float(abs(track.estimatedDataRate))*0.8
233
- }
234
-
235
- let videoBitRate = bitRate ?? height*width*1.5
289
+ let maxSize = (options["maxSize"] as? NSNumber)?.floatValue ?? Float(1920)
290
+ let profile = createCompressionProfile(track: track, maxSize: CGFloat(maxSize), requestedBitrate: bitRate)
236
291
 
237
- exportVideoHelper(url: url, asset: asset, bitRate: Int(videoBitRate), resultWidth: width, resultHeight: height,uuid: uuid,progressDivider: progressDivider) { progress in
292
+ exportVideoHelper(url: url, asset: asset, bitRate: profile.bitrate, frameRate: profile.frameRate, resultWidth: profile.width, resultHeight: profile.height,uuid: uuid,progressDivider: progressDivider) { progress in
238
293
  onProgress(progress)
239
294
  } onCompletion: { outputURL in
240
295
  onCompletion(outputURL)
@@ -243,7 +298,7 @@ class VideoCompressor {
243
298
  }
244
299
  }
245
300
 
246
- func exportVideoHelper(url: URL,asset: AVAsset, bitRate: Int,resultWidth:Float,resultHeight:Float,uuid:String,progressDivider: Int, onProgress: @escaping (Float) -> Void, onCompletion: @escaping (URL) -> Void, onFailure: @escaping (Error) -> Void){
301
+ func exportVideoHelper(url: URL,asset: AVAsset, bitRate: Int, frameRate: Int, resultWidth:Int,resultHeight:Int,uuid:String,progressDivider: Int, onProgress: @escaping (Float) -> Void, onCompletion: @escaping (URL) -> Void, onFailure: @escaping (Error) -> Void){
247
302
  var currentVideoCompression:Int=0
248
303
 
249
304
  var tmpURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
@@ -258,13 +313,15 @@ class VideoCompressor {
258
313
  let compressionDict: [String: Any] = [
259
314
  AVVideoAverageBitRateKey: bitRate,
260
315
  AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
316
+ AVVideoExpectedSourceFrameRateKey: frameRate,
317
+ AVVideoAverageNonDroppableFrameRateKey: frameRate,
261
318
  ]
262
319
  exporter.optimizeForNetworkUse = true;
263
320
  exporter.videoOutputConfiguration = [
264
321
  AVVideoCodecKey: AVVideoCodecType.h264,
265
322
  AVVideoWidthKey: resultWidth,
266
323
  AVVideoHeightKey: resultHeight,
267
- AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill,
324
+ AVVideoScalingModeKey: AVVideoScalingModeResizeAspect,
268
325
  AVVideoCompressionPropertiesKey: compressionDict
269
326
  ]
270
327
  exporter.audioOutputConfiguration = [
@@ -284,6 +341,7 @@ class VideoCompressor {
284
341
  }
285
342
  }, completionHandler: { result in
286
343
  currentVideoCompression=0;
344
+ self.compressorExports[uuid] = nil
287
345
  switch exporter.status {
288
346
  case .completed:
289
347
  onCompletion(exporter.outputURL!)
@@ -293,14 +351,17 @@ class VideoCompressor {
293
351
  onFailure(error)
294
352
  break
295
353
  default:
296
- onCompletion(url)
354
+ onFailure(exporter.error ?? CompressionError(message: "Compression failed"))
297
355
  break
298
356
  }
299
357
  })
300
358
  }
301
359
 
302
- func getVideoTrack(asset: AVAsset) -> AVAssetTrack {
360
+ func getVideoTrack(asset: AVAsset) -> AVAssetTrack? {
303
361
  let tracks = asset.tracks(withMediaType: AVMediaType.video)
362
+ guard tracks.count >= 1 else {
363
+ return nil
364
+ }
304
365
  return tracks[0];
305
366
  }
306
367
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-compressor",
3
- "version": "1.16.2",
3
+ "version": "1.17.0",
4
4
  "description": "Compress Image, Video, and Audio same like Whatsapp & Auto/Manual Compression | Background Upload | Download File | Create Video Thumbnail",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",