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.
- package/android/CMakeLists.txt +27 -0
- package/android/build.gradle +56 -58
- package/android/gradle.properties +5 -5
- package/android/src/main/AndroidManifestNew.xml +3 -0
- package/android/src/main/cpp/cpp-adapter.cpp +8 -0
- package/android/src/main/java/com/margelo/nitro/compressor/HybridCompressor.kt +213 -0
- package/android/src/main/java/com/reactnativecompressor/NitroCompressorPackage.kt +30 -0
- package/android/src/main/java/com/reactnativecompressor/NitroPromiseAdapter.kt +55 -0
- package/android/src/main/java/com/reactnativecompressor/Utils/EventEmitterHandler.kt +49 -45
- package/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt +1 -1
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt +13 -22
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt +72 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt +4 -1
- package/ios/Audio/FormatConverter/FormatConverter+Compressed.swift +1 -1
- package/ios/Audio/FormatConverter/FormatConverter+Utilities.swift +1 -1
- package/ios/Audio/FormatConverter/FormatConverter.swift +5 -3
- package/ios/HybridCompressor.swift +247 -0
- package/ios/Image/ImageCompressor.swift +8 -2
- package/ios/Utils/EventEmitterHandler.swift +56 -34
- package/ios/Video/NextLevelSessionExporter.swift +43 -8
- package/ios/Video/VideoMain.swift +17 -3
- package/lib/commonjs/Audio/index.js +1 -1
- package/lib/commonjs/Audio/index.js.map +1 -1
- package/lib/commonjs/Image/index.js +2 -22
- package/lib/commonjs/Image/index.js.map +1 -1
- package/lib/commonjs/Main.js +8 -8
- package/lib/commonjs/Main.js.map +1 -1
- package/lib/commonjs/Video/index.js +25 -65
- package/lib/commonjs/Video/index.js.map +1 -1
- package/lib/commonjs/specs/Compressor.nitro.js +6 -0
- package/lib/commonjs/specs/Compressor.nitro.js.map +1 -0
- package/lib/commonjs/utils/Downloader.js +8 -24
- package/lib/commonjs/utils/Downloader.js.map +1 -1
- package/lib/commonjs/utils/Uploader.js +3 -18
- package/lib/commonjs/utils/Uploader.js.map +1 -1
- package/lib/commonjs/utils/helpers.js +18 -1
- package/lib/commonjs/utils/helpers.js.map +1 -1
- package/lib/commonjs/utils/index.js +13 -13
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/module/Audio/index.js +2 -2
- package/lib/module/Audio/index.js.map +1 -1
- package/lib/module/Image/index.js +3 -23
- package/lib/module/Image/index.js.map +1 -1
- package/lib/module/Main.js +9 -9
- package/lib/module/Main.js.map +1 -1
- package/lib/module/Video/index.js +26 -66
- package/lib/module/Video/index.js.map +1 -1
- package/lib/module/specs/Compressor.nitro.js +4 -0
- package/lib/module/specs/Compressor.nitro.js.map +1 -0
- package/lib/module/utils/Downloader.js +10 -26
- package/lib/module/utils/Downloader.js.map +1 -1
- package/lib/module/utils/Uploader.js +5 -20
- package/lib/module/utils/Uploader.js.map +1 -1
- package/lib/module/utils/helpers.js +16 -0
- package/lib/module/utils/helpers.js.map +1 -1
- package/lib/module/utils/index.js +2 -1
- package/lib/module/utils/index.js.map +1 -1
- package/lib/typescript/src/Image/index.d.ts.map +1 -1
- package/lib/typescript/src/Main.d.ts +2 -1
- package/lib/typescript/src/Main.d.ts.map +1 -1
- package/lib/typescript/src/Video/index.d.ts +1 -1
- package/lib/typescript/src/Video/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/Compressor.nitro.d.ts +44 -0
- package/lib/typescript/src/specs/Compressor.nitro.d.ts.map +1 -0
- package/lib/typescript/src/utils/Downloader.d.ts.map +1 -1
- package/lib/typescript/src/utils/Uploader.d.ts.map +1 -1
- package/lib/typescript/src/utils/helpers.d.ts +7 -0
- package/lib/typescript/src/utils/helpers.d.ts.map +1 -1
- package/lib/typescript/src/utils/index.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroCompressor+autolinking.cmake +81 -0
- package/nitrogen/generated/android/NitroCompressor+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroCompressorOnLoad.cpp +60 -0
- package/nitrogen/generated/android/NitroCompressorOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridCompressorSpec.cpp +293 -0
- package/nitrogen/generated/android/c++/JHybridCompressorSpec.hpp +78 -0
- package/nitrogen/generated/android/c++/JVideoThumbnailResult.hpp +73 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/HybridCompressorSpec.kt +141 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/NitroCompressorOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/VideoThumbnailResult.kt +71 -0
- package/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.hpp +204 -0
- package/nitrogen/generated/ios/react_native_compressor+autolinking.rb +62 -0
- package/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.cpp +89 -0
- package/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.hpp +337 -0
- package/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Umbrella.hpp +51 -0
- package/nitrogen/generated/ios/react_native_compressorAutolinking.mm +33 -0
- package/nitrogen/generated/ios/react_native_compressorAutolinking.swift +26 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_VideoThumbnailResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_AnyMap_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridCompressorSpec.swift +70 -0
- package/nitrogen/generated/ios/swift/HybridCompressorSpec_cxx.swift +493 -0
- package/nitrogen/generated/ios/swift/VideoThumbnailResult.swift +49 -0
- package/nitrogen/generated/shared/c++/HybridCompressorSpec.cpp +36 -0
- package/nitrogen/generated/shared/c++/HybridCompressorSpec.hpp +83 -0
- package/nitrogen/generated/shared/c++/VideoThumbnailResult.hpp +99 -0
- package/package.json +8 -12
- package/react-native-compressor.podspec +6 -24
- package/src/Audio/index.tsx +2 -2
- package/src/Image/index.tsx +3 -26
- package/src/Main.tsx +14 -16
- package/src/Video/index.tsx +25 -77
- package/src/specs/Compressor.nitro.ts +56 -0
- package/src/utils/Downloader.tsx +8 -27
- package/src/utils/Uploader.tsx +12 -24
- package/src/utils/helpers.ts +18 -0
- package/src/utils/index.tsx +3 -2
- package/android/src/main/java/com/reactnativecompressor/CompressorModule.kt +0 -180
- package/android/src/main/java/com/reactnativecompressor/CompressorPackage.kt +0 -34
- package/android/src/newarch/CompressorSpec.kt +0 -5
- package/android/src/oldarch/CompressorSpec.kt +0 -36
- package/ios/Compressor-Bridging-Header.h +0 -6
- package/ios/Compressor.h +0 -5
- package/ios/Compressor.mm +0 -82
- package/ios/CompressorManager.swift +0 -117
- package/lib/commonjs/Spec/NativeCompressor.js +0 -9
- package/lib/commonjs/Spec/NativeCompressor.js.map +0 -1
- package/lib/module/Spec/NativeCompressor.js +0 -5
- package/lib/module/Spec/NativeCompressor.js.map +0 -1
- package/lib/typescript/src/Spec/NativeCompressor.d.ts +0 -30
- package/lib/typescript/src/Spec/NativeCompressor.d.ts.map +0 -1
- package/src/Spec/NativeCompressor.ts +0 -41
|
@@ -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
|
-
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ""
|
|
@@ -19,7 +19,9 @@ import AVFoundation
|
|
|
19
19
|
}
|
|
20
20
|
```
|
|
21
21
|
*/
|
|
22
|
-
public
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
12
|
-
static var
|
|
13
|
-
|
|
14
|
-
static
|
|
15
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
24
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
}
|