react-native-compressor 1.16.1 → 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.
- package/android/build.gradle +3 -0
- package/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt +2 -2
- package/android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt +27 -32
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressionProfile.kt +144 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/VideoCompressorClass.kt +6 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt +16 -3
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt +3 -8
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt +37 -17
- package/ios/Utils/Uploader.swift +10 -1
- package/ios/Video/VideoMain.swift +128 -67
- package/package.json +1 -1
package/android/build.gradle
CHANGED
|
@@ -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 =
|
|
26
|
-
val actualWidth =
|
|
27
|
-
val bitrate =
|
|
28
|
-
val
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
val bitrate = metaRetriever
|
|
101
|
-
val
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
115
|
-
|
|
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
|
}
|
package/ios/Utils/Uploader.swift
CHANGED
|
@@ -30,6 +30,7 @@ class Uploader : NSObject, URLSessionTaskDelegate{
|
|
|
30
30
|
var uploadResolvers: [String: RCTPromiseResolveBlock] = [:]
|
|
31
31
|
var uploadRejectors: [String: RCTPromiseRejectBlock] = [:]
|
|
32
32
|
var currentTask: URLSessionDataTask?
|
|
33
|
+
var storage:[Int:Data] = [:]
|
|
33
34
|
private lazy var taskManager = UrlTaskManager()
|
|
34
35
|
|
|
35
36
|
func upload(filePath: String, options: [String: Any], resolve:@escaping RCTPromiseResolveBlock, reject:@escaping RCTPromiseRejectBlock) -> Void {
|
|
@@ -123,6 +124,8 @@ class Uploader : NSObject, URLSessionTaskDelegate{
|
|
|
123
124
|
guard let uuid = session.configuration.identifier else {return}
|
|
124
125
|
guard let reject = uploadRejectors[uuid] else{return}
|
|
125
126
|
guard let resolve = uploadResolvers[uuid] else{return}
|
|
127
|
+
guard let data = self.storage[task.taskIdentifier],
|
|
128
|
+
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {return}
|
|
126
129
|
guard error == nil else {
|
|
127
130
|
reject("failed", "Upload Failed", error)
|
|
128
131
|
uploadRejectors[uuid] = nil
|
|
@@ -136,12 +139,18 @@ class Uploader : NSObject, URLSessionTaskDelegate{
|
|
|
136
139
|
return;
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
let result: [String : Any] = ["status": response.statusCode, "headers": response.allHeaderFields, "body":
|
|
142
|
+
let result: [String : Any] = ["status": response.statusCode, "headers": response.allHeaderFields, "body": json]
|
|
140
143
|
|
|
141
144
|
resolve(result)
|
|
142
145
|
uploadResolvers[uuid] = nil
|
|
146
|
+
self.storage[task.taskIdentifier] = nil
|
|
143
147
|
}
|
|
144
148
|
|
|
149
|
+
|
|
150
|
+
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
|
151
|
+
self.storage[dataTask.taskIdentifier] = data
|
|
152
|
+
}
|
|
153
|
+
|
|
145
154
|
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64)
|
|
146
155
|
{
|
|
147
156
|
guard let uuid = session.configuration.identifier else {return}
|
|
@@ -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
|
-
|
|
107
|
+
let videoURL = URL(string: absoluteVideoPath)
|
|
108
108
|
let fileSize=self.getfileSize(forURL: videoURL!);
|
|
109
|
-
|
|
110
|
-
{
|
|
111
|
-
minimumFileSizeForCompress=options["minimumFileSizeForCompress"] as
|
|
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
|
-
|
|
150
|
-
let
|
|
151
|
-
let
|
|
152
|
-
let
|
|
153
|
-
let
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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",
|