react-native-compressor 1.19.2 → 2.0.1
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.java +105 -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 +2 -0
- 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/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
|
@@ -1,66 +1,70 @@
|
|
|
1
1
|
package com.reactnativecompressor.Utils
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
-
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
3
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
6
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Routes native progress emissions to the per-call JS callbacks that
|
|
7
|
+
* `HybridCompressor` registers. This replaces the old `RCTDeviceEventEmitter`
|
|
8
|
+
* bridge: Nitro delivers progress through callback parameters, so the domain
|
|
9
|
+
* layer's `emit*` calls (unchanged) are dispatched to the callback registered
|
|
10
|
+
* under the same `uuid` that the JS layer threads through the options map.
|
|
11
|
+
*/
|
|
7
12
|
class EventEmitterHandler {
|
|
8
13
|
companion object {
|
|
9
|
-
|
|
14
|
+
private val videoCompressProgressCallbacks = ConcurrentHashMap<String, (Double) -> Unit>()
|
|
15
|
+
private val downloadProgressCallbacks = ConcurrentHashMap<String, (Double) -> Unit>()
|
|
16
|
+
private val uploadProgressCallbacks = ConcurrentHashMap<String, (Double, Double) -> Unit>()
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
@Volatile
|
|
19
|
+
private var backgroundTaskExpiredCallback: (() -> Unit)? = null
|
|
20
|
+
|
|
21
|
+
// Registration (called by HybridCompressor)
|
|
22
|
+
|
|
23
|
+
fun registerVideoCompressProgress(uuid: String, callback: ((Double) -> Unit)?) {
|
|
24
|
+
if (callback != null) videoCompressProgressCallbacks[uuid] = callback
|
|
16
25
|
}
|
|
17
26
|
|
|
18
|
-
fun
|
|
19
|
-
|
|
27
|
+
fun registerDownloadProgress(uuid: String, callback: ((Double) -> Unit)?) {
|
|
28
|
+
if (callback != null) downloadProgressCallbacks[uuid] = callback
|
|
20
29
|
}
|
|
21
30
|
|
|
22
|
-
fun
|
|
23
|
-
|
|
24
|
-
val data = Arguments.createMap()
|
|
25
|
-
params.putString("uuid", uuid)
|
|
26
|
-
data.putDouble("progress", progress)
|
|
27
|
-
params.putMap("data", data)
|
|
28
|
-
sendEvent("videoCompressProgress", params)
|
|
31
|
+
fun registerUploadProgress(uuid: String, callback: ((Double, Double) -> Unit)?) {
|
|
32
|
+
if (callback != null) uploadProgressCallbacks[uuid] = callback
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
fun
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
data.putDouble("progress", progress)
|
|
36
|
-
params.putMap("data", data)
|
|
37
|
-
sendEvent("downloadProgress", params)
|
|
35
|
+
fun unregister(uuid: String) {
|
|
36
|
+
videoCompressProgressCallbacks.remove(uuid)
|
|
37
|
+
downloadProgressCallbacks.remove(uuid)
|
|
38
|
+
uploadProgressCallbacks.remove(uuid)
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
fun
|
|
41
|
-
|
|
42
|
-
val params = Arguments.createMap()
|
|
43
|
-
val data = Arguments.createMap()
|
|
44
|
-
params.putString("uuid", uuid)
|
|
45
|
-
params.putString("error", error)
|
|
46
|
-
params.putMap("data", data)
|
|
47
|
-
sendEvent("downloadProgressError", params)
|
|
48
|
-
}
|
|
41
|
+
fun setBackgroundTaskExpiredCallback(callback: (() -> Unit)?) {
|
|
42
|
+
backgroundTaskExpiredCallback = callback
|
|
49
43
|
}
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
_params.putString("uuid", uuid)
|
|
56
|
-
_data.putDouble("written", numBytes.toDouble())
|
|
57
|
-
_data.putDouble("total", totalBytes.toDouble())
|
|
58
|
-
_params.putMap("data", _data)
|
|
59
|
-
sendEvent("uploadProgress", _params)
|
|
60
|
-
}
|
|
45
|
+
// Emission (called by the domain layer — method names preserved)
|
|
46
|
+
|
|
47
|
+
fun emitBackgroundTaskExpired(backgroundId: String?) {
|
|
48
|
+
backgroundTaskExpiredCallback?.invoke()
|
|
61
49
|
}
|
|
62
50
|
|
|
51
|
+
fun emitVideoCompressProgress(progress: Double, uuid: String) {
|
|
52
|
+
videoCompressProgressCallbacks[uuid]?.invoke(progress)
|
|
53
|
+
}
|
|
63
54
|
|
|
64
|
-
|
|
55
|
+
fun emitDownloadProgress(progress: Double, uuid: String) {
|
|
56
|
+
downloadProgressCallbacks[uuid]?.invoke(progress)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun emitDownloadProgressError(uuid: String?, error: String?) {
|
|
60
|
+
// No JS consumer for `downloadProgressError`; download failures surface
|
|
61
|
+
// through the rejected Promise instead. Kept so domain call sites compile.
|
|
62
|
+
}
|
|
65
63
|
|
|
64
|
+
fun sendUploadProgressEvent(numBytes: Long, totalBytes: Long, uuid: String?) {
|
|
65
|
+
if (uuid != null) {
|
|
66
|
+
uploadProgressCallbacks[uuid]?.invoke(numBytes.toDouble(), totalBytes.toDouble())
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
66
70
|
}
|
|
@@ -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,6 +789,7 @@ object Compressor {
|
|
|
788
789
|
inputFormat: MediaFormat,
|
|
789
790
|
outputSurface: OutputSurface,
|
|
790
791
|
): MediaCodec {
|
|
792
|
+
|
|
791
793
|
// Some inputs (e.g. iPhone .MOV files) report a "video/dolby-vision" MIME
|
|
792
794
|
// type that many devices cannot decode. Remap to a decodable base-layer
|
|
793
795
|
// codec, or fail with a clear error, before creating the decoder (#398).
|
|
@@ -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
|
|