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.
@@ -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
- // Feed the decoder until it has no free input slots or the
315
- // extractor is empty. HW codecs typically have 4-8 input
316
- // slots; queuing only one sample per outer iteration starves
317
- // the pipeline and forces serial decode-render-encode.
318
- feedLoop@ while (!inputDone) {
319
- val index = extractor.sampleTrackIndex
320
-
321
- if (index == videoIndex) {
322
- val inputBufferIndex =
323
- decoder.dequeueInputBuffer(0L)
324
- if (inputBufferIndex < 0) break@feedLoop
325
- val inputBuffer = decoder.getInputBuffer(inputBufferIndex)
326
- val chunkSize = extractor.readSampleData(inputBuffer!!, 0)
327
- when {
328
- chunkSize < 0 -> {
329
- decoder.queueInputBuffer(
330
- inputBufferIndex,
331
- 0,
332
- 0,
333
- 0L,
334
- MediaCodec.BUFFER_FLAG_END_OF_STREAM
335
- )
336
- inputDone = true
337
- }
338
- else -> {
339
- decoder.queueInputBuffer(
340
- inputBufferIndex,
341
- 0,
342
- chunkSize,
343
- extractor.sampleTime,
344
- 0
345
- )
346
- extractor.advance()
347
- }
348
- }
349
- } else if (index == -1) { //end of file
350
- val inputBufferIndex =
351
- decoder.dequeueInputBuffer(0L)
352
- if (inputBufferIndex < 0) break@feedLoop
353
- decoder.queueInputBuffer(
354
- inputBufferIndex,
355
- 0,
356
- 0,
357
- 0L,
358
- MediaCodec.BUFFER_FLAG_END_OF_STREAM
359
- )
360
- inputDone = true
361
- } else {
362
- // Different track type at head of extractor (audio etc.).
363
- break@feedLoop
364
- }
365
- }
366
- }
367
-
368
- var decoderOutputAvailable = true
369
- var encoderOutputAvailable = true
370
-
371
- loop@ while (decoderOutputAvailable || encoderOutputAvailable) {
372
-
373
- if (!isRunning) {
374
- dispose(
375
- videoIndex,
376
- decoder,
377
- encoder,
378
- inputSurface,
379
- outputSurface,
380
- extractor
381
- )
382
-
383
- compressionProgressListener.onProgressCancelled(id)
384
- return Result(
385
- id,
386
- success = false,
387
- failureMessage = "The compression has stopped!"
388
- )
389
- }
390
-
391
- //Encoder
392
- val encoderStatus =
393
- encoder.dequeueOutputBuffer(bufferInfo, MEDIACODEC_TIMEOUT_DEFAULT)
394
-
395
- when {
396
- encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER -> encoderOutputAvailable =
397
- false
398
- encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
399
- val newFormat = encoder.outputFormat
400
- if (videoTrackIndex == -5)
401
- videoTrackIndex = mediaMuxer.addTrack(newFormat, false)
402
- }
403
- encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
404
- // ignore this status
405
- }
406
- encoderStatus < 0 -> throw RuntimeException("unexpected result from encoder.dequeueOutputBuffer: $encoderStatus")
407
- else -> {
408
- val encodedData = encoder.getOutputBuffer(encoderStatus)
409
- ?: throw RuntimeException("encoderOutputBuffer $encoderStatus was null")
410
-
411
- if (bufferInfo.size > 1) {
412
- if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
413
- mediaMuxer.writeSampleData(
414
- videoTrackIndex,
415
- encodedData, bufferInfo, false
416
- )
417
- }
418
-
419
- }
420
-
421
- outputDone =
422
- bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0
423
- encoder.releaseOutputBuffer(encoderStatus, false)
424
- }
425
- }
426
- if (encoderStatus != MediaCodec.INFO_TRY_AGAIN_LATER) continue@loop
427
-
428
- //Decoder
429
- val decoderStatus =
430
- decoder.dequeueOutputBuffer(bufferInfo, MEDIACODEC_TIMEOUT_DEFAULT)
431
- when {
432
- decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER -> decoderOutputAvailable =
433
- false
434
- decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
435
- // ignore this status
436
- }
437
- decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
438
- // ignore this status
439
- }
440
- decoderStatus < 0 -> throw RuntimeException("unexpected result from decoder.dequeueOutputBuffer: $decoderStatus")
441
- else -> {
442
- val isEos = (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
443
- var doRender = bufferInfo.size != 0 && !isEos
444
-
445
- // Drop frames whose PTS falls before the next target slot.
446
- // Anchor the next slot to the ideal grid (previous slot +
447
- // interval) instead of to the actual PTS anchoring to PTS
448
- // lets source-side jitter compound into extra drops, which
449
- // collapses the output frame rate well below the target.
450
- if (doRender && targetFrameIntervalUs > 0L) {
451
- if (bufferInfo.presentationTimeUs < nextTargetPtsUs) {
452
- doRender = false
453
- } else {
454
- nextTargetPtsUs += targetFrameIntervalUs
455
- // Snap forward when the source skips past a slot
456
- // (gap, seek, very low source fps) so the gate doesn't
457
- // burst-emit every following frame.
458
- if (bufferInfo.presentationTimeUs >= nextTargetPtsUs) {
459
- nextTargetPtsUs = bufferInfo.presentationTimeUs + targetFrameIntervalUs
460
- }
461
- }
462
- }
463
-
464
- decoder.releaseOutputBuffer(decoderStatus, doRender)
465
- if (doRender) {
466
- var errorWait = false
467
- try {
468
- outputSurface.awaitNewImage()
469
- } catch (e: Exception) {
470
- errorWait = true
471
- Log.e(
472
- "Compressor",
473
- e.message ?: "Compression failed at swapping buffer"
474
- )
475
- }
476
-
477
- if (!errorWait) {
478
- outputSurface.drawImage()
479
-
480
- inputSurface.setPresentationTime(bufferInfo.presentationTimeUs * 1000)
481
-
482
- compressionProgressListener.onProgressChanged(
483
- id,
484
- bufferInfo.presentationTimeUs.toFloat() / duration.toFloat() * 100
485
- )
486
-
487
- inputSurface.swapBuffers()
488
- }
489
- }
490
- if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
491
- decoderOutputAvailable = false
492
- encoder.signalEndOfInputStream()
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
- private fun processAudio(
609
- mediaMuxer: MP4Builder,
610
- bufferInfo: MediaCodec.BufferInfo,
611
- disableAudio: Boolean,
612
- extractor: MediaExtractor
613
- ) {
614
- val audioIndex = findTrack(extractor, isVideo = false)
615
- if (audioIndex >= 0 && !disableAudio) {
616
- extractor.selectTrack(audioIndex)
617
- val audioFormat = extractor.getTrackFormat(audioIndex)
618
- if (!isSupportedAudioFormat(audioFormat)) {
619
- extractor.unselectTrack(audioIndex)
620
- return
621
- }
622
- val muxerTrackIndex = mediaMuxer.addTrack(audioFormat, true)
623
- var maxBufferSize = if (audioFormat.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
624
- audioFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
625
- } else {
626
- 64 * 1024
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
- if (maxBufferSize <= 0) {
630
- maxBufferSize = 64 * 1024
631
- }
637
+ if (maxBufferSize <= 0) {
638
+ maxBufferSize = 64 * 1024
639
+ }
632
640
 
633
- var buffer: ByteBuffer = ByteBuffer.allocateDirect(maxBufferSize)
634
- if (Build.VERSION.SDK_INT >= 28) {
635
- val size = extractor.sampleSize
636
- if (size > maxBufferSize) {
637
- maxBufferSize = (size + 1024).toInt()
638
- buffer = ByteBuffer.allocateDirect(maxBufferSize)
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
- var inputDone = false
642
- extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
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
- } else {
659
- bufferInfo.size = 0
660
- inputDone = true
661
- }
662
- } else if (index == -1) {
663
- inputDone = true
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
- private fun isSupportedAudioFormat(audioFormat: MediaFormat): Boolean {
671
- if (!audioFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE) ||
672
- !audioFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
673
- return false
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
- private fun prepareEncoder(
682
- outputFormat: MediaFormat,
683
- hasQTI: Boolean,
684
- baselineFormatProvider: () -> MediaFormat,
685
- ): MediaCodec {
686
- // Prefer hardware AVC encoder while skipping known-broken QTI codec that
687
- // produces files unplayable on Mac/iOS (c2.qti.avc.encoder).
688
- val encoder = pickAvcEncoder(outputFormat, hasQTI)
689
- try {
690
- encoder.configure(
691
- outputFormat, null, null,
692
- MediaCodec.CONFIGURE_FLAG_ENCODE
693
- )
694
- Log.i("Compressor", "encoder selected: ${encoder.name}")
695
- return encoder
696
- } catch (e: Exception) {
697
- // Some encoders reject the throughput-tuning keys (VBR bitrate mode,
698
- // priority, operating rate) at configure() time. A codec that throws
699
- // from configure() is unusable, so release it and retry on a fresh
700
- // codec with a baseline format (default rate control) rather than
701
- // failing the whole compression.
702
- Log.w(
703
- "Compressor",
704
- "encoder.configure rejected tuned format; retrying with default settings",
705
- e
706
- )
707
- runCatching { encoder.release() }
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
- val baseline = baselineFormatProvider()
711
- val fallback = pickAvcEncoder(baseline, hasQTI)
712
- try {
713
- fallback.configure(
714
- baseline, null, null,
715
- MediaCodec.CONFIGURE_FLAG_ENCODE
716
- )
717
- } catch (e: Exception) {
718
- // Even the baseline format was rejected; release the codec so it
719
- // doesn't leak, then let start()'s outer catch report the failure.
720
- runCatching { fallback.release() }
721
- throw e
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
- private fun pickAvcEncoder(outputFormat: MediaFormat, hasQTI: Boolean): MediaCodec {
728
- // ALL_CODECS surfaces vendor codecs that REGULAR_CODECS hides (e.g. some
729
- // Exynos / MTK HW encoders). We still filter blacklisted / SW codecs below.
730
- val codecList = MediaCodecList(MediaCodecList.ALL_CODECS)
731
- val candidates = codecList.codecInfos.filter { info ->
732
- info.isEncoder && info.supportedTypes.any { it.equals(MIME_TYPE, ignoreCase = true) }
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
- fun isBlacklisted(name: String): Boolean {
736
- val lower = name.lowercase()
737
- return lower.contains("c2.qti.avc.encoder") || lower.contains("omx.qcom.video.encoder.avc.secure")
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
- fun isSoftware(info: MediaCodecInfo): Boolean {
741
- val name = info.name.lowercase()
742
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
743
- if (info.isSoftwareOnly) return true
744
- }
745
- return name.startsWith("omx.google.") ||
746
- name.startsWith("c2.android.") ||
747
- name.contains(".sw.")
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
- val supportsFormat = candidates.filter { info ->
751
- runCatching {
752
- info.getCapabilitiesForType(MIME_TYPE).isFormatSupported(outputFormat)
753
- }.getOrDefault(false) && !isBlacklisted(info.name)
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
- val hardwareFirst = supportsFormat.firstOrNull { !isSoftware(it) }
757
- val chosen = hardwareFirst ?: supportsFormat.firstOrNull()
765
+ val hardwareFirst = supportsFormat.firstOrNull { !isSoftware(it) }
766
+ val chosen = hardwareFirst ?: supportsFormat.firstOrNull()
758
767
 
759
- if (chosen != null) {
760
- return MediaCodec.createByCodecName(chosen.name)
761
- }
768
+ if (chosen != null) {
769
+ return MediaCodec.createByCodecName(chosen.name)
770
+ }
762
771
 
763
- // Fallback: keep historical QTI-safe path when format probing fails.
764
- return if (hasQTI) {
765
- MediaCodec.createByCodecName("c2.android.avc.encoder")
766
- } else {
767
- MediaCodec.createEncoderByType(MIME_TYPE)
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
- private fun isUnsupportedDolbyVision(inputFormat: MediaFormat): Boolean {
776
- val mime = inputFormat.getString(MediaFormat.KEY_MIME) ?: return false
777
- if (!mime.equals("video/dolby-vision", ignoreCase = true)) return false
778
- val profile = if (inputFormat.containsKey(MediaFormat.KEY_PROFILE)) {
779
- inputFormat.getInteger(MediaFormat.KEY_PROFILE)
780
- } else {
781
- -1
782
- }
783
- return profile == 0x20
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
- private fun prepareDecoder(
788
- inputFormat: MediaFormat,
789
- outputSurface: OutputSurface,
790
- ): MediaCodec {
791
- // Some inputs (e.g. iPhone .MOV files) report a "video/dolby-vision" MIME
792
- // type that many devices cannot decode. Remap to a decodable base-layer
793
- // codec, or fail with a clear error, before creating the decoder (#398).
794
- ensureDecodableVideoFormat(inputFormat)
795
-
796
- // Clear Dolby Vision specific profile and level to prevent configuration failures
797
- // when the MIME type has been remapped to AVC/HEVC.
798
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
799
- inputFormat.removeKey(MediaFormat.KEY_PROFILE)
800
- inputFormat.removeKey(MediaFormat.KEY_LEVEL)
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
- val decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME)!!)
825
+ val decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME)!!)
804
826
 
805
- decoder.configure(inputFormat, outputSurface.getSurface(), null, 0)
827
+ decoder.configure(inputFormat, outputSurface.getSurface(), null, 0)
806
828
 
807
- return decoder
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
- private fun dispose(
822
- videoIndex: Int,
823
- decoder: MediaCodec?,
824
- encoder: MediaCodec,
825
- inputSurface: InputSurface?,
826
- outputSurface: OutputSurface?,
827
- extractor: MediaExtractor
828
- ) {
829
- runCatching { extractor.unselectTrack(videoIndex) }
830
- .onFailure { Log.w("Compressor", "extractor.unselectTrack failed", it) }
831
-
832
- runCatching { decoder?.stop() }
833
- .onFailure { Log.w("Compressor", "decoder.stop failed", it) }
834
- runCatching { decoder?.release() }
835
- .onFailure { Log.w("Compressor", "decoder.release failed", it) }
836
-
837
- runCatching { encoder.stop() }
838
- .onFailure { Log.w("Compressor", "encoder.stop failed", it) }
839
- runCatching { encoder.release() }
840
- .onFailure { Log.w("Compressor", "encoder.release failed", it) }
841
-
842
- runCatching { inputSurface?.release() }
843
- .onFailure { Log.w("Compressor", "inputSurface.release failed", it) }
844
- runCatching { outputSurface?.release() }
845
- .onFailure { Log.w("Compressor", "outputSurface.release failed", it) }
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
  }