react-native-compressor 1.19.0 → 1.19.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.
@@ -788,32 +788,21 @@ object Compressor {
788
788
  inputFormat: MediaFormat,
789
789
  outputSurface: OutputSurface,
790
790
  ): MediaCodec {
791
- val originalMime = inputFormat.getString(MediaFormat.KEY_MIME)!!
792
-
793
- // Dolby Vision (video/dolby-vision) has no standalone decoder on most Android
794
- // devices and throws NAME_NOT_FOUND. Profiles 8.1/8.4 carry an HEVC base layer
795
- // that the standard HEVC decoder can render, so we remap them to HEVC. Profile 5
796
- // has no compatible base layer; it is rejected by isUnsupportedDolbyVision() in
797
- // start() before any codec/surface is created, so it never reaches here.
798
- val resolvedMime = if (originalMime.equals("video/dolby-vision", ignoreCase = true)) {
799
- inputFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC)
800
- MediaFormat.MIMETYPE_VIDEO_HEVC
801
- } else {
802
- originalMime
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)
803
801
  }
804
802
 
805
- val decoder = MediaCodec.createDecoderByType(resolvedMime)
806
-
807
- try {
808
- decoder.configure(inputFormat, outputSurface.getSurface(), null, 0)
809
- } catch (e: Exception) {
810
- // A codec that throws from configure() is unusable; release it so a
811
- // configure failure here doesn't leak the decoder handle.
812
- runCatching { decoder.release() }
813
- throw e
814
- }
803
+ val decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME)!!)
815
804
 
816
- Log.i("Compressor", "decoder selected: ${decoder.name} mime=$resolvedMime")
805
+ decoder.configure(inputFormat, outputSurface.getSurface(), null, 0)
817
806
 
818
807
  return decoder
819
808
  }
@@ -13,7 +13,9 @@ public enum NextLevelSessionExporterError: Error, CustomStringConvertible {
13
13
  case readingFailure
14
14
  case writingFailure
15
15
  case cancelled
16
-
16
+ case unsupportedVideoOutputConfiguration
17
+ case missingVideoTrackInOutput
18
+
17
19
  public var description: String {
18
20
  get {
19
21
  switch self {
@@ -25,6 +27,10 @@ public enum NextLevelSessionExporterError: Error, CustomStringConvertible {
25
27
  return "Writing failure"
26
28
  case .cancelled:
27
29
  return "Cancelled"
30
+ case .unsupportedVideoOutputConfiguration:
31
+ return "The writer rejected the video output configuration"
32
+ case .missingVideoTrackInOutput:
33
+ return "Export finished without a video track in the output"
28
34
  }
29
35
  }
30
36
  }
@@ -249,7 +255,16 @@ extension NextLevelSessionExporter {
249
255
  }
250
256
  }
251
257
 
252
- self.setupVideoOutput(withAsset: asset)
258
+ // Fail loudly when the writer rejects the video output configuration.
259
+ // Continuing here used to export only the audio track and still resolve
260
+ // as a success (issue #400).
261
+ guard self.setupVideoOutput(withAsset: asset) else {
262
+ DispatchQueue.main.async {
263
+ self._completionHandler?(.failure(NextLevelSessionExporterError.unsupportedVideoOutputConfiguration))
264
+ self._completionHandler = nil
265
+ }
266
+ return
267
+ }
253
268
  if !self.stripAudio {
254
269
  self.setupAudioOutput(withAsset: asset)
255
270
  self.setupAudioInput()
@@ -316,11 +331,14 @@ extension NextLevelSessionExporter {
316
331
 
317
332
  extension NextLevelSessionExporter {
318
333
 
319
- private func setupVideoOutput(withAsset asset: AVAsset) {
334
+ /// Returns `false` when the asset has video tracks but a video writer input
335
+ /// could not be created — exporting would silently produce an audio-only
336
+ /// file (see numandev1/react-native-compressor#400).
337
+ private func setupVideoOutput(withAsset asset: AVAsset) -> Bool {
320
338
  let videoTracks = asset.tracks(withMediaType: AVMediaType.video)
321
-
339
+
322
340
  guard videoTracks.count > 0 else {
323
- return
341
+ return true
324
342
  }
325
343
 
326
344
  self._videoOutput = AVAssetReaderVideoCompositionOutput(videoTracks: videoTracks, videoSettings: self.videoInputConfiguration)
@@ -344,8 +362,8 @@ extension NextLevelSessionExporter {
344
362
  self._videoInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: self.videoOutputConfiguration)
345
363
  self._videoInput?.expectsMediaDataInRealTime = self.expectsMediaDataInRealTime
346
364
  } else {
347
- print("Unsupported output configuration")
348
- return
365
+ print("NextLevelSessionExporter, unsupported video output configuration, failing export instead of writing an audio-only file")
366
+ return false
349
367
  }
350
368
 
351
369
  if let writer = self._writer,
@@ -367,8 +385,9 @@ extension NextLevelSessionExporter {
367
385
 
368
386
  self._pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoInput, sourcePixelBufferAttributes: pixelBufferAttrib)
369
387
  }
388
+ return true
370
389
  }
371
-
390
+
372
391
  private func setupAudioOutput(withAsset asset: AVAsset) {
373
392
  let audioTracks = asset.tracks(withMediaType: AVMediaType.audio)
374
393
  var audioTracksToUse: [AVAssetTrack] = []
@@ -632,6 +651,22 @@ extension NextLevelSessionExporter {
632
651
  break
633
652
  }
634
653
 
654
+ // Guard against silently returning an audio-only file: when the source
655
+ // has a video track and a video output was configured, the exported file
656
+ // must contain a video track as well. A configuration the encoder rejects
657
+ // at write time can still end with `.completed` while the video track is
658
+ // dropped (issue #400) — surface that as an error instead of a success.
659
+ if self.videoOutputConfiguration != nil,
660
+ let asset = self.asset,
661
+ let outputURL = self.outputURL,
662
+ asset.tracks(withMediaType: AVMediaType.video).count > 0,
663
+ AVAsset(url: outputURL).tracks(withMediaType: AVMediaType.video).count == 0 {
664
+ try? FileManager.default.removeItem(at: outputURL)
665
+ self._completionHandler?(.failure(NextLevelSessionExporterError.missingVideoTrackInOutput))
666
+ self._completionHandler = nil
667
+ return
668
+ }
669
+
635
670
  self._completionHandler?(.success(self.status))
636
671
  self._completionHandler = nil
637
672
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-compressor",
3
- "version": "1.19.0",
3
+ "version": "1.19.2",
4
4
  "description": "Compress Image, Video, and Audio same like Whatsapp & Auto/Manual Compression | Background Upload | Download File | Create Video Thumbnail",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",