react-native-compressor 1.19.2 → 1.19.4
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 +7 -1
- package/android/build.gradle +0 -1
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt +152 -139
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioTranscoder.kt +204 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt +401 -379
- package/ios/Image/ImageCompressor.swift +37 -16
- package/ios/Image/ImageMain.swift +21 -11
- package/package.json +1 -1
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioExtractor.kt +0 -112
|
@@ -151,6 +151,7 @@ object Compressor {
|
|
|
151
151
|
newWidth = tempHeight
|
|
152
152
|
0
|
|
153
153
|
}
|
|
154
|
+
|
|
154
155
|
180 -> 0
|
|
155
156
|
else -> rotation
|
|
156
157
|
}
|
|
@@ -311,189 +312,196 @@ object Compressor {
|
|
|
311
312
|
|
|
312
313
|
while (!outputDone) {
|
|
313
314
|
if (!inputDone) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
326
|
+
val inputBuffer = decoder.getInputBuffer(inputBufferIndex)
|
|
327
|
+
val chunkSize = extractor.readSampleData(inputBuffer!!, 0)
|
|
328
|
+
when {
|
|
329
|
+
chunkSize < 0 -> {
|
|
330
|
+
decoder.queueInputBuffer(
|
|
331
|
+
inputBufferIndex,
|
|
332
|
+
0,
|
|
333
|
+
0,
|
|
334
|
+
0L,
|
|
335
|
+
MediaCodec.BUFFER_FLAG_END_OF_STREAM
|
|
336
|
+
)
|
|
337
|
+
inputDone = true
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
else -> {
|
|
341
|
+
decoder.queueInputBuffer(
|
|
342
|
+
inputBufferIndex,
|
|
343
|
+
0,
|
|
344
|
+
chunkSize,
|
|
345
|
+
extractor.sampleTime,
|
|
346
|
+
0
|
|
347
|
+
)
|
|
348
|
+
extractor.advance()
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} else if (index == -1) { //end of file
|
|
352
|
+
val inputBufferIndex =
|
|
353
|
+
decoder.dequeueInputBuffer(0L)
|
|
354
|
+
if (inputBufferIndex < 0) break@feedLoop
|
|
355
|
+
decoder.queueInputBuffer(
|
|
356
|
+
inputBufferIndex,
|
|
357
|
+
0,
|
|
358
|
+
0,
|
|
359
|
+
0L,
|
|
360
|
+
MediaCodec.BUFFER_FLAG_END_OF_STREAM
|
|
361
|
+
)
|
|
362
|
+
inputDone = true
|
|
363
|
+
} else {
|
|
364
|
+
// Different track type at head of extractor (audio etc.).
|
|
365
|
+
break@feedLoop
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
var decoderOutputAvailable = true
|
|
371
|
+
var encoderOutputAvailable = true
|
|
372
|
+
|
|
373
|
+
loop@ while (decoderOutputAvailable || encoderOutputAvailable) {
|
|
374
|
+
|
|
375
|
+
if (!isRunning) {
|
|
376
|
+
dispose(
|
|
377
|
+
videoIndex,
|
|
378
|
+
decoder,
|
|
379
|
+
encoder,
|
|
380
|
+
inputSurface,
|
|
381
|
+
outputSurface,
|
|
382
|
+
extractor
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
compressionProgressListener.onProgressCancelled(id)
|
|
386
|
+
return Result(
|
|
387
|
+
id,
|
|
388
|
+
success = false,
|
|
389
|
+
failureMessage = "The compression has stopped!"
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
//Encoder
|
|
394
|
+
val encoderStatus =
|
|
395
|
+
encoder.dequeueOutputBuffer(bufferInfo, MEDIACODEC_TIMEOUT_DEFAULT)
|
|
396
|
+
|
|
397
|
+
when {
|
|
398
|
+
encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER -> encoderOutputAvailable =
|
|
399
|
+
false
|
|
400
|
+
|
|
401
|
+
encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
|
402
|
+
val newFormat = encoder.outputFormat
|
|
403
|
+
if (videoTrackIndex == -5)
|
|
404
|
+
videoTrackIndex = mediaMuxer.addTrack(newFormat, false)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
|
|
408
|
+
// ignore this status
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
encoderStatus < 0 -> throw RuntimeException("unexpected result from encoder.dequeueOutputBuffer: $encoderStatus")
|
|
412
|
+
else -> {
|
|
413
|
+
val encodedData = encoder.getOutputBuffer(encoderStatus)
|
|
414
|
+
?: throw RuntimeException("encoderOutputBuffer $encoderStatus was null")
|
|
415
|
+
|
|
416
|
+
if (bufferInfo.size > 1) {
|
|
417
|
+
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
|
|
418
|
+
mediaMuxer.writeSampleData(
|
|
419
|
+
videoTrackIndex,
|
|
420
|
+
encodedData, bufferInfo, false
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
outputDone =
|
|
427
|
+
bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0
|
|
428
|
+
encoder.releaseOutputBuffer(encoderStatus, false)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (encoderStatus != MediaCodec.INFO_TRY_AGAIN_LATER) continue@loop
|
|
432
|
+
|
|
433
|
+
//Decoder
|
|
434
|
+
val decoderStatus =
|
|
435
|
+
decoder.dequeueOutputBuffer(bufferInfo, MEDIACODEC_TIMEOUT_DEFAULT)
|
|
436
|
+
when {
|
|
437
|
+
decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER -> decoderOutputAvailable =
|
|
438
|
+
false
|
|
439
|
+
|
|
440
|
+
decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
|
|
441
|
+
// ignore this status
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
|
445
|
+
// ignore this status
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
decoderStatus < 0 -> throw RuntimeException("unexpected result from decoder.dequeueOutputBuffer: $decoderStatus")
|
|
449
|
+
else -> {
|
|
450
|
+
val isEos = (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
|
|
451
|
+
var doRender = bufferInfo.size != 0 && !isEos
|
|
452
|
+
|
|
453
|
+
// Drop frames whose PTS falls before the next target slot.
|
|
454
|
+
// Anchor the next slot to the ideal grid (previous slot +
|
|
455
|
+
// interval) instead of to the actual PTS — anchoring to PTS
|
|
456
|
+
// lets source-side jitter compound into extra drops, which
|
|
457
|
+
// collapses the output frame rate well below the target.
|
|
458
|
+
if (doRender && targetFrameIntervalUs > 0L) {
|
|
459
|
+
if (bufferInfo.presentationTimeUs < nextTargetPtsUs) {
|
|
460
|
+
doRender = false
|
|
461
|
+
} else {
|
|
462
|
+
nextTargetPtsUs += targetFrameIntervalUs
|
|
463
|
+
// Snap forward when the source skips past a slot
|
|
464
|
+
// (gap, seek, very low source fps) so the gate doesn't
|
|
465
|
+
// burst-emit every following frame.
|
|
466
|
+
if (bufferInfo.presentationTimeUs >= nextTargetPtsUs) {
|
|
467
|
+
nextTargetPtsUs = bufferInfo.presentationTimeUs + targetFrameIntervalUs
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
decoder.releaseOutputBuffer(decoderStatus, doRender)
|
|
473
|
+
if (doRender) {
|
|
474
|
+
var errorWait = false
|
|
475
|
+
try {
|
|
476
|
+
outputSurface.awaitNewImage()
|
|
477
|
+
} catch (e: Exception) {
|
|
478
|
+
errorWait = true
|
|
479
|
+
Log.e(
|
|
480
|
+
"Compressor",
|
|
481
|
+
e.message ?: "Compression failed at swapping buffer"
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!errorWait) {
|
|
486
|
+
outputSurface.drawImage()
|
|
487
|
+
|
|
488
|
+
inputSurface.setPresentationTime(bufferInfo.presentationTimeUs * 1000)
|
|
489
|
+
|
|
490
|
+
compressionProgressListener.onProgressChanged(
|
|
491
|
+
id,
|
|
492
|
+
bufferInfo.presentationTimeUs.toFloat() / duration.toFloat() * 100
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
inputSurface.swapBuffers()
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
499
|
+
decoderOutputAvailable = false
|
|
500
|
+
encoder.signalEndOfInputStream()
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
497
505
|
}
|
|
498
506
|
|
|
499
507
|
} catch (exception: Throwable) {
|
|
@@ -605,207 +613,221 @@ object Compressor {
|
|
|
605
613
|
}
|
|
606
614
|
|
|
607
615
|
// Function to process audio
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
616
|
+
private fun processAudio(
|
|
617
|
+
mediaMuxer: MP4Builder,
|
|
618
|
+
bufferInfo: MediaCodec.BufferInfo,
|
|
619
|
+
disableAudio: Boolean,
|
|
620
|
+
extractor: MediaExtractor
|
|
621
|
+
) {
|
|
622
|
+
val audioIndex = findTrack(extractor, isVideo = false)
|
|
623
|
+
if (audioIndex >= 0 && !disableAudio) {
|
|
624
|
+
extractor.selectTrack(audioIndex)
|
|
625
|
+
val audioFormat = extractor.getTrackFormat(audioIndex)
|
|
626
|
+
if (!isSupportedAudioFormat(audioFormat)) {
|
|
627
|
+
extractor.unselectTrack(audioIndex)
|
|
628
|
+
return
|
|
629
|
+
}
|
|
630
|
+
val muxerTrackIndex = mediaMuxer.addTrack(audioFormat, true)
|
|
631
|
+
var maxBufferSize = if (audioFormat.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
|
|
632
|
+
audioFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
|
|
633
|
+
} else {
|
|
634
|
+
64 * 1024
|
|
635
|
+
}
|
|
628
636
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
637
|
+
if (maxBufferSize <= 0) {
|
|
638
|
+
maxBufferSize = 64 * 1024
|
|
639
|
+
}
|
|
632
640
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
641
|
+
var buffer: ByteBuffer = ByteBuffer.allocateDirect(maxBufferSize)
|
|
642
|
+
if (Build.VERSION.SDK_INT >= 28) {
|
|
643
|
+
val size = extractor.sampleSize
|
|
644
|
+
if (size > maxBufferSize) {
|
|
645
|
+
maxBufferSize = (size + 1024).toInt()
|
|
646
|
+
buffer = ByteBuffer.allocateDirect(maxBufferSize)
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
var inputDone = false
|
|
650
|
+
extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
|
|
651
|
+
|
|
652
|
+
while (!inputDone) {
|
|
653
|
+
val index = extractor.sampleTrackIndex
|
|
654
|
+
if (index == audioIndex) {
|
|
655
|
+
bufferInfo.size = extractor.readSampleData(buffer, 0)
|
|
656
|
+
|
|
657
|
+
if (bufferInfo.size >= 0) {
|
|
658
|
+
bufferInfo.apply {
|
|
659
|
+
presentationTimeUs = extractor.sampleTime
|
|
660
|
+
offset = 0
|
|
661
|
+
flags = MediaCodec.BUFFER_FLAG_KEY_FRAME
|
|
640
662
|
}
|
|
641
|
-
|
|
642
|
-
extractor.
|
|
643
|
-
|
|
644
|
-
while (!inputDone) {
|
|
645
|
-
val index = extractor.sampleTrackIndex
|
|
646
|
-
if (index == audioIndex) {
|
|
647
|
-
bufferInfo.size = extractor.readSampleData(buffer, 0)
|
|
648
|
-
|
|
649
|
-
if (bufferInfo.size >= 0) {
|
|
650
|
-
bufferInfo.apply {
|
|
651
|
-
presentationTimeUs = extractor.sampleTime
|
|
652
|
-
offset = 0
|
|
653
|
-
flags = MediaCodec.BUFFER_FLAG_KEY_FRAME
|
|
654
|
-
}
|
|
655
|
-
mediaMuxer.writeSampleData(muxerTrackIndex, buffer, bufferInfo, true)
|
|
656
|
-
extractor.advance()
|
|
663
|
+
mediaMuxer.writeSampleData(muxerTrackIndex, buffer, bufferInfo, true)
|
|
664
|
+
extractor.advance()
|
|
657
665
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
extractor.unselectTrack(audioIndex)
|
|
666
|
+
} else {
|
|
667
|
+
bufferInfo.size = 0
|
|
668
|
+
inputDone = true
|
|
669
|
+
}
|
|
670
|
+
} else if (index == -1) {
|
|
671
|
+
inputDone = true
|
|
667
672
|
}
|
|
673
|
+
}
|
|
674
|
+
extractor.unselectTrack(audioIndex)
|
|
668
675
|
}
|
|
676
|
+
}
|
|
669
677
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
val sampleRate = audioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
|
|
676
|
-
val channelCount = audioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
|
|
677
|
-
return channelCount > 0 && sampleRate in SUPPORTED_AUDIO_SAMPLE_RATES
|
|
678
|
+
private fun isSupportedAudioFormat(audioFormat: MediaFormat): Boolean {
|
|
679
|
+
if (!audioFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE) ||
|
|
680
|
+
!audioFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT)
|
|
681
|
+
) {
|
|
682
|
+
return false
|
|
678
683
|
}
|
|
684
|
+
val sampleRate = audioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
|
|
685
|
+
val channelCount = audioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
|
|
686
|
+
return channelCount > 0 && sampleRate in SUPPORTED_AUDIO_SAMPLE_RATES
|
|
687
|
+
}
|
|
679
688
|
|
|
680
689
|
// Function to prepare the video encoder
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
690
|
+
private fun prepareEncoder(
|
|
691
|
+
outputFormat: MediaFormat,
|
|
692
|
+
hasQTI: Boolean,
|
|
693
|
+
baselineFormatProvider: () -> MediaFormat,
|
|
694
|
+
): MediaCodec {
|
|
695
|
+
// Prefer hardware AVC encoder while skipping known-broken QTI codec that
|
|
696
|
+
// produces files unplayable on Mac/iOS (c2.qti.avc.encoder).
|
|
697
|
+
val encoder = pickAvcEncoder(outputFormat, hasQTI)
|
|
698
|
+
try {
|
|
699
|
+
encoder.configure(
|
|
700
|
+
outputFormat, null, null,
|
|
701
|
+
MediaCodec.CONFIGURE_FLAG_ENCODE
|
|
702
|
+
)
|
|
703
|
+
Log.i("Compressor", "encoder selected: ${encoder.name}")
|
|
704
|
+
return encoder
|
|
705
|
+
} catch (e: Exception) {
|
|
706
|
+
// Some encoders reject the throughput-tuning keys (VBR bitrate mode,
|
|
707
|
+
// priority, operating rate) at configure() time. A codec that throws
|
|
708
|
+
// from configure() is unusable, so release it and retry on a fresh
|
|
709
|
+
// codec with a baseline format (default rate control) rather than
|
|
710
|
+
// failing the whole compression.
|
|
711
|
+
Log.w(
|
|
712
|
+
"Compressor",
|
|
713
|
+
"encoder.configure rejected tuned format; retrying with default settings",
|
|
714
|
+
e
|
|
715
|
+
)
|
|
716
|
+
runCatching { encoder.release() }
|
|
717
|
+
}
|
|
709
718
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
Log.i("Compressor", "encoder selected (fallback, default rate control): ${fallback.name}")
|
|
724
|
-
return fallback
|
|
719
|
+
val baseline = baselineFormatProvider()
|
|
720
|
+
val fallback = pickAvcEncoder(baseline, hasQTI)
|
|
721
|
+
try {
|
|
722
|
+
fallback.configure(
|
|
723
|
+
baseline, null, null,
|
|
724
|
+
MediaCodec.CONFIGURE_FLAG_ENCODE
|
|
725
|
+
)
|
|
726
|
+
} catch (e: Exception) {
|
|
727
|
+
// Even the baseline format was rejected; release the codec so it
|
|
728
|
+
// doesn't leak, then let start()'s outer catch report the failure.
|
|
729
|
+
runCatching { fallback.release() }
|
|
730
|
+
throw e
|
|
725
731
|
}
|
|
732
|
+
Log.i("Compressor", "encoder selected (fallback, default rate control): ${fallback.name}")
|
|
733
|
+
return fallback
|
|
734
|
+
}
|
|
726
735
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
736
|
+
private fun pickAvcEncoder(outputFormat: MediaFormat, hasQTI: Boolean): MediaCodec {
|
|
737
|
+
// ALL_CODECS surfaces vendor codecs that REGULAR_CODECS hides (e.g. some
|
|
738
|
+
// Exynos / MTK HW encoders). We still filter blacklisted / SW codecs below.
|
|
739
|
+
val codecList = MediaCodecList(MediaCodecList.ALL_CODECS)
|
|
740
|
+
val candidates = codecList.codecInfos.filter { info ->
|
|
741
|
+
info.isEncoder && info.supportedTypes.any { it.equals(MIME_TYPE, ignoreCase = true) }
|
|
742
|
+
}
|
|
734
743
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
744
|
+
fun isBlacklisted(name: String): Boolean {
|
|
745
|
+
val lower = name.lowercase()
|
|
746
|
+
return lower.contains("c2.qti.avc.encoder") || lower.contains("omx.qcom.video.encoder.avc.secure")
|
|
747
|
+
}
|
|
739
748
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
+
fun isSoftware(info: MediaCodecInfo): Boolean {
|
|
750
|
+
val name = info.name.lowercase()
|
|
751
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
752
|
+
if (info.isSoftwareOnly) return true
|
|
753
|
+
}
|
|
754
|
+
return name.startsWith("omx.google.") ||
|
|
755
|
+
name.startsWith("c2.android.") ||
|
|
756
|
+
name.contains(".sw.")
|
|
757
|
+
}
|
|
749
758
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
759
|
+
val supportsFormat = candidates.filter { info ->
|
|
760
|
+
runCatching {
|
|
761
|
+
info.getCapabilitiesForType(MIME_TYPE).isFormatSupported(outputFormat)
|
|
762
|
+
}.getOrDefault(false) && !isBlacklisted(info.name)
|
|
763
|
+
}
|
|
755
764
|
|
|
756
|
-
|
|
757
|
-
|
|
765
|
+
val hardwareFirst = supportsFormat.firstOrNull { !isSoftware(it) }
|
|
766
|
+
val chosen = hardwareFirst ?: supportsFormat.firstOrNull()
|
|
758
767
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
768
|
+
if (chosen != null) {
|
|
769
|
+
return MediaCodec.createByCodecName(chosen.name)
|
|
770
|
+
}
|
|
762
771
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
772
|
+
// Fallback: keep historical QTI-safe path when format probing fails.
|
|
773
|
+
return if (hasQTI) {
|
|
774
|
+
MediaCodec.createByCodecName("c2.android.avc.encoder")
|
|
775
|
+
} else {
|
|
776
|
+
MediaCodec.createEncoderByType(MIME_TYPE)
|
|
769
777
|
}
|
|
778
|
+
}
|
|
770
779
|
|
|
771
780
|
// Dolby Vision profile 5 (0x20) carries no HEVC base layer, so no standard
|
|
772
781
|
// Android decoder can render it. Detect it up front so start() can reject the
|
|
773
782
|
// input before allocating the muxer/encoder/EGL surfaces. Profiles 8.x do carry
|
|
774
783
|
// an HEVC base layer and are remapped to HEVC in prepareDecoder.
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
+
private fun isUnsupportedDolbyVision(inputFormat: MediaFormat): Boolean {
|
|
785
|
+
val mime = inputFormat.getString(MediaFormat.KEY_MIME) ?: return false
|
|
786
|
+
if (!mime.equals("video/dolby-vision", ignoreCase = true)) return false
|
|
787
|
+
val profile = if (inputFormat.containsKey(MediaFormat.KEY_PROFILE)) {
|
|
788
|
+
inputFormat.getInteger(MediaFormat.KEY_PROFILE)
|
|
789
|
+
} else {
|
|
790
|
+
-1
|
|
791
|
+
}
|
|
792
|
+
return profile == 0x20
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Some inputs (e.g. iPhone .MOV files) report a "video/dolby-vision" MIME
|
|
796
|
+
// type that many devices cannot decode. Profiles 8.x carry an HEVC base
|
|
797
|
+
// layer that the standard HEVC decoder can render, so we remap them to
|
|
798
|
+
// HEVC here. Profile 5 has no compatible base layer and is rejected by
|
|
799
|
+
// isUnsupportedDolbyVision() in start() before any codec/surface is
|
|
800
|
+
// created, so it never reaches this helper (#398).
|
|
801
|
+
private fun ensureDecodableVideoFormat(inputFormat: MediaFormat) {
|
|
802
|
+
val mime = inputFormat.getString(MediaFormat.KEY_MIME) ?: return
|
|
803
|
+
if (mime.equals("video/dolby-vision", ignoreCase = true)) {
|
|
804
|
+
inputFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC)
|
|
784
805
|
}
|
|
806
|
+
}
|
|
785
807
|
|
|
786
808
|
// Function to prepare the video decoder
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
809
|
+
private fun prepareDecoder(
|
|
810
|
+
inputFormat: MediaFormat,
|
|
811
|
+
outputSurface: OutputSurface,
|
|
812
|
+
): MediaCodec {
|
|
813
|
+
// Some inputs (e.g. iPhone .MOV files) report a "video/dolby-vision" MIME
|
|
814
|
+
// type that many devices cannot decode. Remap to a decodable base-layer
|
|
815
|
+
// codec, or fail with a clear error, before creating the decoder (#398).
|
|
816
|
+
ensureDecodableVideoFormat(inputFormat)
|
|
817
|
+
|
|
818
|
+
// Clear Dolby Vision specific profile and level to prevent configuration failures
|
|
819
|
+
// when the MIME type has been remapped to AVC/HEVC.
|
|
820
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
821
|
+
inputFormat.removeKey(MediaFormat.KEY_PROFILE)
|
|
822
|
+
inputFormat.removeKey(MediaFormat.KEY_LEVEL)
|
|
823
|
+
}
|
|
802
824
|
|
|
803
|
-
|
|
825
|
+
val decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME)!!)
|
|
804
826
|
|
|
805
|
-
|
|
827
|
+
decoder.configure(inputFormat, outputSurface.getSurface(), null, 0)
|
|
806
828
|
|
|
807
|
-
|
|
808
|
-
|
|
829
|
+
return decoder
|
|
830
|
+
}
|
|
809
831
|
|
|
810
832
|
// Function to release resources.
|
|
811
833
|
// Every call is wrapped in runCatching so a failure in one teardown step
|
|
@@ -818,30 +840,30 @@ object Compressor {
|
|
|
818
840
|
// decoder / inputSurface / outputSurface are nullable so this also serves the
|
|
819
841
|
// partial-init cleanup path, where a setup failure leaves some handles
|
|
820
842
|
// uncreated. The encoder is always created before teardown is reachable.
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
843
|
+
private fun dispose(
|
|
844
|
+
videoIndex: Int,
|
|
845
|
+
decoder: MediaCodec?,
|
|
846
|
+
encoder: MediaCodec,
|
|
847
|
+
inputSurface: InputSurface?,
|
|
848
|
+
outputSurface: OutputSurface?,
|
|
849
|
+
extractor: MediaExtractor
|
|
850
|
+
) {
|
|
851
|
+
runCatching { extractor.unselectTrack(videoIndex) }
|
|
852
|
+
.onFailure { Log.w("Compressor", "extractor.unselectTrack failed", it) }
|
|
853
|
+
|
|
854
|
+
runCatching { decoder?.stop() }
|
|
855
|
+
.onFailure { Log.w("Compressor", "decoder.stop failed", it) }
|
|
856
|
+
runCatching { decoder?.release() }
|
|
857
|
+
.onFailure { Log.w("Compressor", "decoder.release failed", it) }
|
|
858
|
+
|
|
859
|
+
runCatching { encoder.stop() }
|
|
860
|
+
.onFailure { Log.w("Compressor", "encoder.stop failed", it) }
|
|
861
|
+
runCatching { encoder.release() }
|
|
862
|
+
.onFailure { Log.w("Compressor", "encoder.release failed", it) }
|
|
863
|
+
|
|
864
|
+
runCatching { inputSurface?.release() }
|
|
865
|
+
.onFailure { Log.w("Compressor", "inputSurface.release failed", it) }
|
|
866
|
+
runCatching { outputSurface?.release() }
|
|
867
|
+
.onFailure { Log.w("Compressor", "outputSurface.release failed", it) }
|
|
868
|
+
}
|
|
847
869
|
}
|