react-native-compressor 1.8.0 → 1.8.2
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/README.md +22 -1
- package/android/build.gradle +6 -2
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt +2 -2
- package/android/src/main/java/com/reactnativecompressor/CompressorModule.kt +12 -0
- package/android/src/main/java/com/reactnativecompressor/Utils/Uploader.kt +11 -11
- package/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt +42 -36
- package/android/src/main/java/com/reactnativecompressor/Utils/createVideoThumbnail.kt +158 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/CompressionInterface.kt +73 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/VideoCompressorClass.kt +148 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt +562 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/Atoms.kt +55 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt +196 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/NumbersUtils.kt +47 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/StreamableVideo.kt +209 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/InputSurface.kt +128 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/MP4Builder.kt +388 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Mdat.kt +74 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Mp4Movie.kt +54 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/OutputSurface.kt +102 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Result.kt +9 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Sample.kt +3 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/TextureRenderer.kt +214 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Track.kt +286 -0
- package/android/src/oldarch/CompressorSpec.kt +2 -0
- package/ios/Compressor.mm +9 -0
- package/ios/CompressorManager.swift +11 -0
- package/ios/Utils/CreateVideoThumbnail.swift +111 -0
- package/lib/commonjs/Spec/NativeCompressor.js.map +1 -1
- package/lib/commonjs/index.js +14 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/utils/index.js +12 -1
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/module/Spec/NativeCompressor.js.map +1 -1
- package/lib/module/index.js +4 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/index.js +7 -0
- package/lib/module/utils/index.js.map +1 -1
- package/lib/typescript/Spec/NativeCompressor.d.ts +8 -0
- package/lib/typescript/Spec/NativeCompressor.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +15 -3
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/utils/index.d.ts +16 -1
- package/lib/typescript/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Spec/NativeCompressor.ts +11 -0
- package/src/index.tsx +6 -0
- package/src/utils/index.tsx +28 -1
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
package com.reactnativecompressor.Video.VideoCompressor.compressor
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.media.MediaCodec
|
|
5
|
+
import android.media.MediaExtractor
|
|
6
|
+
import android.media.MediaFormat
|
|
7
|
+
import android.media.MediaMetadataRetriever
|
|
8
|
+
import android.net.Uri
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import android.util.Log
|
|
11
|
+
import com.reactnativecompressor.Video.VideoCompressor.CompressionProgressListener
|
|
12
|
+
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.findTrack
|
|
13
|
+
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.hasQTI
|
|
14
|
+
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.prepareVideoHeight
|
|
15
|
+
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.prepareVideoWidth
|
|
16
|
+
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.printException
|
|
17
|
+
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.setOutputFileParameters
|
|
18
|
+
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.setUpMP4Movie
|
|
19
|
+
import com.reactnativecompressor.Video.VideoCompressor.utils.StreamableVideo
|
|
20
|
+
import com.reactnativecompressor.Video.VideoCompressor.video.InputSurface
|
|
21
|
+
import com.reactnativecompressor.Video.VideoCompressor.video.MP4Builder
|
|
22
|
+
import com.reactnativecompressor.Video.VideoCompressor.video.OutputSurface
|
|
23
|
+
import com.reactnativecompressor.Video.VideoCompressor.video.Result
|
|
24
|
+
import kotlinx.coroutines.Dispatchers
|
|
25
|
+
import kotlinx.coroutines.withContext
|
|
26
|
+
import java.io.File
|
|
27
|
+
import java.nio.ByteBuffer
|
|
28
|
+
|
|
29
|
+
object Compressor {
|
|
30
|
+
|
|
31
|
+
// Minimum bitrate for compression (2Mbps)
|
|
32
|
+
private const val MIN_BITRATE = 2000000
|
|
33
|
+
|
|
34
|
+
// MIME type for H.264 Advanced Video Coding
|
|
35
|
+
private const val MIME_TYPE = "video/avc"
|
|
36
|
+
|
|
37
|
+
// Default timeout for MediaCodec operations
|
|
38
|
+
private const val MEDIACODEC_TIMEOUT_DEFAULT = 100L
|
|
39
|
+
|
|
40
|
+
// Error message for invalid bitrate
|
|
41
|
+
private const val INVALID_BITRATE =
|
|
42
|
+
"The provided bitrate is smaller than what is needed for compression, " +
|
|
43
|
+
"try to set isMinBitRateEnabled to false"
|
|
44
|
+
|
|
45
|
+
// Flag to check if compression is running
|
|
46
|
+
var isRunning = true
|
|
47
|
+
|
|
48
|
+
suspend fun compressVideo(
|
|
49
|
+
index: Int,
|
|
50
|
+
context: Context,
|
|
51
|
+
srcUri: Uri,
|
|
52
|
+
destination: String,
|
|
53
|
+
streamableFile: String?,
|
|
54
|
+
outputWidth: Int,
|
|
55
|
+
outputHeight: Int,
|
|
56
|
+
outputBitrate: Int,
|
|
57
|
+
listener: CompressionProgressListener,
|
|
58
|
+
): Result = withContext(Dispatchers.Default) {
|
|
59
|
+
|
|
60
|
+
// Initialize MediaExtractor and MediaMetadataRetriever
|
|
61
|
+
val extractor = MediaExtractor()
|
|
62
|
+
val mediaMetadataRetriever = MediaMetadataRetriever()
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// Set the data source for the MediaMetadataRetriever
|
|
66
|
+
mediaMetadataRetriever.setDataSource(context, srcUri)
|
|
67
|
+
} catch (exception: IllegalArgumentException) {
|
|
68
|
+
printException(exception)
|
|
69
|
+
return@withContext Result(
|
|
70
|
+
index,
|
|
71
|
+
success = false,
|
|
72
|
+
failureMessage = "${exception.message}"
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
runCatching {
|
|
77
|
+
// Set the data source for the MediaExtractor
|
|
78
|
+
extractor.setDataSource(context, srcUri, null)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Retrieve video metadata
|
|
82
|
+
val height: Double = prepareVideoHeight(mediaMetadataRetriever)
|
|
83
|
+
val width: Double = prepareVideoWidth(mediaMetadataRetriever)
|
|
84
|
+
val rotationData = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
|
|
85
|
+
val bitrateData = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)
|
|
86
|
+
val durationData = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
|
87
|
+
|
|
88
|
+
// Check if any metadata is missing
|
|
89
|
+
if (rotationData.isNullOrEmpty() || bitrateData.isNullOrEmpty() || durationData.isNullOrEmpty()) {
|
|
90
|
+
// Exit execution
|
|
91
|
+
return@withContext Result(
|
|
92
|
+
index,
|
|
93
|
+
success = false,
|
|
94
|
+
failureMessage = "Failed to extract video metadata, please try again"
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var rotation = rotationData.toInt()
|
|
99
|
+
val duration = durationData.toLong() * 1000
|
|
100
|
+
|
|
101
|
+
// Handle new bitrate value
|
|
102
|
+
val newBitrate: Int = outputBitrate
|
|
103
|
+
|
|
104
|
+
// Handle new width and height values
|
|
105
|
+
var (newWidth, newHeight) = Pair(outputWidth, outputHeight)
|
|
106
|
+
|
|
107
|
+
// Handle rotation values and swapping height and width if needed
|
|
108
|
+
rotation = when (rotation) {
|
|
109
|
+
90, 270 -> {
|
|
110
|
+
val tempHeight = newHeight
|
|
111
|
+
newHeight = newWidth
|
|
112
|
+
newWidth = tempHeight
|
|
113
|
+
0
|
|
114
|
+
}
|
|
115
|
+
180 -> 0
|
|
116
|
+
else -> rotation
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Start video compression
|
|
120
|
+
return@withContext start(
|
|
121
|
+
index,
|
|
122
|
+
newWidth!!,
|
|
123
|
+
newHeight!!,
|
|
124
|
+
destination,
|
|
125
|
+
newBitrate,
|
|
126
|
+
streamableFile,
|
|
127
|
+
false,
|
|
128
|
+
extractor,
|
|
129
|
+
listener,
|
|
130
|
+
duration,
|
|
131
|
+
rotation
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Function to start video compression
|
|
136
|
+
@Suppress("DEPRECATION")
|
|
137
|
+
private fun start(
|
|
138
|
+
id: Int,
|
|
139
|
+
newWidth: Int,
|
|
140
|
+
newHeight: Int,
|
|
141
|
+
destination: String,
|
|
142
|
+
newBitrate: Int,
|
|
143
|
+
streamableFile: String?,
|
|
144
|
+
disableAudio: Boolean,
|
|
145
|
+
extractor: MediaExtractor,
|
|
146
|
+
compressionProgressListener: CompressionProgressListener,
|
|
147
|
+
duration: Long,
|
|
148
|
+
rotation: Int
|
|
149
|
+
): Result {
|
|
150
|
+
// Check if newWidth and newHeight are valid
|
|
151
|
+
if (newWidth != 0 && newHeight != 0) {
|
|
152
|
+
// Create a cache file for the compressed video
|
|
153
|
+
val cacheFile = File(destination)
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// MediaCodec accesses encoder and decoder components and processes the new video
|
|
157
|
+
// input to generate a compressed/smaller size video
|
|
158
|
+
val bufferInfo = MediaCodec.BufferInfo()
|
|
159
|
+
|
|
160
|
+
// Setup mp4 movie
|
|
161
|
+
val movie = setUpMP4Movie(rotation, cacheFile)
|
|
162
|
+
|
|
163
|
+
// MediaMuxer outputs MP4 in this app
|
|
164
|
+
val mediaMuxer = MP4Builder().createMovie(movie)
|
|
165
|
+
|
|
166
|
+
// Start with the video track
|
|
167
|
+
val videoIndex = findTrack(extractor, isVideo = true)
|
|
168
|
+
|
|
169
|
+
extractor.selectTrack(videoIndex)
|
|
170
|
+
extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
|
|
171
|
+
val inputFormat = extractor.getTrackFormat(videoIndex)
|
|
172
|
+
|
|
173
|
+
val outputFormat: MediaFormat =
|
|
174
|
+
MediaFormat.createVideoFormat(MIME_TYPE, newWidth, newHeight)
|
|
175
|
+
|
|
176
|
+
// Set output format
|
|
177
|
+
setOutputFileParameters(
|
|
178
|
+
inputFormat,
|
|
179
|
+
outputFormat,
|
|
180
|
+
newBitrate,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
val decoder: MediaCodec
|
|
184
|
+
|
|
185
|
+
// Check if QTI hardware acceleration is available
|
|
186
|
+
val hasQTI = hasQTI()
|
|
187
|
+
|
|
188
|
+
// Prepare the video encoder
|
|
189
|
+
val encoder = prepareEncoder(outputFormat, hasQTI)
|
|
190
|
+
|
|
191
|
+
val inputSurface: InputSurface
|
|
192
|
+
val outputSurface: OutputSurface
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
var inputDone = false
|
|
196
|
+
var outputDone = false
|
|
197
|
+
|
|
198
|
+
var videoTrackIndex = -5
|
|
199
|
+
|
|
200
|
+
inputSurface = InputSurface(encoder.createInputSurface())
|
|
201
|
+
inputSurface.makeCurrent()
|
|
202
|
+
// Move to executing state
|
|
203
|
+
encoder.start()
|
|
204
|
+
|
|
205
|
+
outputSurface = OutputSurface()
|
|
206
|
+
|
|
207
|
+
decoder = prepareDecoder(inputFormat, outputSurface)
|
|
208
|
+
|
|
209
|
+
// Move to executing state
|
|
210
|
+
decoder.start()
|
|
211
|
+
|
|
212
|
+
while (!outputDone) {
|
|
213
|
+
if (!inputDone) {
|
|
214
|
+
|
|
215
|
+
val index = extractor.sampleTrackIndex
|
|
216
|
+
|
|
217
|
+
if (index == videoIndex) {
|
|
218
|
+
val inputBufferIndex =
|
|
219
|
+
decoder.dequeueInputBuffer(MEDIACODEC_TIMEOUT_DEFAULT)
|
|
220
|
+
if (inputBufferIndex >= 0) {
|
|
221
|
+
val inputBuffer = decoder.getInputBuffer(inputBufferIndex)
|
|
222
|
+
val chunkSize = extractor.readSampleData(inputBuffer!!, 0)
|
|
223
|
+
when {
|
|
224
|
+
chunkSize < 0 -> {
|
|
225
|
+
|
|
226
|
+
decoder.queueInputBuffer(
|
|
227
|
+
inputBufferIndex,
|
|
228
|
+
0,
|
|
229
|
+
0,
|
|
230
|
+
0L,
|
|
231
|
+
MediaCodec.BUFFER_FLAG_END_OF_STREAM
|
|
232
|
+
)
|
|
233
|
+
inputDone = true
|
|
234
|
+
}
|
|
235
|
+
else -> {
|
|
236
|
+
|
|
237
|
+
decoder.queueInputBuffer(
|
|
238
|
+
inputBufferIndex,
|
|
239
|
+
0,
|
|
240
|
+
chunkSize,
|
|
241
|
+
extractor.sampleTime,
|
|
242
|
+
0
|
|
243
|
+
)
|
|
244
|
+
extractor.advance()
|
|
245
|
+
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
} else if (index == -1) { //end of file
|
|
251
|
+
val inputBufferIndex =
|
|
252
|
+
decoder.dequeueInputBuffer(MEDIACODEC_TIMEOUT_DEFAULT)
|
|
253
|
+
if (inputBufferIndex >= 0) {
|
|
254
|
+
decoder.queueInputBuffer(
|
|
255
|
+
inputBufferIndex,
|
|
256
|
+
0,
|
|
257
|
+
0,
|
|
258
|
+
0L,
|
|
259
|
+
MediaCodec.BUFFER_FLAG_END_OF_STREAM
|
|
260
|
+
)
|
|
261
|
+
inputDone = true
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
var decoderOutputAvailable = true
|
|
267
|
+
var encoderOutputAvailable = true
|
|
268
|
+
|
|
269
|
+
loop@ while (decoderOutputAvailable || encoderOutputAvailable) {
|
|
270
|
+
|
|
271
|
+
if (!isRunning) {
|
|
272
|
+
dispose(
|
|
273
|
+
videoIndex,
|
|
274
|
+
decoder,
|
|
275
|
+
encoder,
|
|
276
|
+
inputSurface,
|
|
277
|
+
outputSurface,
|
|
278
|
+
extractor
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
compressionProgressListener.onProgressCancelled(id)
|
|
282
|
+
return Result(
|
|
283
|
+
id,
|
|
284
|
+
success = false,
|
|
285
|
+
failureMessage = "The compression has stopped!"
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
//Encoder
|
|
290
|
+
val encoderStatus =
|
|
291
|
+
encoder.dequeueOutputBuffer(bufferInfo, MEDIACODEC_TIMEOUT_DEFAULT)
|
|
292
|
+
|
|
293
|
+
when {
|
|
294
|
+
encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER -> encoderOutputAvailable =
|
|
295
|
+
false
|
|
296
|
+
encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
|
297
|
+
val newFormat = encoder.outputFormat
|
|
298
|
+
if (videoTrackIndex == -5)
|
|
299
|
+
videoTrackIndex = mediaMuxer.addTrack(newFormat, false)
|
|
300
|
+
}
|
|
301
|
+
encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
|
|
302
|
+
// ignore this status
|
|
303
|
+
}
|
|
304
|
+
encoderStatus < 0 -> throw RuntimeException("unexpected result from encoder.dequeueOutputBuffer: $encoderStatus")
|
|
305
|
+
else -> {
|
|
306
|
+
val encodedData = encoder.getOutputBuffer(encoderStatus)
|
|
307
|
+
?: throw RuntimeException("encoderOutputBuffer $encoderStatus was null")
|
|
308
|
+
|
|
309
|
+
if (bufferInfo.size > 1) {
|
|
310
|
+
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
|
|
311
|
+
mediaMuxer.writeSampleData(
|
|
312
|
+
videoTrackIndex,
|
|
313
|
+
encodedData, bufferInfo, false
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
outputDone =
|
|
320
|
+
bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0
|
|
321
|
+
encoder.releaseOutputBuffer(encoderStatus, false)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (encoderStatus != MediaCodec.INFO_TRY_AGAIN_LATER) continue@loop
|
|
325
|
+
|
|
326
|
+
//Decoder
|
|
327
|
+
val decoderStatus =
|
|
328
|
+
decoder.dequeueOutputBuffer(bufferInfo, MEDIACODEC_TIMEOUT_DEFAULT)
|
|
329
|
+
when {
|
|
330
|
+
decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER -> decoderOutputAvailable =
|
|
331
|
+
false
|
|
332
|
+
decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
|
|
333
|
+
// ignore this status
|
|
334
|
+
}
|
|
335
|
+
decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
|
336
|
+
// ignore this status
|
|
337
|
+
}
|
|
338
|
+
decoderStatus < 0 -> throw RuntimeException("unexpected result from decoder.dequeueOutputBuffer: $decoderStatus")
|
|
339
|
+
else -> {
|
|
340
|
+
val doRender = bufferInfo.size != 0
|
|
341
|
+
|
|
342
|
+
decoder.releaseOutputBuffer(decoderStatus, doRender)
|
|
343
|
+
if (doRender) {
|
|
344
|
+
var errorWait = false
|
|
345
|
+
try {
|
|
346
|
+
outputSurface.awaitNewImage()
|
|
347
|
+
} catch (e: Exception) {
|
|
348
|
+
errorWait = true
|
|
349
|
+
Log.e(
|
|
350
|
+
"Compressor",
|
|
351
|
+
e.message ?: "Compression failed at swapping buffer"
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!errorWait) {
|
|
356
|
+
outputSurface.drawImage()
|
|
357
|
+
|
|
358
|
+
inputSurface.setPresentationTime(bufferInfo.presentationTimeUs * 1000)
|
|
359
|
+
|
|
360
|
+
compressionProgressListener.onProgressChanged(
|
|
361
|
+
id,
|
|
362
|
+
bufferInfo.presentationTimeUs.toFloat() / duration.toFloat() * 100
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
inputSurface.swapBuffers()
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
369
|
+
decoderOutputAvailable = false
|
|
370
|
+
encoder.signalEndOfInputStream()
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
} catch (exception: Exception) {
|
|
378
|
+
printException(exception)
|
|
379
|
+
return Result(id, success = false, failureMessage = exception.message)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Release resources
|
|
383
|
+
dispose(
|
|
384
|
+
videoIndex,
|
|
385
|
+
decoder,
|
|
386
|
+
encoder,
|
|
387
|
+
inputSurface,
|
|
388
|
+
outputSurface,
|
|
389
|
+
extractor
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
// Process audio if necessary
|
|
393
|
+
processAudio(
|
|
394
|
+
mediaMuxer = mediaMuxer,
|
|
395
|
+
bufferInfo = bufferInfo,
|
|
396
|
+
disableAudio = disableAudio,
|
|
397
|
+
extractor
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
extractor.release()
|
|
401
|
+
try {
|
|
402
|
+
mediaMuxer.finishMovie()
|
|
403
|
+
} catch (e: Exception) {
|
|
404
|
+
printException(e)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
} catch (exception: Exception) {
|
|
408
|
+
printException(exception)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
var resultFile = cacheFile
|
|
412
|
+
|
|
413
|
+
// Process the result and create a streamable video if requested
|
|
414
|
+
streamableFile?.let {
|
|
415
|
+
try {
|
|
416
|
+
val result = StreamableVideo.start(`in` = cacheFile, out = File(it))
|
|
417
|
+
resultFile = File(it)
|
|
418
|
+
if (result && cacheFile.exists()) {
|
|
419
|
+
cacheFile.delete()
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
} catch (e: Exception) {
|
|
423
|
+
printException(e)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return Result(
|
|
427
|
+
id,
|
|
428
|
+
success = true,
|
|
429
|
+
failureMessage = null,
|
|
430
|
+
size = resultFile.length(),
|
|
431
|
+
resultFile.path
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return Result(
|
|
436
|
+
id,
|
|
437
|
+
success = false,
|
|
438
|
+
failureMessage = "Something went wrong, please try again"
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Function to process audio
|
|
443
|
+
private fun processAudio(
|
|
444
|
+
mediaMuxer: MP4Builder,
|
|
445
|
+
bufferInfo: MediaCodec.BufferInfo,
|
|
446
|
+
disableAudio: Boolean,
|
|
447
|
+
extractor: MediaExtractor
|
|
448
|
+
) {
|
|
449
|
+
val audioIndex = findTrack(extractor, isVideo = false)
|
|
450
|
+
if (audioIndex >= 0 && !disableAudio) {
|
|
451
|
+
extractor.selectTrack(audioIndex)
|
|
452
|
+
val audioFormat = extractor.getTrackFormat(audioIndex)
|
|
453
|
+
val muxerTrackIndex = mediaMuxer.addTrack(audioFormat, true)
|
|
454
|
+
var maxBufferSize = audioFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
|
|
455
|
+
|
|
456
|
+
if (maxBufferSize <= 0) {
|
|
457
|
+
maxBufferSize = 64 * 1024
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
var buffer: ByteBuffer = ByteBuffer.allocateDirect(maxBufferSize)
|
|
461
|
+
if (Build.VERSION.SDK_INT >= 28) {
|
|
462
|
+
val size = extractor.sampleSize
|
|
463
|
+
if (size > maxBufferSize) {
|
|
464
|
+
maxBufferSize = (size + 1024).toInt()
|
|
465
|
+
buffer = ByteBuffer.allocateDirect(maxBufferSize)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
var inputDone = false
|
|
469
|
+
extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
|
|
470
|
+
|
|
471
|
+
while (!inputDone) {
|
|
472
|
+
val index = extractor.sampleTrackIndex
|
|
473
|
+
if (index == audioIndex) {
|
|
474
|
+
bufferInfo.size = extractor.readSampleData(buffer, 0)
|
|
475
|
+
|
|
476
|
+
if (bufferInfo.size >= 0) {
|
|
477
|
+
bufferInfo.apply {
|
|
478
|
+
presentationTimeUs = extractor.sampleTime
|
|
479
|
+
offset = 0
|
|
480
|
+
flags = MediaCodec.BUFFER_FLAG_KEY_FRAME
|
|
481
|
+
}
|
|
482
|
+
mediaMuxer.writeSampleData(muxerTrackIndex, buffer, bufferInfo, true)
|
|
483
|
+
extractor.advance()
|
|
484
|
+
|
|
485
|
+
} else {
|
|
486
|
+
bufferInfo.size = 0
|
|
487
|
+
inputDone = true
|
|
488
|
+
}
|
|
489
|
+
} else if (index == -1) {
|
|
490
|
+
inputDone = true
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
extractor.unselectTrack(audioIndex)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Function to prepare the video encoder
|
|
498
|
+
private fun prepareEncoder(outputFormat: MediaFormat, hasQTI: Boolean): MediaCodec {
|
|
499
|
+
|
|
500
|
+
// This seems to cause an issue with certain phones
|
|
501
|
+
// val encoderName = MediaCodecList(REGULAR_CODECS).findEncoderForFormat(outputFormat)
|
|
502
|
+
// val encoder: MediaCodec = MediaCodec.createByCodecName(encoderName)
|
|
503
|
+
// Log.i("encoderName", encoder.name)
|
|
504
|
+
// c2.qti.avc.encoder results in a corrupted .mp4 video that does not play in
|
|
505
|
+
// Mac and iphones
|
|
506
|
+
val encoder = if (hasQTI) {
|
|
507
|
+
MediaCodec.createByCodecName("c2.android.avc.encoder")
|
|
508
|
+
} else {
|
|
509
|
+
MediaCodec.createEncoderByType(MIME_TYPE)
|
|
510
|
+
}
|
|
511
|
+
encoder.configure(
|
|
512
|
+
outputFormat, null, null,
|
|
513
|
+
MediaCodec.CONFIGURE_FLAG_ENCODE
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
return encoder
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Function to prepare the video decoder
|
|
520
|
+
private fun prepareDecoder(
|
|
521
|
+
inputFormat: MediaFormat,
|
|
522
|
+
outputSurface: OutputSurface,
|
|
523
|
+
): MediaCodec {
|
|
524
|
+
// This seems to cause an issue with certain phones
|
|
525
|
+
// val decoderName =
|
|
526
|
+
// MediaCodecList(REGULAR_CODECS).findDecoderForFormat(inputFormat)
|
|
527
|
+
// val decoder = MediaCodec.createByCodecName(decoderName)
|
|
528
|
+
// Log.i("decoderName", decoder.name)
|
|
529
|
+
|
|
530
|
+
// val decoder = if (hasQTI) {
|
|
531
|
+
// MediaCodec.createByCodecName("c2.android.avc.decoder")
|
|
532
|
+
//} else {
|
|
533
|
+
|
|
534
|
+
val decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME)!!)
|
|
535
|
+
//}
|
|
536
|
+
|
|
537
|
+
decoder.configure(inputFormat, outputSurface.getSurface(), null, 0)
|
|
538
|
+
|
|
539
|
+
return decoder
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Function to release resources
|
|
543
|
+
private fun dispose(
|
|
544
|
+
videoIndex: Int,
|
|
545
|
+
decoder: MediaCodec,
|
|
546
|
+
encoder: MediaCodec,
|
|
547
|
+
inputSurface: InputSurface,
|
|
548
|
+
outputSurface: OutputSurface,
|
|
549
|
+
extractor: MediaExtractor
|
|
550
|
+
) {
|
|
551
|
+
extractor.unselectTrack(videoIndex)
|
|
552
|
+
|
|
553
|
+
decoder.stop()
|
|
554
|
+
decoder.release()
|
|
555
|
+
|
|
556
|
+
encoder.stop()
|
|
557
|
+
encoder.release()
|
|
558
|
+
|
|
559
|
+
inputSurface.release()
|
|
560
|
+
outputSurface.release()
|
|
561
|
+
}
|
|
562
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.reactnativecompressor.Video.VideoCompressor.data
|
|
2
|
+
|
|
3
|
+
import java.nio.ByteBuffer
|
|
4
|
+
import java.nio.ByteOrder
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Converts a four-character code (FOURCC) represented as a byte array into an integer.
|
|
8
|
+
* FOURCC is an identifier for video codecs, compression formats, and color/pixel formats in media files.
|
|
9
|
+
* Each FOURCC code is a 32-bit value stored in 4 bytes.
|
|
10
|
+
*/
|
|
11
|
+
fun fourCcToInt(byteArray: ByteArray): Int {
|
|
12
|
+
// The bytes in the byteArray are ordered from most significant to least significant.
|
|
13
|
+
return ByteBuffer.wrap(byteArray).order(ByteOrder.BIG_ENDIAN).int
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Constants for commonly used FOURCC codes in media file formats:
|
|
17
|
+
|
|
18
|
+
// Unused space available in the file.
|
|
19
|
+
val FREE_ATOM = fourCcToInt(byteArrayOf('f'.code.toByte(), 'r'.code.toByte(), 'e'.code.toByte(), 'e'.code.toByte()))
|
|
20
|
+
|
|
21
|
+
// Junk data in the file.
|
|
22
|
+
val JUNK_ATOM = fourCcToInt(byteArrayOf('j'.code.toByte(), 'u'.code.toByte(), 'n'.code.toByte(), 'k'.code.toByte()))
|
|
23
|
+
|
|
24
|
+
// Media data containing samples like video frames and audio groups.
|
|
25
|
+
val MDAT_ATOM = fourCcToInt(byteArrayOf('m'.code.toByte(), 'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte()))
|
|
26
|
+
|
|
27
|
+
// Metadata about the movie, including the number and type of tracks and sample data location.
|
|
28
|
+
val MOOV_ATOM = fourCcToInt(byteArrayOf('m'.code.toByte(), 'o'.code.toByte(), 'o'.code.toByte(), 'v'.code.toByte()))
|
|
29
|
+
|
|
30
|
+
// Reference to movie preview data.
|
|
31
|
+
val PNOT_ATOM = fourCcToInt(byteArrayOf('p'.code.toByte(), 'n'.code.toByte(), 'o'.code.toByte(), 't'.code.toByte()))
|
|
32
|
+
|
|
33
|
+
// Unused space available in the file.
|
|
34
|
+
val SKIP_ATOM = fourCcToInt(byteArrayOf('s'.code.toByte(), 'k'.code.toByte(), 'i'.code.toByte(), 'p'.code.toByte()))
|
|
35
|
+
|
|
36
|
+
// Reserved space that can be overwritten by an extended size field.
|
|
37
|
+
val WIDE_ATOM = fourCcToInt(byteArrayOf('w'.code.toByte(), 'i'.code.toByte(), 'd'.code.toByte(), 'e'.code.toByte()))
|
|
38
|
+
|
|
39
|
+
// Picture atom for graphics data.
|
|
40
|
+
val PICT_ATOM = fourCcToInt(byteArrayOf('P'.code.toByte(), 'I'.code.toByte(), 'C'.code.toByte(), 'T'.code.toByte()))
|
|
41
|
+
|
|
42
|
+
// File type compatibility atom that identifies the file type.
|
|
43
|
+
val FTYP_ATOM = fourCcToInt(byteArrayOf('f'.code.toByte(), 't'.code.toByte(), 'y'.code.toByte(), 'p'.code.toByte()))
|
|
44
|
+
|
|
45
|
+
// Universally unique identifier atom.
|
|
46
|
+
val UUID_ATOM = fourCcToInt(byteArrayOf('u'.code.toByte(), 'u'.code.toByte(), 'i'.code.toByte(), 'd'.code.toByte()))
|
|
47
|
+
|
|
48
|
+
// Compressed movie atom.
|
|
49
|
+
val CMOV_ATOM = fourCcToInt(byteArrayOf('c'.code.toByte(), 'm'.code.toByte(), 'o'.code.toByte(), 'v'.code.toByte()))
|
|
50
|
+
|
|
51
|
+
// Sample table chunk offset atom.
|
|
52
|
+
val STCO_ATOM = fourCcToInt(byteArrayOf('s'.code.toByte(), 't'.code.toByte(), 'c'.code.toByte(), 'o'.code.toByte()))
|
|
53
|
+
|
|
54
|
+
// 64-bit chunk offset atom.
|
|
55
|
+
val CO64_ATOM = fourCcToInt(byteArrayOf('c'.code.toByte(), 'o'.code.toByte(), '6'.code.toByte(), '4'.code.toByte()))
|