react-native-compressor 1.19.0 → 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.
Files changed (133) hide show
  1. package/android/CMakeLists.txt +27 -0
  2. package/android/build.gradle +56 -58
  3. package/android/gradle.properties +5 -5
  4. package/android/src/main/AndroidManifestNew.xml +3 -0
  5. package/android/src/main/cpp/cpp-adapter.cpp +8 -0
  6. package/android/src/main/java/com/margelo/nitro/compressor/HybridCompressor.kt +213 -0
  7. package/android/src/main/java/com/reactnativecompressor/NitroCompressorPackage.kt +30 -0
  8. package/android/src/main/java/com/reactnativecompressor/NitroPromiseAdapter.kt +55 -0
  9. package/android/src/main/java/com/reactnativecompressor/Utils/EventEmitterHandler.kt +49 -45
  10. package/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt +1 -1
  11. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt +13 -22
  12. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt +72 -0
  13. package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt +4 -1
  14. package/ios/Audio/FormatConverter/FormatConverter+Compressed.swift +1 -1
  15. package/ios/Audio/FormatConverter/FormatConverter+Utilities.swift +1 -1
  16. package/ios/Audio/FormatConverter/FormatConverter.swift +5 -3
  17. package/ios/HybridCompressor.swift +247 -0
  18. package/ios/Image/ImageCompressor.swift +8 -2
  19. package/ios/Utils/EventEmitterHandler.swift +56 -34
  20. package/ios/Video/NextLevelSessionExporter.swift +43 -8
  21. package/ios/Video/VideoMain.swift +17 -3
  22. package/lib/commonjs/Audio/index.js +1 -1
  23. package/lib/commonjs/Audio/index.js.map +1 -1
  24. package/lib/commonjs/Image/index.js +2 -22
  25. package/lib/commonjs/Image/index.js.map +1 -1
  26. package/lib/commonjs/Main.js +8 -8
  27. package/lib/commonjs/Main.js.map +1 -1
  28. package/lib/commonjs/Video/index.js +25 -65
  29. package/lib/commonjs/Video/index.js.map +1 -1
  30. package/lib/commonjs/specs/Compressor.nitro.js +6 -0
  31. package/lib/commonjs/specs/Compressor.nitro.js.map +1 -0
  32. package/lib/commonjs/utils/Downloader.js +8 -24
  33. package/lib/commonjs/utils/Downloader.js.map +1 -1
  34. package/lib/commonjs/utils/Uploader.js +3 -18
  35. package/lib/commonjs/utils/Uploader.js.map +1 -1
  36. package/lib/commonjs/utils/helpers.js +18 -1
  37. package/lib/commonjs/utils/helpers.js.map +1 -1
  38. package/lib/commonjs/utils/index.js +13 -13
  39. package/lib/commonjs/utils/index.js.map +1 -1
  40. package/lib/module/Audio/index.js +2 -2
  41. package/lib/module/Audio/index.js.map +1 -1
  42. package/lib/module/Image/index.js +3 -23
  43. package/lib/module/Image/index.js.map +1 -1
  44. package/lib/module/Main.js +9 -9
  45. package/lib/module/Main.js.map +1 -1
  46. package/lib/module/Video/index.js +26 -66
  47. package/lib/module/Video/index.js.map +1 -1
  48. package/lib/module/specs/Compressor.nitro.js +4 -0
  49. package/lib/module/specs/Compressor.nitro.js.map +1 -0
  50. package/lib/module/utils/Downloader.js +10 -26
  51. package/lib/module/utils/Downloader.js.map +1 -1
  52. package/lib/module/utils/Uploader.js +5 -20
  53. package/lib/module/utils/Uploader.js.map +1 -1
  54. package/lib/module/utils/helpers.js +16 -0
  55. package/lib/module/utils/helpers.js.map +1 -1
  56. package/lib/module/utils/index.js +2 -1
  57. package/lib/module/utils/index.js.map +1 -1
  58. package/lib/typescript/src/Image/index.d.ts.map +1 -1
  59. package/lib/typescript/src/Main.d.ts +2 -1
  60. package/lib/typescript/src/Main.d.ts.map +1 -1
  61. package/lib/typescript/src/Video/index.d.ts +1 -1
  62. package/lib/typescript/src/Video/index.d.ts.map +1 -1
  63. package/lib/typescript/src/index.d.ts.map +1 -1
  64. package/lib/typescript/src/specs/Compressor.nitro.d.ts +44 -0
  65. package/lib/typescript/src/specs/Compressor.nitro.d.ts.map +1 -0
  66. package/lib/typescript/src/utils/Downloader.d.ts.map +1 -1
  67. package/lib/typescript/src/utils/Uploader.d.ts.map +1 -1
  68. package/lib/typescript/src/utils/helpers.d.ts +7 -0
  69. package/lib/typescript/src/utils/helpers.d.ts.map +1 -1
  70. package/lib/typescript/src/utils/index.d.ts.map +1 -1
  71. package/nitrogen/generated/android/NitroCompressor+autolinking.cmake +81 -0
  72. package/nitrogen/generated/android/NitroCompressor+autolinking.gradle +27 -0
  73. package/nitrogen/generated/android/NitroCompressorOnLoad.cpp +60 -0
  74. package/nitrogen/generated/android/NitroCompressorOnLoad.hpp +34 -0
  75. package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  76. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  77. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
  78. package/nitrogen/generated/android/c++/JHybridCompressorSpec.cpp +293 -0
  79. package/nitrogen/generated/android/c++/JHybridCompressorSpec.hpp +78 -0
  80. package/nitrogen/generated/android/c++/JVideoThumbnailResult.hpp +73 -0
  81. package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void.kt +80 -0
  82. package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double.kt +80 -0
  83. package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double_double.kt +80 -0
  84. package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/HybridCompressorSpec.kt +141 -0
  85. package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/NitroCompressorOnLoad.kt +35 -0
  86. package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/VideoThumbnailResult.kt +71 -0
  87. package/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.cpp +11 -0
  88. package/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.hpp +204 -0
  89. package/nitrogen/generated/ios/react_native_compressor+autolinking.rb +62 -0
  90. package/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.cpp +89 -0
  91. package/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.hpp +337 -0
  92. package/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Umbrella.hpp +51 -0
  93. package/nitrogen/generated/ios/react_native_compressorAutolinking.mm +33 -0
  94. package/nitrogen/generated/ios/react_native_compressorAutolinking.swift +26 -0
  95. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  96. package/nitrogen/generated/ios/swift/Func_void_VideoThumbnailResult.swift +46 -0
  97. package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
  98. package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
  99. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  100. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_AnyMap_.swift +46 -0
  101. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  102. package/nitrogen/generated/ios/swift/HybridCompressorSpec.swift +70 -0
  103. package/nitrogen/generated/ios/swift/HybridCompressorSpec_cxx.swift +493 -0
  104. package/nitrogen/generated/ios/swift/VideoThumbnailResult.swift +49 -0
  105. package/nitrogen/generated/shared/c++/HybridCompressorSpec.cpp +36 -0
  106. package/nitrogen/generated/shared/c++/HybridCompressorSpec.hpp +83 -0
  107. package/nitrogen/generated/shared/c++/VideoThumbnailResult.hpp +99 -0
  108. package/package.json +8 -12
  109. package/react-native-compressor.podspec +6 -24
  110. package/src/Audio/index.tsx +2 -2
  111. package/src/Image/index.tsx +3 -26
  112. package/src/Main.tsx +14 -16
  113. package/src/Video/index.tsx +25 -77
  114. package/src/specs/Compressor.nitro.ts +56 -0
  115. package/src/utils/Downloader.tsx +8 -27
  116. package/src/utils/Uploader.tsx +12 -24
  117. package/src/utils/helpers.ts +18 -0
  118. package/src/utils/index.tsx +3 -2
  119. package/android/src/main/java/com/reactnativecompressor/CompressorModule.kt +0 -180
  120. package/android/src/main/java/com/reactnativecompressor/CompressorPackage.kt +0 -34
  121. package/android/src/newarch/CompressorSpec.kt +0 -5
  122. package/android/src/oldarch/CompressorSpec.kt +0 -36
  123. package/ios/Compressor-Bridging-Header.h +0 -6
  124. package/ios/Compressor.h +0 -5
  125. package/ios/Compressor.mm +0 -82
  126. package/ios/CompressorManager.swift +0 -117
  127. package/lib/commonjs/Spec/NativeCompressor.js +0 -9
  128. package/lib/commonjs/Spec/NativeCompressor.js.map +0 -1
  129. package/lib/module/Spec/NativeCompressor.js +0 -5
  130. package/lib/module/Spec/NativeCompressor.js.map +0 -1
  131. package/lib/typescript/src/Spec/NativeCompressor.d.ts +0 -30
  132. package/lib/typescript/src/Spec/NativeCompressor.d.ts.map +0 -1
  133. package/src/Spec/NativeCompressor.ts +0 -41
