react-native-compressor 1.18.2 → 2.0.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/CMakeLists.txt +27 -0
- package/android/build.gradle +56 -58
- package/android/gradle.properties +5 -5
- package/android/src/main/AndroidManifestNew.xml +3 -0
- package/android/src/main/cpp/cpp-adapter.cpp +8 -0
- package/android/src/main/java/com/margelo/nitro/compressor/HybridCompressor.kt +213 -0
- package/android/src/main/java/com/reactnativecompressor/Image/ImageCompressor.kt +41 -21
- package/android/src/main/java/com/reactnativecompressor/NitroCompressorPackage.kt +30 -0
- package/android/src/main/java/com/reactnativecompressor/NitroPromiseAdapter.kt +55 -0
- package/android/src/main/java/com/reactnativecompressor/Utils/EventEmitterHandler.kt +49 -45
- package/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt +1 -1
- package/android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt +1 -1
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressionProfile.kt +21 -11
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt +311 -76
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt +96 -5
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/LocationExtractor.kt +397 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/LocationBox.kt +47 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/MP4Builder.kt +39 -7
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Mp4Movie.kt +7 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/OutputSurface.kt +30 -4
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt +28 -2
- package/ios/Audio/FormatConverter/FormatConverter+Compressed.swift +1 -1
- package/ios/Audio/FormatConverter/FormatConverter+Utilities.swift +1 -1
- package/ios/Audio/FormatConverter/FormatConverter.swift +5 -3
- package/ios/HybridCompressor.swift +247 -0
- package/ios/Image/ImageCompressor.swift +8 -2
- package/ios/Utils/EventEmitterHandler.swift +56 -34
- package/ios/Video/NextLevelSessionExporter.swift +43 -8
- package/ios/Video/VideoMain.swift +44 -14
- package/lib/commonjs/Audio/index.js +1 -1
- package/lib/commonjs/Audio/index.js.map +1 -1
- package/lib/commonjs/Image/index.js +2 -22
- package/lib/commonjs/Image/index.js.map +1 -1
- package/lib/commonjs/Main.js +8 -8
- package/lib/commonjs/Main.js.map +1 -1
- package/lib/commonjs/Video/index.js +25 -65
- package/lib/commonjs/Video/index.js.map +1 -1
- package/lib/commonjs/specs/Compressor.nitro.js +6 -0
- package/lib/commonjs/specs/Compressor.nitro.js.map +1 -0
- package/lib/commonjs/utils/Downloader.js +8 -24
- package/lib/commonjs/utils/Downloader.js.map +1 -1
- package/lib/commonjs/utils/Uploader.js +3 -18
- package/lib/commonjs/utils/Uploader.js.map +1 -1
- package/lib/commonjs/utils/helpers.js +18 -1
- package/lib/commonjs/utils/helpers.js.map +1 -1
- package/lib/commonjs/utils/index.js +13 -13
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/module/Audio/index.js +2 -2
- package/lib/module/Audio/index.js.map +1 -1
- package/lib/module/Image/index.js +3 -23
- package/lib/module/Image/index.js.map +1 -1
- package/lib/module/Main.js +9 -9
- package/lib/module/Main.js.map +1 -1
- package/lib/module/Video/index.js +26 -66
- package/lib/module/Video/index.js.map +1 -1
- package/lib/module/specs/Compressor.nitro.js +4 -0
- package/lib/module/specs/Compressor.nitro.js.map +1 -0
- package/lib/module/utils/Downloader.js +10 -26
- package/lib/module/utils/Downloader.js.map +1 -1
- package/lib/module/utils/Uploader.js +5 -20
- package/lib/module/utils/Uploader.js.map +1 -1
- package/lib/module/utils/helpers.js +16 -0
- package/lib/module/utils/helpers.js.map +1 -1
- package/lib/module/utils/index.js +2 -1
- package/lib/module/utils/index.js.map +1 -1
- package/lib/typescript/src/Image/index.d.ts.map +1 -1
- package/lib/typescript/src/Main.d.ts +2 -1
- package/lib/typescript/src/Main.d.ts.map +1 -1
- package/lib/typescript/src/Video/index.d.ts +1 -1
- package/lib/typescript/src/Video/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/Compressor.nitro.d.ts +44 -0
- package/lib/typescript/src/specs/Compressor.nitro.d.ts.map +1 -0
- package/lib/typescript/src/utils/Downloader.d.ts.map +1 -1
- package/lib/typescript/src/utils/Uploader.d.ts.map +1 -1
- package/lib/typescript/src/utils/helpers.d.ts +7 -0
- package/lib/typescript/src/utils/helpers.d.ts.map +1 -1
- package/lib/typescript/src/utils/index.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroCompressor+autolinking.cmake +81 -0
- package/nitrogen/generated/android/NitroCompressor+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroCompressorOnLoad.cpp +60 -0
- package/nitrogen/generated/android/NitroCompressorOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridCompressorSpec.cpp +293 -0
- package/nitrogen/generated/android/c++/JHybridCompressorSpec.hpp +78 -0
- package/nitrogen/generated/android/c++/JVideoThumbnailResult.hpp +73 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/HybridCompressorSpec.kt +141 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/NitroCompressorOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/VideoThumbnailResult.kt +71 -0
- package/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.hpp +204 -0
- package/nitrogen/generated/ios/react_native_compressor+autolinking.rb +62 -0
- package/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.cpp +89 -0
- package/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.hpp +337 -0
- package/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Umbrella.hpp +51 -0
- package/nitrogen/generated/ios/react_native_compressorAutolinking.mm +33 -0
- package/nitrogen/generated/ios/react_native_compressorAutolinking.swift +26 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_VideoThumbnailResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_AnyMap_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridCompressorSpec.swift +70 -0
- package/nitrogen/generated/ios/swift/HybridCompressorSpec_cxx.swift +493 -0
- package/nitrogen/generated/ios/swift/VideoThumbnailResult.swift +49 -0
- package/nitrogen/generated/shared/c++/HybridCompressorSpec.cpp +36 -0
- package/nitrogen/generated/shared/c++/HybridCompressorSpec.hpp +83 -0
- package/nitrogen/generated/shared/c++/VideoThumbnailResult.hpp +99 -0
- package/package.json +8 -12
- package/react-native-compressor.podspec +6 -24
- package/src/Audio/index.tsx +2 -2
- package/src/Image/index.tsx +3 -26
- package/src/Main.tsx +14 -16
- package/src/Video/index.tsx +25 -77
- package/src/specs/Compressor.nitro.ts +56 -0
- package/src/utils/Downloader.tsx +8 -27
- package/src/utils/Uploader.tsx +12 -24
- package/src/utils/helpers.ts +18 -0
- package/src/utils/index.tsx +3 -2
- package/android/src/main/java/com/reactnativecompressor/CompressorModule.kt +0 -180
- package/android/src/main/java/com/reactnativecompressor/CompressorPackage.kt +0 -34
- package/android/src/newarch/CompressorSpec.kt +0 -5
- package/android/src/oldarch/CompressorSpec.kt +0 -36
- package/ios/Compressor-Bridging-Header.h +0 -6
- package/ios/Compressor.h +0 -5
- package/ios/Compressor.mm +0 -82
- package/ios/CompressorManager.swift +0 -117
- package/lib/commonjs/Spec/NativeCompressor.js +0 -9
- package/lib/commonjs/Spec/NativeCompressor.js.map +0 -1
- package/lib/module/Spec/NativeCompressor.js +0 -5
- package/lib/module/Spec/NativeCompressor.js.map +0 -1
- package/lib/typescript/src/Spec/NativeCompressor.d.ts +0 -30
- package/lib/typescript/src/Spec/NativeCompressor.d.ts.map +0 -1
- package/src/Spec/NativeCompressor.ts +0 -41
|
@@ -2,6 +2,8 @@ package com.reactnativecompressor.Video.VideoCompressor.compressor
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.media.MediaCodec
|
|
5
|
+
import android.media.MediaCodecInfo
|
|
6
|
+
import android.media.MediaCodecList
|
|
5
7
|
import android.media.MediaExtractor
|
|
6
8
|
import android.media.MediaFormat
|
|
7
9
|
import android.media.MediaMetadataRetriever
|
|
@@ -9,6 +11,7 @@ import android.net.Uri
|
|
|
9
11
|
import android.os.Build
|
|
10
12
|
import android.util.Log
|
|
11
13
|
import com.reactnativecompressor.Video.VideoCompressor.CompressionProgressListener
|
|
14
|
+
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.ensureDecodableVideoFormat
|
|
12
15
|
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.findTrack
|
|
13
16
|
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.hasQTI
|
|
14
17
|
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.prepareVideoHeight
|
|
@@ -16,6 +19,7 @@ import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.pre
|
|
|
16
19
|
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.printException
|
|
17
20
|
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.setOutputFileParameters
|
|
18
21
|
import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.setUpMP4Movie
|
|
22
|
+
import com.reactnativecompressor.Video.VideoCompressor.utils.LocationExtractor
|
|
19
23
|
import com.reactnativecompressor.Video.VideoCompressor.utils.StreamableVideo
|
|
20
24
|
import com.reactnativecompressor.Video.VideoCompressor.video.InputSurface
|
|
21
25
|
import com.reactnativecompressor.Video.VideoCompressor.video.MP4Builder
|
|
@@ -97,6 +101,29 @@ object Compressor {
|
|
|
97
101
|
val rotationData = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
|
|
98
102
|
val bitrateData = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)
|
|
99
103
|
val durationData = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
|
104
|
+
// ISO 6709 string (e.g. "+37.4220-122.0840/"). Forwarded into the
|
|
105
|
+
// output udta/©xyz box so GPS metadata survives the rewrite.
|
|
106
|
+
//
|
|
107
|
+
// Some Samsung firmwares (S10 / Android 12) place "©xyz" in the
|
|
108
|
+
// per-track udta, or use a 'loci' box, or iTunes-style meta/keys+ilst.
|
|
109
|
+
// MediaMetadataRetriever only reads moov/udta/©xyz — returning null
|
|
110
|
+
// (or empty) and dropping GPS. Fall back to a raw MP4 walker that
|
|
111
|
+
// scans the whole file for every known location encoding.
|
|
112
|
+
val retrievedLocation =
|
|
113
|
+
mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
|
|
114
|
+
val locationData = if (!retrievedLocation.isNullOrEmpty()) {
|
|
115
|
+
retrievedLocation
|
|
116
|
+
} else {
|
|
117
|
+
LocationExtractor.extract(context, srcUri)
|
|
118
|
+
}
|
|
119
|
+
// Never log the resolved ISO 6709 string — it is the user's exact GPS
|
|
120
|
+
// coordinates. Log only presence and which mechanism resolved it.
|
|
121
|
+
val locationSource = when {
|
|
122
|
+
!retrievedLocation.isNullOrEmpty() -> "retriever"
|
|
123
|
+
!locationData.isNullOrEmpty() -> "extractor"
|
|
124
|
+
else -> "none"
|
|
125
|
+
}
|
|
126
|
+
Log.i("Compressor", "source location resolved: hasLocation=${!locationData.isNullOrEmpty()} source=$locationSource")
|
|
100
127
|
|
|
101
128
|
// Check if any metadata is missing
|
|
102
129
|
if (rotationData.isNullOrEmpty() || bitrateData.isNullOrEmpty() || durationData.isNullOrEmpty()) {
|
|
@@ -142,7 +169,8 @@ object Compressor {
|
|
|
142
169
|
extractor,
|
|
143
170
|
listener,
|
|
144
171
|
duration,
|
|
145
|
-
rotation
|
|
172
|
+
rotation,
|
|
173
|
+
locationData,
|
|
146
174
|
)
|
|
147
175
|
}
|
|
148
176
|
|
|
@@ -160,31 +188,50 @@ object Compressor {
|
|
|
160
188
|
extractor: MediaExtractor,
|
|
161
189
|
compressionProgressListener: CompressionProgressListener,
|
|
162
190
|
duration: Long,
|
|
163
|
-
rotation: Int
|
|
191
|
+
rotation: Int,
|
|
192
|
+
location: String?,
|
|
164
193
|
): Result {
|
|
165
194
|
// Check if newWidth and newHeight are valid
|
|
166
195
|
if (newWidth != 0 && newHeight != 0) {
|
|
167
196
|
// Create a cache file for the compressed video
|
|
168
197
|
val cacheFile = File(destination)
|
|
169
198
|
|
|
199
|
+
// Hoisted so the outer catch can close the muxer even though the val
|
|
200
|
+
// below is scoped to the try. Stays null until createMovie() succeeds.
|
|
201
|
+
var muxer: MP4Builder? = null
|
|
202
|
+
|
|
170
203
|
try {
|
|
171
204
|
// MediaCodec accesses encoder and decoder components and processes the new video
|
|
172
205
|
// input to generate a compressed/smaller size video
|
|
173
206
|
val bufferInfo = MediaCodec.BufferInfo()
|
|
174
207
|
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// Start with the video track
|
|
208
|
+
// Resolve the source video track and its format BEFORE allocating the
|
|
209
|
+
// muxer, encoder or EGL surfaces. Dolby Vision profile 5 has no HEVC
|
|
210
|
+
// base layer and cannot be transcoded; rejecting it here — instead of
|
|
211
|
+
// inside prepareDecoder, after the muxer file stream, encoder and EGL
|
|
212
|
+
// surfaces are already live — avoids leaking those resources on bail-out.
|
|
182
213
|
val videoIndex = findTrack(extractor, isVideo = true)
|
|
183
214
|
|
|
184
215
|
extractor.selectTrack(videoIndex)
|
|
185
216
|
extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
|
|
186
217
|
val inputFormat = extractor.getTrackFormat(videoIndex)
|
|
187
218
|
|
|
219
|
+
if (isUnsupportedDolbyVision(inputFormat)) {
|
|
220
|
+
runCatching { extractor.release() }
|
|
221
|
+
return Result(
|
|
222
|
+
id,
|
|
223
|
+
success = false,
|
|
224
|
+
failureMessage = "Dolby Vision profile 5 has no HEVC base layer; cannot transcode"
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Setup mp4 movie
|
|
229
|
+
val movie = setUpMP4Movie(rotation, cacheFile, location)
|
|
230
|
+
|
|
231
|
+
// MediaMuxer outputs MP4 in this app
|
|
232
|
+
val mediaMuxer = MP4Builder().createMovie(movie)
|
|
233
|
+
muxer = mediaMuxer
|
|
234
|
+
|
|
188
235
|
val outputFormat: MediaFormat =
|
|
189
236
|
MediaFormat.createVideoFormat(MIME_TYPE, newWidth, newHeight)
|
|
190
237
|
|
|
@@ -196,16 +243,31 @@ object Compressor {
|
|
|
196
243
|
outputFrameRate,
|
|
197
244
|
)
|
|
198
245
|
|
|
199
|
-
val decoder: MediaCodec
|
|
200
|
-
|
|
201
246
|
// Check if QTI hardware acceleration is available
|
|
202
247
|
val hasQTI = hasQTI()
|
|
203
248
|
|
|
204
|
-
// Prepare the video encoder
|
|
205
|
-
|
|
249
|
+
// Prepare the video encoder. If the encoder rejects the throughput-tuned
|
|
250
|
+
// format at configure() time, prepareEncoder reconfigures using this
|
|
251
|
+
// baseline format (same params, no VBR/priority/operating-rate keys).
|
|
252
|
+
val encoder = prepareEncoder(outputFormat, hasQTI) {
|
|
253
|
+
MediaFormat.createVideoFormat(MIME_TYPE, newWidth, newHeight).also {
|
|
254
|
+
setOutputFileParameters(
|
|
255
|
+
inputFormat,
|
|
256
|
+
it,
|
|
257
|
+
newBitrate,
|
|
258
|
+
outputFrameRate,
|
|
259
|
+
applyThroughputTuning = false,
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
206
263
|
|
|
207
|
-
|
|
208
|
-
|
|
264
|
+
// Track pipeline handles as they come up so a failure mid-setup
|
|
265
|
+
// (EGL/GL init, decoder configure, encoder.start) releases whatever was
|
|
266
|
+
// already created instead of leaking it. The encoder above is always
|
|
267
|
+
// non-null by this point.
|
|
268
|
+
var decoderRef: MediaCodec? = null
|
|
269
|
+
var inputSurfaceRef: InputSurface? = null
|
|
270
|
+
var outputSurfaceRef: OutputSurface? = null
|
|
209
271
|
|
|
210
272
|
try {
|
|
211
273
|
var inputDone = false
|
|
@@ -213,32 +275,58 @@ object Compressor {
|
|
|
213
275
|
|
|
214
276
|
var videoTrackIndex = -5
|
|
215
277
|
|
|
216
|
-
|
|
278
|
+
// Frame dropping: when source fps is higher than target output fps,
|
|
279
|
+
// skip decoded frames whose PTS falls before the next target slot.
|
|
280
|
+
// Saves GL render + encoder work proportional to the drop ratio
|
|
281
|
+
// (e.g. 60fps → 30fps cuts pipeline work roughly in half).
|
|
282
|
+
//
|
|
283
|
+
// Only enable dropping when the source frame rate is reliably
|
|
284
|
+
// higher than the target. If the source advertises 30 fps and the
|
|
285
|
+
// target is 30 fps, even tiny PTS jitter can push a frame just
|
|
286
|
+
// before its slot, get it dropped, and turn 30 fps output into
|
|
287
|
+
// 20 fps — the choppy playback users reported.
|
|
288
|
+
val sourceFrameRate: Int = if (inputFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) {
|
|
289
|
+
inputFormat.getInteger(MediaFormat.KEY_FRAME_RATE)
|
|
290
|
+
} else 0
|
|
291
|
+
val shouldDropFrames = outputFrameRate > 0 &&
|
|
292
|
+
sourceFrameRate > 0 &&
|
|
293
|
+
outputFrameRate < sourceFrameRate
|
|
294
|
+
val targetFrameIntervalUs: Long =
|
|
295
|
+
if (shouldDropFrames) 1_000_000L / outputFrameRate else 0L
|
|
296
|
+
var nextTargetPtsUs: Long = 0L
|
|
297
|
+
|
|
298
|
+
val inputSurface = InputSurface(encoder.createInputSurface())
|
|
299
|
+
inputSurfaceRef = inputSurface
|
|
217
300
|
inputSurface.makeCurrent()
|
|
218
301
|
// Move to executing state
|
|
219
302
|
encoder.start()
|
|
220
303
|
|
|
221
|
-
outputSurface = OutputSurface()
|
|
304
|
+
val outputSurface = OutputSurface()
|
|
305
|
+
outputSurfaceRef = outputSurface
|
|
222
306
|
|
|
223
|
-
decoder = prepareDecoder(inputFormat, outputSurface)
|
|
307
|
+
val decoder = prepareDecoder(inputFormat, outputSurface)
|
|
308
|
+
decoderRef = decoder
|
|
224
309
|
|
|
225
310
|
// Move to executing state
|
|
226
311
|
decoder.start()
|
|
227
312
|
|
|
228
313
|
while (!outputDone) {
|
|
229
314
|
if (!inputDone) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
315
|
+
// Feed the decoder until it has no free input slots or the
|
|
316
|
+
// extractor is empty. HW codecs typically have 4-8 input
|
|
317
|
+
// slots; queuing only one sample per outer iteration starves
|
|
318
|
+
// the pipeline and forces serial decode-render-encode.
|
|
319
|
+
feedLoop@ while (!inputDone) {
|
|
320
|
+
val index = extractor.sampleTrackIndex
|
|
321
|
+
|
|
322
|
+
if (index == videoIndex) {
|
|
323
|
+
val inputBufferIndex =
|
|
324
|
+
decoder.dequeueInputBuffer(0L)
|
|
325
|
+
if (inputBufferIndex < 0) break@feedLoop
|
|
237
326
|
val inputBuffer = decoder.getInputBuffer(inputBufferIndex)
|
|
238
327
|
val chunkSize = extractor.readSampleData(inputBuffer!!, 0)
|
|
239
328
|
when {
|
|
240
329
|
chunkSize < 0 -> {
|
|
241
|
-
|
|
242
330
|
decoder.queueInputBuffer(
|
|
243
331
|
inputBufferIndex,
|
|
244
332
|
0,
|
|
@@ -249,7 +337,6 @@ object Compressor {
|
|
|
249
337
|
inputDone = true
|
|
250
338
|
}
|
|
251
339
|
else -> {
|
|
252
|
-
|
|
253
340
|
decoder.queueInputBuffer(
|
|
254
341
|
inputBufferIndex,
|
|
255
342
|
0,
|
|
@@ -258,15 +345,12 @@ object Compressor {
|
|
|
258
345
|
0
|
|
259
346
|
)
|
|
260
347
|
extractor.advance()
|
|
261
|
-
|
|
262
348
|
}
|
|
263
349
|
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
decoder.dequeueInputBuffer(MEDIACODEC_TIMEOUT_DEFAULT)
|
|
269
|
-
if (inputBufferIndex >= 0) {
|
|
350
|
+
} else if (index == -1) { //end of file
|
|
351
|
+
val inputBufferIndex =
|
|
352
|
+
decoder.dequeueInputBuffer(0L)
|
|
353
|
+
if (inputBufferIndex < 0) break@feedLoop
|
|
270
354
|
decoder.queueInputBuffer(
|
|
271
355
|
inputBufferIndex,
|
|
272
356
|
0,
|
|
@@ -275,6 +359,9 @@ object Compressor {
|
|
|
275
359
|
MediaCodec.BUFFER_FLAG_END_OF_STREAM
|
|
276
360
|
)
|
|
277
361
|
inputDone = true
|
|
362
|
+
} else {
|
|
363
|
+
// Different track type at head of extractor (audio etc.).
|
|
364
|
+
break@feedLoop
|
|
278
365
|
}
|
|
279
366
|
}
|
|
280
367
|
}
|
|
@@ -353,7 +440,27 @@ object Compressor {
|
|
|
353
440
|
}
|
|
354
441
|
decoderStatus < 0 -> throw RuntimeException("unexpected result from decoder.dequeueOutputBuffer: $decoderStatus")
|
|
355
442
|
else -> {
|
|
356
|
-
val
|
|
443
|
+
val isEos = (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
|
|
444
|
+
var doRender = bufferInfo.size != 0 && !isEos
|
|
445
|
+
|
|
446
|
+
// Drop frames whose PTS falls before the next target slot.
|
|
447
|
+
// Anchor the next slot to the ideal grid (previous slot +
|
|
448
|
+
// interval) instead of to the actual PTS — anchoring to PTS
|
|
449
|
+
// lets source-side jitter compound into extra drops, which
|
|
450
|
+
// collapses the output frame rate well below the target.
|
|
451
|
+
if (doRender && targetFrameIntervalUs > 0L) {
|
|
452
|
+
if (bufferInfo.presentationTimeUs < nextTargetPtsUs) {
|
|
453
|
+
doRender = false
|
|
454
|
+
} else {
|
|
455
|
+
nextTargetPtsUs += targetFrameIntervalUs
|
|
456
|
+
// Snap forward when the source skips past a slot
|
|
457
|
+
// (gap, seek, very low source fps) so the gate doesn't
|
|
458
|
+
// burst-emit every following frame.
|
|
459
|
+
if (bufferInfo.presentationTimeUs >= nextTargetPtsUs) {
|
|
460
|
+
nextTargetPtsUs = bufferInfo.presentationTimeUs + targetFrameIntervalUs
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
357
464
|
|
|
358
465
|
decoder.releaseOutputBuffer(decoderStatus, doRender)
|
|
359
466
|
if (doRender) {
|
|
@@ -392,16 +499,32 @@ object Compressor {
|
|
|
392
499
|
|
|
393
500
|
} catch (exception: Throwable) {
|
|
394
501
|
printException(exception)
|
|
502
|
+
// Release whatever was initialized before the failure. Setup errors
|
|
503
|
+
// (EGL/GL init, decoder configure, encoder.start) and in-loop throws
|
|
504
|
+
// land here; without this the encoder + EGL surfaces would leak and
|
|
505
|
+
// break the next compression. dispose() tolerates the null handles
|
|
506
|
+
// that occur when the failure happens mid-setup.
|
|
507
|
+
dispose(
|
|
508
|
+
videoIndex,
|
|
509
|
+
decoderRef,
|
|
510
|
+
encoder,
|
|
511
|
+
inputSurfaceRef,
|
|
512
|
+
outputSurfaceRef,
|
|
513
|
+
extractor
|
|
514
|
+
)
|
|
515
|
+
// finishMovie() never runs on this path, so close the MP4Builder
|
|
516
|
+
// streams explicitly or the output file handle leaks.
|
|
517
|
+
mediaMuxer.close()
|
|
395
518
|
return Result(id, success = false, failureMessage = exception.message)
|
|
396
519
|
}
|
|
397
520
|
|
|
398
521
|
// Release resources
|
|
399
522
|
dispose(
|
|
400
523
|
videoIndex,
|
|
401
|
-
|
|
524
|
+
decoderRef,
|
|
402
525
|
encoder,
|
|
403
|
-
|
|
404
|
-
|
|
526
|
+
inputSurfaceRef,
|
|
527
|
+
outputSurfaceRef,
|
|
405
528
|
extractor
|
|
406
529
|
)
|
|
407
530
|
|
|
@@ -418,18 +541,27 @@ object Compressor {
|
|
|
418
541
|
mediaMuxer.finishMovie()
|
|
419
542
|
} catch (e: Throwable) {
|
|
420
543
|
printException(e)
|
|
544
|
+
// finishMovie() may throw before it closes its own streams; close
|
|
545
|
+
// them here so a finalize failure doesn't leak the file handle.
|
|
546
|
+
mediaMuxer.close()
|
|
421
547
|
return Result(id, success = false, failureMessage = e.message ?: "Failed to finalize compressed video")
|
|
422
548
|
}
|
|
423
549
|
|
|
424
550
|
} catch (exception: Throwable) {
|
|
425
551
|
printException(exception)
|
|
552
|
+
// Covers throws after the inner pipeline closed (e.g. processAudio,
|
|
553
|
+
// extractor.release) where the MP4Builder is still open. close() is
|
|
554
|
+
// idempotent, so calling it after a successful finishMovie() is a no-op.
|
|
555
|
+
muxer?.close()
|
|
426
556
|
return Result(id, success = false, failureMessage = exception.message)
|
|
427
557
|
}
|
|
428
558
|
|
|
429
559
|
var resultFile = cacheFile
|
|
430
560
|
|
|
431
561
|
try {
|
|
432
|
-
// Keep default outputs browser-compatible by moving the
|
|
562
|
+
// Keep default outputs browser/progressive-playback compatible by moving the
|
|
563
|
+
// MP4 moov atom in front of the media data. This runs for every output; when
|
|
564
|
+
// no explicit streamableFile is requested, the rewritten copy replaces cacheFile.
|
|
433
565
|
val targetFile = streamableFile?.let { File(it) } ?: getStreamableOutputFile(cacheFile)
|
|
434
566
|
val outputFile = if (targetFile.absolutePath == cacheFile.absolutePath) {
|
|
435
567
|
getStreamableOutputFile(cacheFile)
|
|
@@ -547,25 +679,109 @@ object Compressor {
|
|
|
547
679
|
}
|
|
548
680
|
|
|
549
681
|
// Function to prepare the video encoder
|
|
550
|
-
private fun prepareEncoder(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
//
|
|
556
|
-
// c2.qti.avc.encoder
|
|
557
|
-
|
|
558
|
-
|
|
682
|
+
private fun prepareEncoder(
|
|
683
|
+
outputFormat: MediaFormat,
|
|
684
|
+
hasQTI: Boolean,
|
|
685
|
+
baselineFormatProvider: () -> MediaFormat,
|
|
686
|
+
): MediaCodec {
|
|
687
|
+
// Prefer hardware AVC encoder while skipping known-broken QTI codec that
|
|
688
|
+
// produces files unplayable on Mac/iOS (c2.qti.avc.encoder).
|
|
689
|
+
val encoder = pickAvcEncoder(outputFormat, hasQTI)
|
|
690
|
+
try {
|
|
691
|
+
encoder.configure(
|
|
692
|
+
outputFormat, null, null,
|
|
693
|
+
MediaCodec.CONFIGURE_FLAG_ENCODE
|
|
694
|
+
)
|
|
695
|
+
Log.i("Compressor", "encoder selected: ${encoder.name}")
|
|
696
|
+
return encoder
|
|
697
|
+
} catch (e: Exception) {
|
|
698
|
+
// Some encoders reject the throughput-tuning keys (VBR bitrate mode,
|
|
699
|
+
// priority, operating rate) at configure() time. A codec that throws
|
|
700
|
+
// from configure() is unusable, so release it and retry on a fresh
|
|
701
|
+
// codec with a baseline format (default rate control) rather than
|
|
702
|
+
// failing the whole compression.
|
|
703
|
+
Log.w(
|
|
704
|
+
"Compressor",
|
|
705
|
+
"encoder.configure rejected tuned format; retrying with default settings",
|
|
706
|
+
e
|
|
707
|
+
)
|
|
708
|
+
runCatching { encoder.release() }
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
val baseline = baselineFormatProvider()
|
|
712
|
+
val fallback = pickAvcEncoder(baseline, hasQTI)
|
|
713
|
+
try {
|
|
714
|
+
fallback.configure(
|
|
715
|
+
baseline, null, null,
|
|
716
|
+
MediaCodec.CONFIGURE_FLAG_ENCODE
|
|
717
|
+
)
|
|
718
|
+
} catch (e: Exception) {
|
|
719
|
+
// Even the baseline format was rejected; release the codec so it
|
|
720
|
+
// doesn't leak, then let start()'s outer catch report the failure.
|
|
721
|
+
runCatching { fallback.release() }
|
|
722
|
+
throw e
|
|
723
|
+
}
|
|
724
|
+
Log.i("Compressor", "encoder selected (fallback, default rate control): ${fallback.name}")
|
|
725
|
+
return fallback
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private fun pickAvcEncoder(outputFormat: MediaFormat, hasQTI: Boolean): MediaCodec {
|
|
729
|
+
// ALL_CODECS surfaces vendor codecs that REGULAR_CODECS hides (e.g. some
|
|
730
|
+
// Exynos / MTK HW encoders). We still filter blacklisted / SW codecs below.
|
|
731
|
+
val codecList = MediaCodecList(MediaCodecList.ALL_CODECS)
|
|
732
|
+
val candidates = codecList.codecInfos.filter { info ->
|
|
733
|
+
info.isEncoder && info.supportedTypes.any { it.equals(MIME_TYPE, ignoreCase = true) }
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
fun isBlacklisted(name: String): Boolean {
|
|
737
|
+
val lower = name.lowercase()
|
|
738
|
+
return lower.contains("c2.qti.avc.encoder") || lower.contains("omx.qcom.video.encoder.avc.secure")
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
fun isSoftware(info: MediaCodecInfo): Boolean {
|
|
742
|
+
val name = info.name.lowercase()
|
|
743
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
744
|
+
if (info.isSoftwareOnly) return true
|
|
745
|
+
}
|
|
746
|
+
return name.startsWith("omx.google.") ||
|
|
747
|
+
name.startsWith("c2.android.") ||
|
|
748
|
+
name.contains(".sw.")
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
val supportsFormat = candidates.filter { info ->
|
|
752
|
+
runCatching {
|
|
753
|
+
info.getCapabilitiesForType(MIME_TYPE).isFormatSupported(outputFormat)
|
|
754
|
+
}.getOrDefault(false) && !isBlacklisted(info.name)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
val hardwareFirst = supportsFormat.firstOrNull { !isSoftware(it) }
|
|
758
|
+
val chosen = hardwareFirst ?: supportsFormat.firstOrNull()
|
|
759
|
+
|
|
760
|
+
if (chosen != null) {
|
|
761
|
+
return MediaCodec.createByCodecName(chosen.name)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Fallback: keep historical QTI-safe path when format probing fails.
|
|
765
|
+
return if (hasQTI) {
|
|
559
766
|
MediaCodec.createByCodecName("c2.android.avc.encoder")
|
|
560
767
|
} else {
|
|
561
768
|
MediaCodec.createEncoderByType(MIME_TYPE)
|
|
562
769
|
}
|
|
563
|
-
|
|
564
|
-
outputFormat, null, null,
|
|
565
|
-
MediaCodec.CONFIGURE_FLAG_ENCODE
|
|
566
|
-
)
|
|
770
|
+
}
|
|
567
771
|
|
|
568
|
-
|
|
772
|
+
// Dolby Vision profile 5 (0x20) carries no HEVC base layer, so no standard
|
|
773
|
+
// Android decoder can render it. Detect it up front so start() can reject the
|
|
774
|
+
// input before allocating the muxer/encoder/EGL surfaces. Profiles 8.x do carry
|
|
775
|
+
// an HEVC base layer and are remapped to HEVC in prepareDecoder.
|
|
776
|
+
private fun isUnsupportedDolbyVision(inputFormat: MediaFormat): Boolean {
|
|
777
|
+
val mime = inputFormat.getString(MediaFormat.KEY_MIME) ?: return false
|
|
778
|
+
if (!mime.equals("video/dolby-vision", ignoreCase = true)) return false
|
|
779
|
+
val profile = if (inputFormat.containsKey(MediaFormat.KEY_PROFILE)) {
|
|
780
|
+
inputFormat.getInteger(MediaFormat.KEY_PROFILE)
|
|
781
|
+
} else {
|
|
782
|
+
-1
|
|
783
|
+
}
|
|
784
|
+
return profile == 0x20
|
|
569
785
|
}
|
|
570
786
|
|
|
571
787
|
// Function to prepare the video decoder
|
|
@@ -573,42 +789,61 @@ object Compressor {
|
|
|
573
789
|
inputFormat: MediaFormat,
|
|
574
790
|
outputSurface: OutputSurface,
|
|
575
791
|
): MediaCodec {
|
|
576
|
-
// This seems to cause an issue with certain phones
|
|
577
|
-
// val decoderName =
|
|
578
|
-
// MediaCodecList(REGULAR_CODECS).findDecoderForFormat(inputFormat)
|
|
579
|
-
// val decoder = MediaCodec.createByCodecName(decoderName)
|
|
580
|
-
// Log.i("decoderName", decoder.name)
|
|
581
792
|
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
793
|
+
// Some inputs (e.g. iPhone .MOV files) report a "video/dolby-vision" MIME
|
|
794
|
+
// type that many devices cannot decode. Remap to a decodable base-layer
|
|
795
|
+
// codec, or fail with a clear error, before creating the decoder (#398).
|
|
796
|
+
ensureDecodableVideoFormat(inputFormat)
|
|
797
|
+
|
|
798
|
+
// Clear Dolby Vision specific profile and level to prevent configuration failures
|
|
799
|
+
// when the MIME type has been remapped to AVC/HEVC.
|
|
800
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
801
|
+
inputFormat.removeKey(MediaFormat.KEY_PROFILE)
|
|
802
|
+
inputFormat.removeKey(MediaFormat.KEY_LEVEL)
|
|
803
|
+
}
|
|
585
804
|
|
|
586
805
|
val decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME)!!)
|
|
587
|
-
//}
|
|
588
806
|
|
|
589
807
|
decoder.configure(inputFormat, outputSurface.getSurface(), null, 0)
|
|
590
808
|
|
|
591
809
|
return decoder
|
|
592
810
|
}
|
|
593
811
|
|
|
594
|
-
// Function to release resources
|
|
812
|
+
// Function to release resources.
|
|
813
|
+
// Every call is wrapped in runCatching so a failure in one teardown step
|
|
814
|
+
// does not skip the others (leaking codec handles + GL surfaces). Order:
|
|
815
|
+
// detach extractor → stop+release decoder → stop+release encoder →
|
|
816
|
+
// release input EGL surface → release output surface (joins its
|
|
817
|
+
// HandlerThread). Releasing surfaces last avoids the encoder asking a
|
|
818
|
+
// freed EGL surface for buffers during its own shutdown.
|
|
819
|
+
//
|
|
820
|
+
// decoder / inputSurface / outputSurface are nullable so this also serves the
|
|
821
|
+
// partial-init cleanup path, where a setup failure leaves some handles
|
|
822
|
+
// uncreated. The encoder is always created before teardown is reachable.
|
|
595
823
|
private fun dispose(
|
|
596
824
|
videoIndex: Int,
|
|
597
|
-
decoder: MediaCodec
|
|
825
|
+
decoder: MediaCodec?,
|
|
598
826
|
encoder: MediaCodec,
|
|
599
|
-
inputSurface: InputSurface
|
|
600
|
-
outputSurface: OutputSurface
|
|
827
|
+
inputSurface: InputSurface?,
|
|
828
|
+
outputSurface: OutputSurface?,
|
|
601
829
|
extractor: MediaExtractor
|
|
602
830
|
) {
|
|
603
|
-
extractor.unselectTrack(videoIndex)
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
decoder
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
831
|
+
runCatching { extractor.unselectTrack(videoIndex) }
|
|
832
|
+
.onFailure { Log.w("Compressor", "extractor.unselectTrack failed", it) }
|
|
833
|
+
|
|
834
|
+
runCatching { decoder?.stop() }
|
|
835
|
+
.onFailure { Log.w("Compressor", "decoder.stop failed", it) }
|
|
836
|
+
runCatching { decoder?.release() }
|
|
837
|
+
.onFailure { Log.w("Compressor", "decoder.release failed", it) }
|
|
838
|
+
|
|
839
|
+
runCatching { encoder.stop() }
|
|
840
|
+
.onFailure { Log.w("Compressor", "encoder.stop failed", it) }
|
|
841
|
+
runCatching { encoder.release() }
|
|
842
|
+
.onFailure { Log.w("Compressor", "encoder.release failed", it) }
|
|
843
|
+
|
|
844
|
+
runCatching { inputSurface?.release() }
|
|
845
|
+
.onFailure { Log.w("Compressor", "inputSurface.release failed", it) }
|
|
846
|
+
runCatching { outputSurface?.release() }
|
|
847
|
+
.onFailure { Log.w("Compressor", "outputSurface.release failed", it) }
|
|
613
848
|
}
|
|
614
849
|
}
|