@@ -11,6 +11,7 @@ import android.net.Uri
11
11
  import android.os.Build
12
12
  import android.util.Log
13
13
  import com.reactnativecompressor.Video.VideoCompressor.CompressionProgressListener
14
+ import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.ensureDecodableVideoFormat
14
15
  import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.findTrack
15
16
  import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.hasQTI
16
17
  import com.reactnativecompressor.Video.VideoCompressor.utils.CompressorUtils.prepareVideoHeight
@@ -788,32 +789,22 @@ object Compressor {
788
789
  inputFormat: MediaFormat,
789
790
  outputSurface: OutputSurface,
790
791
  ): 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
803
- }
804
792
 
805
- val decoder = MediaCodec.createDecoderByType(resolvedMime)
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)
806
797
 
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
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)
814
803
  }
815
804
 
816
- Log.i("Compressor", "decoder selected: ${decoder.name} mime=$resolvedMime")
805
+ val decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME)!!)
806
+
807
+ decoder.configure(inputFormat, outputSurface.getSurface(), null, 0)
817
808
 
818
809
  return decoder
819
810
  }
@@ -207,4 +207,76 @@ object CompressorUtils {
207
207
  }
208
208
  return false
209
209
  }
210
+
211
+ // MIME type reported by MediaExtractor for Dolby Vision tracks (e.g. iPhone .MOV files)
212
+ private const val MIMETYPE_VIDEO_DOLBY_VISION = "video/dolby-vision"
213
+
214
+ /**
215
+ * Check whether the device exposes a decoder for the given MIME type.
216
+ */
217
+ private fun hasDecoderForMime(mime: String): Boolean {
218
+ val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
219
+ for (codec in codecList.codecInfos) {
220
+ if (codec.isEncoder) continue
221
+ for (type in codec.supportedTypes) {
222
+ if (type.equals(mime, ignoreCase = true)) return true
223
+ }
224
+ }
225
+ return false
226
+ }
227
+
228
+ /**
229
+ * Resolve the backward-compatible base-layer MIME type for a Dolby Vision track.
230
+ *
231
+ * Dolby Vision profiles 8.x (DvheSt) and 4 (DvheDtr) carry an HEVC base layer, and
232
+ * profile 9 (DvavSe) carries an AVC base layer; these can be decoded by the standard
233
+ * HEVC/AVC decoders. Profiles 5 (DvheStn) and 7 (DvheDtb) have no usable single base
234
+ * layer, so they return null. When the profile is unknown we assume HEVC, which covers
235
+ * the common consumer case (e.g. iPhone records Dolby Vision profile 8).
236
+ */
237
+ private fun dolbyVisionBaseLayerMime(inputFormat: MediaFormat): String? {
238
+ if (!inputFormat.containsKey(MediaFormat.KEY_PROFILE)) {
239
+ return MediaFormat.MIMETYPE_VIDEO_HEVC
240
+ }
241
+ return when (inputFormat.getInteger(MediaFormat.KEY_PROFILE)) {
242
+ MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvavSe -> MediaFormat.MIMETYPE_VIDEO_AVC
243
+ MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt,
244
+ MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtr -> MediaFormat.MIMETYPE_VIDEO_HEVC
245
+ else -> null
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Ensure the input video format can be decoded on this device.
251
+ *
252
+ * Some containers (notably iPhone `.MOV` files) expose the video track as
253
+ * `video/dolby-vision`. Many Android devices have no Dolby Vision decoder, so
254
+ * `MediaCodec.createDecoderByType("video/dolby-vision")` fails with NAME_NOT_FOUND.
255
+ * When the dedicated decoder is missing but the stream carries a backward-compatible
256
+ * base layer, this rewrites the format MIME to the base-layer codec so the standard
257
+ * HEVC/AVC decoder can decode it. If no compatible decoder exists, it throws a clear
258
+ * error instead of letting the cryptic native failure surface (see issue #398).
259
+ */
260
+ fun ensureDecodableVideoFormat(inputFormat: MediaFormat) {
261
+ val mime = inputFormat.getString(MediaFormat.KEY_MIME) ?: return
262
+ if (hasDecoderForMime(mime)) return
263
+
264
+ if (mime.equals(MIMETYPE_VIDEO_DOLBY_VISION, ignoreCase = true)) {
265
+ val fallbackMime = dolbyVisionBaseLayerMime(inputFormat)
266
+ if (fallbackMime != null && hasDecoderForMime(fallbackMime)) {
267
+ Log.w(
268
+ "Compressor",
269
+ "No Dolby Vision decoder on this device; decoding the $fallbackMime base layer instead."
270
+ )
271
+ inputFormat.setString(MediaFormat.KEY_MIME, fallbackMime)
272
+ return
273
+ }
274
+ throw IllegalStateException(
275
+ "This video uses Dolby Vision, which is not supported by this device's decoders " +
276
+ "and has no backward-compatible base layer to fall back to."
277
+ )
278
+ }
279
+
280
+ throw IllegalStateException("No decoder available for video format: $mime")
281
+ }
210
282
  }
@@ -5,6 +5,7 @@ import android.content.Context
5
5
  import android.media.MediaMetadataRetriever
6
6
  import android.net.Uri
7
7
  import android.os.Handler
8
+ import android.os.Looper
8
9
  import android.os.PowerManager
9
10
  import android.os.PowerManager.WakeLock
10
11
  import com.facebook.react.bridge.LifecycleEventListener
@@ -58,7 +59,9 @@ class VideoCompressorHelper {
58
59
  if (!wakeLock!!.isHeld()) {
59
60
  wakeLock!!.acquire()
60
61
  }
61
- handler = Handler()
62
+ // Bind to the main Looper: under Nitro this runs on a background executor
63
+ // thread (no Looper), so the no-arg Handler() constructor would throw.
64
+ handler = Handler(Looper.getMainLooper())
62
65
  runnable = Runnable { }
63
66
  handler!!.post(runnable!!)
64
67
  return ""
@@ -5,7 +5,7 @@ import AVFoundation
5
5
  // MARK: - internal helper functions
6
6
 
7
7
 
8
- public extension AVURLAsset {
8
+ extension AVURLAsset {
9
9
  /// Audio format for the file in the URL asset
10
10
  var audioFormat: AVAudioFormat? {
11
11
  // pull the input format out of the audio file...
@@ -11,7 +11,7 @@ extension FormatConverter {
11
11
  }
12
12
  }
13
13
 
14
- public extension FormatConverter {
14
+ extension FormatConverter {
15
15
  /// Is this file a PCM file?
16
16
  /// - Parameters:
17
17
  /// - url: The URL to parse
@@ -19,7 +19,9 @@ import AVFoundation
19
19
  }
20
20
  ```
21
21
  */
22
- public class FormatConverter {
22
+ // `internal` (not `public`): keeps these types out of the Swift↔C++ interop surface
23
+ // that Nitro enables module-wide, which otherwise fails to link the nested `Options` type.
24
+ class FormatConverter {
23
25
  // MARK: - properties
24
26
 
25
27
  /// The source audio file
@@ -122,7 +124,7 @@ public class FormatConverter {
122
124
 
123
125
  // MARK: - Definitions
124
126
 
125
- public enum AudioFileFormat: String {
127
+ enum AudioFileFormat: String {
126
128
  case aac
127
129
  case aif
128
130
  case aifc
@@ -141,7 +143,7 @@ public enum AudioFileFormat: String {
141
143
  case wav
142
144
  }
143
145
 
144
- public extension FormatConverter {
146
+ extension FormatConverter {
145
147
 
146
148
  /// FormatConverterCallback is the callback format for start()
147
149
  /// - Parameter: error This will contain one parameter of type Error which is nil if the conversion was successful.
@@ -0,0 +1,247 @@
1
+ //
2
+ // HybridCompressor.swift
3
+ // react-native-compressor
4
+ //
5
+ // Nitro HybridObject implementation of the single `Compressor` native module.
6
+ // It is a thin binding layer: it converts Nitro's `AnyMap` options into the
7
+ // `NSDictionary`/`[String: Any]` the existing domain code already consumes,
8
+ // bridges the Nitro `Promise` to the domain layer's `RCTPromiseResolveBlock`/
9
+ // `RCTPromiseRejectBlock`, and registers progress callbacks (keyed by `uuid`)
10
+ // with `EventEmitterHandler`. All heavy logic stays in the domain classes.
11
+ //
12
+
13
+ import Foundation
14
+ import NitroModules
15
+
16
+ // Previously imported from React. We define them locally so this Swift module no
17
+ // longer needs to `import React`: under Nitro's Swift↔C++ interop, importing
18
+ // React pulls move-only C++ types (e.g. jsinspector's RuntimeSamplingProfile)
19
+ // into Swift's importer and fails to compile. These blocks are created here and
20
+ // consumed by the domain layer entirely in Swift, so plain closures suffice.
21
+ typealias RCTPromiseResolveBlock = (Any?) -> Void
22
+ typealias RCTPromiseRejectBlock = (String?, String?, (any Error)?) -> Void
23
+
24
+ private let videoCompressor = VideoCompressor()
25
+ private let uploader = Uploader()
26
+
27
+ final class HybridCompressor: HybridCompressorSpec {
28
+ // MARK: - Image
29
+
30
+ func image_compress(imagePath: String, optionMap: AnyMap, onDownloadProgress: ((Double) -> Void)?) throws -> Promise<String> {
31
+ let promise = Promise<String>()
32
+ var options = dictionary(from: optionMap)
33
+ // Remote-image download progress is keyed by `uuid` inside the Downloader. The JS
34
+ // layer no longer sends one (the callback is passed directly), so mint one here.
35
+ let uuid = (options["uuid"] as? String) ?? UUID().uuidString
36
+ options["uuid"] = uuid
37
+ EventEmitterHandler.registerDownloadProgress(uuid: uuid, onDownloadProgress)
38
+ ImageMain.image_compress(imagePath, optionMap: options as NSDictionary, resolve: resolveString(promise, uuid), reject: reject(promise, uuid))
39
+ return promise
40
+ }
41
+
42
+ func getImageMetaData(filePath: String) throws -> Promise<AnyMap> {
43
+ let promise = Promise<AnyMap>()
44
+ ImageMain.getImageMetaData(filePath, resolve: resolveAnyMap(promise, nil), reject: reject(promise, nil))
45
+ return promise
46
+ }
47
+
48
+ // MARK: - Video
49
+
50
+ func compress(fileUrl: String, optionMap: AnyMap, onProgress: ((Double) -> Void)?, onDownloadProgress: ((Double) -> Void)?) throws -> Promise<String> {
51
+ let promise = Promise<String>()
52
+ let options = dictionary(from: optionMap)
53
+ let uuid = (options["uuid"] as? String) ?? ""
54
+ EventEmitterHandler.registerVideoCompressProgress(uuid: uuid, onProgress)
55
+ EventEmitterHandler.registerDownloadProgress(uuid: uuid, onDownloadProgress)
56
+ videoCompressor.compress(fileUrl: fileUrl, options: options, resolve: resolveString(promise, uuid), reject: reject(promise, uuid))
57
+ return promise
58
+ }
59
+
60
+ func cancelCompression(uuid: String) throws {
61
+ videoCompressor.cancelCompression(uuid: uuid)
62
+ }
63
+
64
+ func getVideoMetaData(filePath: String) throws -> Promise<AnyMap> {
65
+ let promise = Promise<AnyMap>()
66
+ videoCompressor.getVideoMetaData(filePath, resolve: resolveAnyMap(promise, nil), reject: reject(promise, nil))
67
+ return promise
68
+ }
69
+
70
+ func activateBackgroundTask(options: AnyMap, onExpired: (() -> Void)?) throws -> Promise<String> {
71
+ let promise = Promise<String>()
72
+ EventEmitterHandler.setBackgroundTaskExpiredCallback(onExpired)
73
+ videoCompressor.activateBackgroundTask(options: dictionary(from: options), resolve: resolveString(promise, nil), reject: reject(promise, nil))
74
+ return promise
75
+ }
76
+
77
+ func deactivateBackgroundTask(options: AnyMap) throws -> Promise<String> {
78
+ let promise = Promise<String>()
79
+ EventEmitterHandler.setBackgroundTaskExpiredCallback(nil)
80
+ videoCompressor.deactivateBackgroundTask(options: dictionary(from: options), resolve: resolveString(promise, nil), reject: reject(promise, nil))
81
+ return promise
82
+ }
83
+
84
+ // MARK: - Audio
85
+
86
+ func compress_audio(fileUrl: String, optionMap: AnyMap) throws -> Promise<String> {
87
+ let promise = Promise<String>()
88
+ AudioMain.compress_audio(fileUrl, optionMap: dictionary(from: optionMap) as NSDictionary, resolve: resolveString(promise, nil), reject: reject(promise, nil))
89
+ return promise
90
+ }
91
+
92
+ // MARK: - Upload / Download
93
+
94
+ func upload(fileUrl: String, options: AnyMap, onProgress: ((Double, Double) -> Void)?) throws -> Promise<AnyMap> {
95
+ let promise = Promise<AnyMap>()
96
+ let dict = dictionary(from: options)
97
+ let uuid = (dict["uuid"] as? String) ?? ""
98
+ EventEmitterHandler.registerUploadProgress(uuid: uuid, onProgress)
99
+ uploader.upload(filePath: fileUrl, options: dict, resolve: resolveAnyMap(promise, uuid), reject: reject(promise, uuid))
100
+ return promise
101
+ }
102
+
103
+ func cancelUpload(uuid: String, shouldCancelAll: Bool) throws {
104
+ uploader.cancelUpload(uuid: uuid, shouldCancelAll: shouldCancelAll)
105
+ }
106
+
107
+ func download(fileUrl: String, options: AnyMap, onProgress: ((Double) -> Void)?) throws -> Promise<String> {
108
+ let promise = Promise<String>()
109
+ let dict = dictionary(from: options)
110
+ let uuid = (dict["uuid"] as? String) ?? ""
111
+ let progressDivider = (dict["progressDivider"] as? NSNumber)?.intValue ?? 0
112
+ EventEmitterHandler.registerDownloadProgress(uuid: uuid, onProgress)
113
+ Downloader.downloadFileAndSaveToCache(fileUrl, uuid: uuid, progressDivider: progressDivider) { downloadedPath in
114
+ EventEmitterHandler.unregister(uuid: uuid)
115
+ promise.resolve(withResult: downloadedPath)
116
+ }
117
+ return promise
118
+ }
119
+
120
+ // MARK: - Others
121
+
122
+ func generateFilePath(fileExtension: String) throws -> Promise<String> {
123
+ let promise = Promise<String>()
124
+ Utils.generateFilePath(fileExtension, resolve: resolveString(promise, nil), reject: reject(promise, nil))
125
+ return promise
126
+ }
127
+
128
+ func getRealPath(path: String, type: String) throws -> Promise<String> {
129
+ let promise = Promise<String>()
130
+ Utils.getRealPath(path, type: type, resolve: resolveString(promise, nil), reject: reject(promise, nil))
131
+ return promise
132
+ }
133
+
134
+ func getFileSize(filePath: String) throws -> Promise<String> {
135
+ let promise = Promise<String>()
136
+ Utils.getFileSize(filePath, resolve: resolveString(promise, nil), reject: reject(promise, nil))
137
+ return promise
138
+ }
139
+
140
+ func createVideoThumbnail(fileUrl: String, options: AnyMap) throws -> Promise<VideoThumbnailResult> {
141
+ let promise = Promise<VideoThumbnailResult>()
142
+ let thumbnail = CreateVideoThumbnail()
143
+ thumbnail.create(fileUrl, options: dictionary(from: options) as NSDictionary, resolve: { value in
144
+ let dict = HybridCompressor.stringKeyedDictionary(value)
145
+ let result = VideoThumbnailResult(
146
+ path: dict["path"] as? String ?? "",
147
+ size: HybridCompressor.doubleValue(dict["size"]),
148
+ mime: dict["mime"] as? String ?? "",
149
+ width: HybridCompressor.doubleValue(dict["width"]),
150
+ height: HybridCompressor.doubleValue(dict["height"])
151
+ )
152
+ promise.resolve(withResult: result)
153
+ }, rejecter: reject(promise, nil))
154
+ return promise
155
+ }
156
+
157
+ func clearCache(cacheDir: String?) throws -> Promise<String> {
158
+ let promise = Promise<String>()
159
+ CreateVideoThumbnail.cleanCacheDir(cacheDir: cacheDir ?? "", resolve: resolveString(promise, nil), rejecter: reject(promise, nil))
160
+ return promise
161
+ }
162
+
163
+ // MARK: - Helpers
164
+
165
+ /// Convert a Nitro `AnyMap` to a `[String: Any]`, round-tripping through
166
+ /// `NSDictionary` so numbers are boxed as `NSNumber` (the domain parsers rely
167
+ /// on `as? Int` / `as? Bool`, which fail on a bare Swift `Double`).
168
+ private func dictionary(from map: AnyMap) -> [String: Any] {
169
+ let normalized = (HybridCompressor.normalize(map.toDictionary()) as? [String: Any]) ?? [:]
170
+ return ((normalized as NSDictionary) as? [String: Any]) ?? normalized
171
+ }
172
+
173
+ private func resolveString(_ promise: Promise<String>, _ uuid: String?) -> RCTPromiseResolveBlock {
174
+ return { value in
175
+ if let uuid = uuid { EventEmitterHandler.unregister(uuid: uuid) }
176
+ promise.resolve(withResult: value as? String ?? "")
177
+ }
178
+ }
179
+
180
+ private func resolveAnyMap(_ promise: Promise<AnyMap>, _ uuid: String?) -> RCTPromiseResolveBlock {
181
+ return { value in
182
+ if let uuid = uuid { EventEmitterHandler.unregister(uuid: uuid) }
183
+ promise.resolve(withResult: AnyMap.fromDictionaryIgnoreIncompatible(HybridCompressor.stringKeyedOptionalDictionary(value)))
184
+ }
185
+ }
186
+
187
+ private func reject<T>(_ promise: Promise<T>, _ uuid: String?) -> RCTPromiseRejectBlock {
188
+ return { code, message, error in
189
+ if let uuid = uuid { EventEmitterHandler.unregister(uuid: uuid) }
190
+ let nsError = error ?? RuntimeError.error(withMessage: message ?? code ?? "react-native-compressor error")
191
+ promise.reject(withError: nsError)
192
+ }
193
+ }
194
+
195
+ /// Recursively strip optionals so a `[String: Any?]` (what `AnyMap.toDictionary()`
196
+ /// returns, including nested objects/arrays) becomes a plain `[String: Any]`.
197
+ private static func normalize(_ value: Any?) -> Any? {
198
+ switch value {
199
+ case let dict as [String: Any?]:
200
+ var out = [String: Any]()
201
+ for (key, nested) in dict {
202
+ if let nestedValue = normalize(nested) { out[key] = nestedValue }
203
+ }
204
+ return out
205
+ case let array as [Any?]:
206
+ return array.compactMap { normalize($0) }
207
+ default:
208
+ return value
209
+ }
210
+ }
211
+
212
+ private static func stringKeyedDictionary(_ value: Any?) -> [String: Any] {
213
+ if let dict = value as? [String: Any] { return dict }
214
+ if let ns = value as? NSDictionary {
215
+ var out = [String: Any]()
216
+ for (key, val) in ns where key is String {
217
+ out[key as! String] = val
218
+ }
219
+ return out
220
+ }
221
+ return [:]
222
+ }
223
+
224
+ private static func stringKeyedOptionalDictionary(_ value: Any?) -> [String: Any?] {
225
+ if let ns = value as? NSDictionary {
226
+ var out = [String: Any?]()
227
+ for (key, val) in ns where key is String {
228
+ out[key as! String] = val
229
+ }
230
+ return out
231
+ }
232
+ if let dict = value as? [String: Any] {
233
+ var out = [String: Any?]()
234
+ for (key, val) in dict { out[key] = val }
235
+ return out
236
+ }
237
+ return [:]
238
+ }
239
+
240
+ private static func doubleValue(_ value: Any?) -> Double {
241
+ if let number = value as? NSNumber { return number.doubleValue }
242
+ if let double = value as? Double { return double }
243
+ if let float = value as? Float { return Double(float) }
244
+ if let int = value as? Int { return Double(int) }
245
+ return 0
246
+ }
247
+ }
@@ -1,11 +1,17 @@
1
1
  import Accelerate
2
2
  import CoreGraphics
3
3
  import Photos
4
- import React
5
4
  import Foundation
6
5
  import MobileCoreServices
7
6
 
8
7
 
8
+ // Local stand-in for the single `RCTResizeMode` case this file relies on, so the
9
+ // module no longer needs to `import React` (see HybridCompressor for the reason).
10
+ private enum CompressorResizeMode {
11
+ case contain
12
+ }
13
+
14
+
9
15
  class ImageCompressor {
10
16
  static func findTargetSize(_ image: UIImage, maxWidth: Int, maxHeight: Int) -> CGSize {
11
17
  let width = image.size.width
@@ -392,7 +398,7 @@ class ImageCompressor {
392
398
  let imageURL = URL(string: imagePath.replacingOccurrences(of: " ", with: "%20"))
393
399
  var size = CGSize.zero
394
400
  var scale: CGFloat = 1
395
- var resizeMode = RCTResizeMode.contain
401
+ var resizeMode = CompressorResizeMode.contain
396
402
  let assetID = imagePath.replacingOccurrences(of: "ph://", with: "")
397
403
  let results = PHAsset.fetchAssets(withLocalIdentifiers: [assetID], options: nil)
398
404
 
@@ -6,49 +6,71 @@
6
6
  //
7
7
 
8
8
  import Foundation
9
+ import UIKit
9
10
 
11
+ /// Routes native progress emissions to the per-call JS callbacks that
12
+ /// `HybridCompressor` registers. This replaces the old `RCTEventEmitter` bridge:
13
+ /// Nitro delivers progress through callback parameters, so the domain layer's
14
+ /// `emit*` calls (unchanged) are dispatched to the callback registered under the
15
+ /// same `uuid` that the JS layer threads through the options map.
10
16
  class EventEmitterHandler {
11
- static var sharedCompressorObject: Any!
12
- static var hasListener: Bool=false
13
-
14
- static func initCompressorInstance(_ object: Any) {
15
- sharedCompressorObject = object
17
+ private static let lock = NSLock()
18
+ private static var videoCompressProgressCallbacks = [String: (Double) -> Void]()
19
+ private static var downloadProgressCallbacks = [String: (Double) -> Void]()
20
+ private static var uploadProgressCallbacks = [String: (Double, Double) -> Void]()
21
+ private static var backgroundTaskExpiredCallback: (() -> Void)?
22
+
23
+ // MARK: - Registration (called by HybridCompressor)
24
+
25
+ static func registerVideoCompressProgress(uuid: String, _ callback: ((Double) -> Void)?) {
26
+ guard let callback = callback else { return }
27
+ lock.lock(); defer { lock.unlock() }
28
+ videoCompressProgressCallbacks[uuid] = callback
29
+ }
30
+
31
+ static func registerDownloadProgress(uuid: String, _ callback: ((Double) -> Void)?) {
32
+ guard let callback = callback else { return }
33
+ lock.lock(); defer { lock.unlock() }
34
+ downloadProgressCallbacks[uuid] = callback
16
35
  }
17
-
18
-
19
- static func stopObserving() -> Void {
20
- hasListener = false
36
+
37
+ static func registerUploadProgress(uuid: String, _ callback: ((Double, Double) -> Void)?) {
38
+ guard let callback = callback else { return }
39
+ lock.lock(); defer { lock.unlock() }
40
+ uploadProgressCallbacks[uuid] = callback
41
+ }
42
+
43
+ static func unregister(uuid: String) {
44
+ lock.lock(); defer { lock.unlock() }
45
+ videoCompressProgressCallbacks[uuid] = nil
46
+ downloadProgressCallbacks[uuid] = nil
47
+ uploadProgressCallbacks[uuid] = nil
21
48
  }
22
49
 
23
- static func startObserving() -> Void {
24
- hasListener = true
50
+ static func setBackgroundTaskExpiredCallback(_ callback: (() -> Void)?) {
51
+ lock.lock(); defer { lock.unlock() }
52
+ backgroundTaskExpiredCallback = callback
25
53
  }
26
-
54
+
55
+ // MARK: - Emission (called by the domain layer — method names preserved)
56
+
27
57
  static func emitDownloadProgress(_ progress: NSNumber, uuid: String) {
28
- var params = [String: Any]()
29
- var data = [String: Any]()
30
- params["uuid"] = uuid
31
- data["progress"] = progress
32
- params["data"] = data
33
-
34
- (sharedCompressorObject as AnyObject).sendEvent(withName: "downloadProgress", body: params)
35
- }
36
-
58
+ lock.lock(); let callback = downloadProgressCallbacks[uuid]; lock.unlock()
59
+ callback?(progress.doubleValue)
60
+ }
61
+
37
62
  static func emitVideoCompressProgress(_ progress: Float, uuid: String) {
38
- if(self.hasListener){
39
- (sharedCompressorObject as AnyObject).sendEvent(withName: "videoCompressProgress", body: ["uuid": uuid, "data": ["progress": progress]])
40
- }
63
+ lock.lock(); let callback = videoCompressProgressCallbacks[uuid]; lock.unlock()
64
+ callback?(Double(progress))
41
65
  }
42
-
66
+
43
67
  static func emituploadProgress(_ uuid: String, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
44
- if(self.hasListener){
45
- (sharedCompressorObject as AnyObject).sendEvent(withName: "uploadProgress", body: ["uuid": uuid, "data": ["written": totalBytesSent, "total": totalBytesExpectedToSend]])
46
- }
47
- }
48
-
49
- static func emitBackgroundTaskExpired(_ backgroundTaskId:UIBackgroundTaskIdentifier) {
50
- if(self.hasListener){
51
- (sharedCompressorObject as AnyObject).sendEvent(withName: "backgroundTaskExpired", body: ["backgroundTaskId": backgroundTaskId])
52
- }
68
+ lock.lock(); let callback = uploadProgressCallbacks[uuid]; lock.unlock()
69
+ callback?(Double(totalBytesSent), Double(totalBytesExpectedToSend))
70
+ }
71
+
72
+ static func emitBackgroundTaskExpired(_ backgroundTaskId: UIBackgroundTaskIdentifier) {
73
+ lock.lock(); let callback = backgroundTaskExpiredCallback; lock.unlock()
74
+ callback?()
53
75
  }
54
76
  }