react-native-rn-media-compressor 0.1.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/LICENSE +20 -0
- package/README.md +37 -0
- package/android/build.gradle +42 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/rnmediacompressor/RnMediaCompressorModule.java +206 -0
- package/android/src/main/java/com/rnmediacompressor/RnMediaCompressorPackage.java +29 -0
- package/ios/RnMediaCompressor.h +6 -0
- package/ios/RnMediaCompressor.m +343 -0
- package/lib/module/index.js +89 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/index.d.ts +54 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +160 -0
- package/react-native-rn-media-compressor.podspec +22 -0
- package/react-native.config.js +13 -0
- package/src/index.tsx +153 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Siddesh7972
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# react-native-rn-media-compressor
|
|
2
|
+
|
|
3
|
+
This package will reduce the size of media file before uplaoding it to server
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install react-native-rn-media-compressor
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import { multiply } from 'react-native-rn-media-compressor';
|
|
18
|
+
|
|
19
|
+
// ...
|
|
20
|
+
|
|
21
|
+
const result = await multiply(3, 7);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Contributing
|
|
26
|
+
|
|
27
|
+
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
28
|
+
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
29
|
+
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext {
|
|
3
|
+
buildToolsVersion = "34.0.0"
|
|
4
|
+
minSdkVersion = 21
|
|
5
|
+
compileSdkVersion = 34
|
|
6
|
+
targetSdkVersion = 34
|
|
7
|
+
ndkVersion = "25.1.8937393"
|
|
8
|
+
kotlinVersion = "1.8.0"
|
|
9
|
+
}
|
|
10
|
+
repositories {
|
|
11
|
+
google()
|
|
12
|
+
mavenCentral()
|
|
13
|
+
}
|
|
14
|
+
dependencies {
|
|
15
|
+
classpath("com.android.tools.build:gradle")
|
|
16
|
+
classpath("com.facebook.react:react-native-gradle-plugin")
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
apply plugin: "com.android.library"
|
|
21
|
+
apply plugin: "com.facebook.react-native"
|
|
22
|
+
|
|
23
|
+
android {
|
|
24
|
+
ndkVersion rootProject.ext.ndkVersion
|
|
25
|
+
compileSdkVersion rootProject.ext.compileSdkVersion
|
|
26
|
+
|
|
27
|
+
namespace "com.rnmediacompressor"
|
|
28
|
+
defaultConfig {
|
|
29
|
+
minSdkVersion rootProject.ext.minSdkVersion
|
|
30
|
+
targetSdkVersion rootProject.ext.targetSdkVersion
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
repositories {
|
|
35
|
+
mavenCentral()
|
|
36
|
+
google()
|
|
37
|
+
maven { url "https://www.jitpack.io" }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
dependencies {
|
|
41
|
+
implementation "com.facebook.react:react-native"
|
|
42
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
package com.rnmediacompressor;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap;
|
|
4
|
+
import android.graphics.BitmapFactory;
|
|
5
|
+
import android.media.MediaMetadataRetriever;
|
|
6
|
+
import android.util.Log;
|
|
7
|
+
|
|
8
|
+
import androidx.annotation.NonNull;
|
|
9
|
+
|
|
10
|
+
import com.facebook.react.bridge.Arguments;
|
|
11
|
+
import com.facebook.react.bridge.Promise;
|
|
12
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
13
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
14
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
15
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
16
|
+
import com.facebook.react.bridge.WritableMap;
|
|
17
|
+
|
|
18
|
+
import java.io.File;
|
|
19
|
+
import java.io.FileOutputStream;
|
|
20
|
+
import java.util.concurrent.ExecutorService;
|
|
21
|
+
import java.util.concurrent.Executors;
|
|
22
|
+
|
|
23
|
+
public class RnMediaCompressorModule extends ReactContextBaseJavaModule {
|
|
24
|
+
private static final String MODULE_NAME = "RnMediaCompressor";
|
|
25
|
+
private static final String TAG = "RnMediaCompressor";
|
|
26
|
+
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
|
27
|
+
|
|
28
|
+
public RnMediaCompressorModule(ReactApplicationContext reactContext) {
|
|
29
|
+
super(reactContext);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Override
|
|
33
|
+
public void onCatalystInstanceDestroy() {
|
|
34
|
+
super.onCatalystInstanceDestroy();
|
|
35
|
+
executorService.shutdown();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@NonNull
|
|
39
|
+
@Override
|
|
40
|
+
public String getName() {
|
|
41
|
+
return MODULE_NAME;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@ReactMethod
|
|
45
|
+
public void compressImage(String filePath, ReadableMap options, Promise promise) {
|
|
46
|
+
executorService.execute(() -> {
|
|
47
|
+
compressImageAsync(filePath, options, promise);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@ReactMethod
|
|
52
|
+
public void compressVideo(String filePath, ReadableMap options, Promise promise) {
|
|
53
|
+
promise.reject("NOT_IMPLEMENTED", "Video compression is not yet implemented for Android");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@ReactMethod
|
|
57
|
+
public void getMediaInfo(String filePath, Promise promise) {
|
|
58
|
+
try {
|
|
59
|
+
File file = new File(filePath);
|
|
60
|
+
if (!file.exists()) {
|
|
61
|
+
promise.reject("FILE_NOT_FOUND", "File does not exist: " + filePath);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
WritableMap info = Arguments.createMap();
|
|
66
|
+
info.putDouble("size", file.length());
|
|
67
|
+
|
|
68
|
+
// Try to get image dimensions
|
|
69
|
+
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
|
|
70
|
+
bitmapOptions.inJustDecodeBounds = true;
|
|
71
|
+
BitmapFactory.decodeFile(filePath, bitmapOptions);
|
|
72
|
+
|
|
73
|
+
if (bitmapOptions.outWidth > 0 && bitmapOptions.outHeight > 0) {
|
|
74
|
+
info.putInt("width", bitmapOptions.outWidth);
|
|
75
|
+
info.putInt("height", bitmapOptions.outHeight);
|
|
76
|
+
info.putString("mimeType", bitmapOptions.outMimeType);
|
|
77
|
+
} else {
|
|
78
|
+
// Try video
|
|
79
|
+
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
|
80
|
+
try {
|
|
81
|
+
retriever.setDataSource(filePath);
|
|
82
|
+
String widthStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
|
|
83
|
+
String heightStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
|
|
84
|
+
String durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
|
|
85
|
+
String mimeType = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE);
|
|
86
|
+
|
|
87
|
+
if (widthStr != null) {
|
|
88
|
+
info.putInt("width", Integer.parseInt(widthStr));
|
|
89
|
+
}
|
|
90
|
+
if (heightStr != null) {
|
|
91
|
+
info.putInt("height", Integer.parseInt(heightStr));
|
|
92
|
+
}
|
|
93
|
+
if (durationStr != null) {
|
|
94
|
+
info.putDouble("duration", Double.parseDouble(durationStr) / 1000.0);
|
|
95
|
+
}
|
|
96
|
+
if (mimeType != null) {
|
|
97
|
+
info.putString("mimeType", mimeType);
|
|
98
|
+
}
|
|
99
|
+
} catch (Exception e) {
|
|
100
|
+
Log.e(TAG, "Error getting video metadata", e);
|
|
101
|
+
} finally {
|
|
102
|
+
retriever.release();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
promise.resolve(info);
|
|
107
|
+
} catch (Exception e) {
|
|
108
|
+
promise.reject("ERROR", "Failed to get media info: " + e.getMessage(), e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private void compressImageAsync(String filePath, ReadableMap options, Promise promise) {
|
|
113
|
+
try {
|
|
114
|
+
File inputFile = new File(filePath);
|
|
115
|
+
if (!inputFile.exists()) {
|
|
116
|
+
promise.reject("FILE_NOT_FOUND", "File does not exist: " + filePath);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
long originalSize = inputFile.length();
|
|
121
|
+
|
|
122
|
+
// Read options
|
|
123
|
+
int maxWidth = options.hasKey("maxWidth") ? options.getInt("maxWidth") : 0;
|
|
124
|
+
int maxHeight = options.hasKey("maxHeight") ? options.getInt("maxHeight") : 0;
|
|
125
|
+
float quality = options.hasKey("quality") ? (float) options.getDouble("quality") : 0.8f;
|
|
126
|
+
String outputFormat = options.hasKey("outputFormat") ? options.getString("outputFormat") : "jpeg";
|
|
127
|
+
|
|
128
|
+
// Load bitmap
|
|
129
|
+
Bitmap bitmap = BitmapFactory.decodeFile(filePath);
|
|
130
|
+
if (bitmap == null) {
|
|
131
|
+
promise.reject("DECODE_ERROR", "Failed to decode image file");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
int originalWidth = bitmap.getWidth();
|
|
136
|
+
int originalHeight = bitmap.getHeight();
|
|
137
|
+
|
|
138
|
+
// Calculate new dimensions
|
|
139
|
+
int newWidth = originalWidth;
|
|
140
|
+
int newHeight = originalHeight;
|
|
141
|
+
|
|
142
|
+
if (maxWidth > 0 || maxHeight > 0) {
|
|
143
|
+
float widthRatio = maxWidth > 0 ? (float) maxWidth / originalWidth : Float.MAX_VALUE;
|
|
144
|
+
float heightRatio = maxHeight > 0 ? (float) maxHeight / originalHeight : Float.MAX_VALUE;
|
|
145
|
+
float ratio = Math.min(widthRatio, heightRatio);
|
|
146
|
+
|
|
147
|
+
if (ratio < 1.0f) {
|
|
148
|
+
newWidth = Math.round(originalWidth * ratio);
|
|
149
|
+
newHeight = Math.round(originalHeight * ratio);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Resize if needed
|
|
154
|
+
Bitmap resizedBitmap = bitmap;
|
|
155
|
+
if (newWidth != originalWidth || newHeight != originalHeight) {
|
|
156
|
+
resizedBitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
|
|
157
|
+
if (resizedBitmap != bitmap) {
|
|
158
|
+
bitmap.recycle();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Create output file
|
|
163
|
+
String outputPath = filePath;
|
|
164
|
+
if (filePath.contains(".")) {
|
|
165
|
+
String extension = outputFormat.equals("png") ? ".png" : ".jpg";
|
|
166
|
+
outputPath = filePath.substring(0, filePath.lastIndexOf(".")) + "_compressed" + extension;
|
|
167
|
+
} else {
|
|
168
|
+
outputPath = filePath + "_compressed." + (outputFormat.equals("png") ? "png" : "jpg");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
File outputFile = new File(outputPath);
|
|
172
|
+
|
|
173
|
+
// Compress and save
|
|
174
|
+
FileOutputStream fos = new FileOutputStream(outputFile);
|
|
175
|
+
Bitmap.CompressFormat format = outputFormat.equals("png")
|
|
176
|
+
? Bitmap.CompressFormat.PNG
|
|
177
|
+
: Bitmap.CompressFormat.JPEG;
|
|
178
|
+
|
|
179
|
+
resizedBitmap.compress(format, Math.round(quality * 100), fos);
|
|
180
|
+
fos.flush();
|
|
181
|
+
fos.close();
|
|
182
|
+
|
|
183
|
+
if (resizedBitmap != bitmap) {
|
|
184
|
+
resizedBitmap.recycle();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
long compressedSize = outputFile.length();
|
|
188
|
+
double compressionRatio = originalSize > 0
|
|
189
|
+
? (double) compressedSize / originalSize
|
|
190
|
+
: 1.0;
|
|
191
|
+
|
|
192
|
+
WritableMap result = Arguments.createMap();
|
|
193
|
+
result.putString("path", outputFile.getAbsolutePath());
|
|
194
|
+
result.putDouble("size", compressedSize);
|
|
195
|
+
result.putDouble("originalSize", originalSize);
|
|
196
|
+
result.putDouble("compressionRatio", compressionRatio);
|
|
197
|
+
result.putInt("width", newWidth);
|
|
198
|
+
result.putInt("height", newHeight);
|
|
199
|
+
|
|
200
|
+
promise.resolve(result);
|
|
201
|
+
} catch (Exception e) {
|
|
202
|
+
promise.reject("COMPRESSION_ERROR", "Failed to compress image: " + e.getMessage(), e);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package com.rnmediacompressor;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.ReactPackage;
|
|
6
|
+
import com.facebook.react.bridge.NativeModule;
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
8
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
9
|
+
|
|
10
|
+
import java.util.ArrayList;
|
|
11
|
+
import java.util.Collections;
|
|
12
|
+
import java.util.List;
|
|
13
|
+
|
|
14
|
+
public class RnMediaCompressorPackage implements ReactPackage {
|
|
15
|
+
@NonNull
|
|
16
|
+
@Override
|
|
17
|
+
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
|
18
|
+
List<NativeModule> modules = new ArrayList<>();
|
|
19
|
+
modules.add(new RnMediaCompressorModule(reactContext));
|
|
20
|
+
return modules;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@NonNull
|
|
24
|
+
@Override
|
|
25
|
+
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
|
26
|
+
return Collections.emptyList();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#import "RnMediaCompressor.h"
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
#import <AVFoundation/AVFoundation.h>
|
|
4
|
+
#import <ImageIO/ImageIO.h>
|
|
5
|
+
#import <MobileCoreServices/MobileCoreServices.h>
|
|
6
|
+
|
|
7
|
+
@implementation RnMediaCompressor
|
|
8
|
+
|
|
9
|
+
RCT_EXPORT_MODULE(RnMediaCompressor)
|
|
10
|
+
|
|
11
|
+
RCT_EXPORT_METHOD(compressImage:(NSString *)filePath
|
|
12
|
+
options:(NSDictionary *)options
|
|
13
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
14
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
15
|
+
{
|
|
16
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
17
|
+
@try {
|
|
18
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
19
|
+
if (![fileManager fileExistsAtPath:filePath]) {
|
|
20
|
+
reject(@"FILE_NOT_FOUND", [NSString stringWithFormat:@"File does not exist: %@", filePath], nil);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get original file size
|
|
25
|
+
NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:filePath error:nil];
|
|
26
|
+
NSNumber *originalSize = [fileAttributes objectForKey:NSFileSize];
|
|
27
|
+
|
|
28
|
+
// Read options
|
|
29
|
+
NSNumber *maxWidth = options[@"maxWidth"] ? options[@"maxWidth"] : nil;
|
|
30
|
+
NSNumber *maxHeight = options[@"maxHeight"] ? options[@"maxHeight"] : nil;
|
|
31
|
+
NSNumber *quality = options[@"quality"] ? options[@"quality"] : @0.8;
|
|
32
|
+
NSString *outputFormat = options[@"outputFormat"] ? options[@"outputFormat"] : @"jpeg";
|
|
33
|
+
|
|
34
|
+
// Load image
|
|
35
|
+
UIImage *image = [UIImage imageWithContentsOfFile:filePath];
|
|
36
|
+
if (!image) {
|
|
37
|
+
reject(@"DECODE_ERROR", @"Failed to decode image file", nil);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
CGFloat originalWidth = image.size.width;
|
|
42
|
+
CGFloat originalHeight = image.size.height;
|
|
43
|
+
|
|
44
|
+
// Calculate new dimensions
|
|
45
|
+
CGFloat newWidth = originalWidth;
|
|
46
|
+
CGFloat newHeight = originalHeight;
|
|
47
|
+
|
|
48
|
+
if (maxWidth || maxHeight) {
|
|
49
|
+
CGFloat widthRatio = maxWidth ? [maxWidth floatValue] / originalWidth : CGFLOAT_MAX;
|
|
50
|
+
CGFloat heightRatio = maxHeight ? [maxHeight floatValue] / originalHeight : CGFLOAT_MAX;
|
|
51
|
+
CGFloat ratio = MIN(widthRatio, heightRatio);
|
|
52
|
+
|
|
53
|
+
if (ratio < 1.0) {
|
|
54
|
+
newWidth = originalWidth * ratio;
|
|
55
|
+
newHeight = originalHeight * ratio;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Resize image
|
|
60
|
+
UIImage *resizedImage = image;
|
|
61
|
+
if (newWidth != originalWidth || newHeight != originalHeight) {
|
|
62
|
+
CGSize newSize = CGSizeMake(newWidth, newHeight);
|
|
63
|
+
UIGraphicsBeginImageContextWithOptions(newSize, NO, 1.0);
|
|
64
|
+
[image drawInRect:CGRectMake(0, 0, newWidth, newHeight)];
|
|
65
|
+
resizedImage = UIGraphicsGetImageFromCurrentImageContext();
|
|
66
|
+
UIGraphicsEndImageContext();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create output path
|
|
70
|
+
NSString *outputPath = filePath;
|
|
71
|
+
if ([filePath containsString:@"."]) {
|
|
72
|
+
NSString *extension = [outputFormat isEqualToString:@"png"] ? @".png" : @".jpg";
|
|
73
|
+
NSRange range = [filePath rangeOfString:@"." options:NSBackwardsSearch];
|
|
74
|
+
outputPath = [[filePath substringToIndex:range.location] stringByAppendingString:@"_compressed"];
|
|
75
|
+
outputPath = [outputPath stringByAppendingString:extension];
|
|
76
|
+
} else {
|
|
77
|
+
NSString *extension = [outputFormat isEqualToString:@"png"] ? @".png" : @".jpg";
|
|
78
|
+
outputPath = [filePath stringByAppendingString:@"_compressed"];
|
|
79
|
+
outputPath = [outputPath stringByAppendingString:extension];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Compress and save
|
|
83
|
+
NSData *imageData;
|
|
84
|
+
if ([outputFormat isEqualToString:@"png"]) {
|
|
85
|
+
imageData = UIImagePNGRepresentation(resizedImage);
|
|
86
|
+
} else {
|
|
87
|
+
imageData = UIImageJPEGRepresentation(resizedImage, [quality floatValue]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!imageData) {
|
|
91
|
+
reject(@"COMPRESSION_ERROR", @"Failed to compress image", nil);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
BOOL success = [imageData writeToFile:outputPath atomically:YES];
|
|
96
|
+
if (!success) {
|
|
97
|
+
reject(@"WRITE_ERROR", @"Failed to write compressed image to file", nil);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get compressed file size
|
|
102
|
+
NSDictionary *compressedAttributes = [fileManager attributesOfItemAtPath:outputPath error:nil];
|
|
103
|
+
NSNumber *compressedSize = [compressedAttributes objectForKey:NSFileSize];
|
|
104
|
+
|
|
105
|
+
double compressionRatio = [originalSize doubleValue] > 0
|
|
106
|
+
? [compressedSize doubleValue] / [originalSize doubleValue]
|
|
107
|
+
: 1.0;
|
|
108
|
+
|
|
109
|
+
NSDictionary *result = @{
|
|
110
|
+
@"path": outputPath,
|
|
111
|
+
@"size": compressedSize,
|
|
112
|
+
@"originalSize": originalSize,
|
|
113
|
+
@"compressionRatio": @(compressionRatio),
|
|
114
|
+
@"width": @(newWidth),
|
|
115
|
+
@"height": @(newHeight)
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
resolve(result);
|
|
119
|
+
} @catch (NSException *exception) {
|
|
120
|
+
reject(@"COMPRESSION_ERROR", exception.reason, nil);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
RCT_EXPORT_METHOD(compressVideo:(NSString *)filePath
|
|
126
|
+
options:(NSDictionary *)options
|
|
127
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
128
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
129
|
+
{
|
|
130
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
131
|
+
@try {
|
|
132
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
133
|
+
if (![fileManager fileExistsAtPath:filePath]) {
|
|
134
|
+
reject(@"FILE_NOT_FOUND", [NSString stringWithFormat:@"File does not exist: %@", filePath], nil);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Get original file size
|
|
139
|
+
NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:filePath error:nil];
|
|
140
|
+
NSNumber *originalSize = [fileAttributes objectForKey:NSFileSize];
|
|
141
|
+
|
|
142
|
+
AVAsset *asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:filePath]];
|
|
143
|
+
if (!asset) {
|
|
144
|
+
reject(@"DECODE_ERROR", @"Failed to load video file", nil);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Read options
|
|
149
|
+
NSNumber *maxWidth = options[@"maxWidth"];
|
|
150
|
+
NSNumber *maxHeight = options[@"maxHeight"];
|
|
151
|
+
NSNumber *bitrate = options[@"bitrate"];
|
|
152
|
+
NSNumber *quality = options[@"quality"]; // 0.0 - 1.0
|
|
153
|
+
|
|
154
|
+
AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
|
|
155
|
+
if (!videoTrack) {
|
|
156
|
+
reject(@"DECODE_ERROR", @"No video track found", nil);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
CGSize originalSize_video = videoTrack.naturalSize;
|
|
161
|
+
CGFloat originalWidth = originalSize_video.width;
|
|
162
|
+
CGFloat originalHeight = originalSize_video.height;
|
|
163
|
+
|
|
164
|
+
// Calculate new dimensions
|
|
165
|
+
CGFloat newWidth = originalWidth;
|
|
166
|
+
CGFloat newHeight = originalHeight;
|
|
167
|
+
|
|
168
|
+
if (maxWidth || maxHeight) {
|
|
169
|
+
CGFloat widthRatio = maxWidth ? [maxWidth floatValue] / originalWidth : CGFLOAT_MAX;
|
|
170
|
+
CGFloat heightRatio = maxHeight ? [maxHeight floatValue] / originalHeight : CGFLOAT_MAX;
|
|
171
|
+
CGFloat ratio = MIN(widthRatio, heightRatio);
|
|
172
|
+
|
|
173
|
+
if (ratio < 1.0) {
|
|
174
|
+
newWidth = originalWidth * ratio;
|
|
175
|
+
newHeight = originalHeight * ratio;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Create output path
|
|
180
|
+
NSString *outputPath = filePath;
|
|
181
|
+
if ([filePath containsString:@"."]) {
|
|
182
|
+
NSRange range = [filePath rangeOfString:@"." options:NSBackwardsSearch];
|
|
183
|
+
outputPath = [[filePath substringToIndex:range.location] stringByAppendingString:@"_compressed"];
|
|
184
|
+
outputPath = [outputPath stringByAppendingString:@".mp4"];
|
|
185
|
+
} else {
|
|
186
|
+
outputPath = [filePath stringByAppendingString:@"_compressed.mp4"];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Remove existing file if it exists
|
|
190
|
+
if ([fileManager fileExistsAtPath:outputPath]) {
|
|
191
|
+
[fileManager removeItemAtPath:outputPath error:nil];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Choose export preset based on quality / bitrate
|
|
195
|
+
NSString *presetName = AVAssetExportPresetMediumQuality;
|
|
196
|
+
|
|
197
|
+
if (bitrate != nil) {
|
|
198
|
+
// Rough mapping of target bitrate to presets
|
|
199
|
+
double targetBitrate = [bitrate doubleValue];
|
|
200
|
+
// These thresholds are approximate and device dependent
|
|
201
|
+
if (targetBitrate <= 1000000.0) { // <= 1 Mbps
|
|
202
|
+
presetName = AVAssetExportPresetLowQuality;
|
|
203
|
+
} else if (targetBitrate >= 5000000.0) { // >= 5 Mbps
|
|
204
|
+
presetName = AVAssetExportPresetHighestQuality;
|
|
205
|
+
} else {
|
|
206
|
+
presetName = AVAssetExportPresetMediumQuality;
|
|
207
|
+
}
|
|
208
|
+
} else if (quality != nil) {
|
|
209
|
+
CGFloat q = [quality floatValue];
|
|
210
|
+
if (q <= 0.4f) {
|
|
211
|
+
presetName = AVAssetExportPresetLowQuality;
|
|
212
|
+
} else if (q >= 0.85f) {
|
|
213
|
+
presetName = AVAssetExportPresetHighestQuality;
|
|
214
|
+
} else {
|
|
215
|
+
presetName = AVAssetExportPresetMediumQuality;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Create export session
|
|
220
|
+
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:asset presetName:presetName];
|
|
221
|
+
exportSession.outputURL = [NSURL fileURLWithPath:outputPath];
|
|
222
|
+
exportSession.outputFileType = AVFileTypeMPEG4;
|
|
223
|
+
exportSession.shouldOptimizeForNetworkUse = YES;
|
|
224
|
+
|
|
225
|
+
// Set video composition for resizing
|
|
226
|
+
if (newWidth != originalWidth || newHeight != originalHeight) {
|
|
227
|
+
AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
|
|
228
|
+
videoComposition.renderSize = CGSizeMake(newWidth, newHeight);
|
|
229
|
+
videoComposition.frameDuration = CMTimeMake(1, 30);
|
|
230
|
+
|
|
231
|
+
AVMutableVideoCompositionInstruction *instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
|
|
232
|
+
instruction.timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration);
|
|
233
|
+
|
|
234
|
+
AVMutableVideoCompositionLayerInstruction *layerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];
|
|
235
|
+
CGAffineTransform transform = CGAffineTransformMakeScale(newWidth / originalWidth, newHeight / originalHeight);
|
|
236
|
+
[layerInstruction setTransform:transform atTime:kCMTimeZero];
|
|
237
|
+
|
|
238
|
+
instruction.layerInstructions = @[layerInstruction];
|
|
239
|
+
videoComposition.instructions = @[instruction];
|
|
240
|
+
|
|
241
|
+
exportSession.videoComposition = videoComposition;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Export video
|
|
245
|
+
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
246
|
+
__block NSError *exportError = nil;
|
|
247
|
+
|
|
248
|
+
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
|
249
|
+
exportError = exportSession.error;
|
|
250
|
+
dispatch_semaphore_signal(semaphore);
|
|
251
|
+
}];
|
|
252
|
+
|
|
253
|
+
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
|
254
|
+
|
|
255
|
+
if (exportError) {
|
|
256
|
+
reject(@"COMPRESSION_ERROR", [NSString stringWithFormat:@"Video compression failed: %@", exportError.localizedDescription], exportError);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Get compressed file size
|
|
261
|
+
NSDictionary *compressedAttributes = [fileManager attributesOfItemAtPath:outputPath error:nil];
|
|
262
|
+
NSNumber *compressedSize = [compressedAttributes objectForKey:NSFileSize];
|
|
263
|
+
|
|
264
|
+
double compressionRatio = [originalSize doubleValue] > 0
|
|
265
|
+
? [compressedSize doubleValue] / [originalSize doubleValue]
|
|
266
|
+
: 1.0;
|
|
267
|
+
|
|
268
|
+
NSDictionary *result = @{
|
|
269
|
+
@"path": outputPath,
|
|
270
|
+
@"size": compressedSize,
|
|
271
|
+
@"originalSize": originalSize,
|
|
272
|
+
@"compressionRatio": @(compressionRatio),
|
|
273
|
+
@"width": @(newWidth),
|
|
274
|
+
@"height": @(newHeight)
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
resolve(result);
|
|
278
|
+
} @catch (NSException *exception) {
|
|
279
|
+
reject(@"COMPRESSION_ERROR", exception.reason, nil);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
RCT_EXPORT_METHOD(getMediaInfo:(NSString *)filePath
|
|
285
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
286
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
287
|
+
{
|
|
288
|
+
@try {
|
|
289
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
290
|
+
if (![fileManager fileExistsAtPath:filePath]) {
|
|
291
|
+
reject(@"FILE_NOT_FOUND", [NSString stringWithFormat:@"File does not exist: %@", filePath], nil);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:filePath error:nil];
|
|
296
|
+
NSNumber *fileSize = [fileAttributes objectForKey:NSFileSize];
|
|
297
|
+
|
|
298
|
+
// Try to get image info
|
|
299
|
+
UIImage *image = [UIImage imageWithContentsOfFile:filePath];
|
|
300
|
+
if (image) {
|
|
301
|
+
NSDictionary *result = @{
|
|
302
|
+
@"width": @(image.size.width),
|
|
303
|
+
@"height": @(image.size.height),
|
|
304
|
+
@"size": fileSize
|
|
305
|
+
};
|
|
306
|
+
resolve(result);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Try to get video info
|
|
311
|
+
AVAsset *asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:filePath]];
|
|
312
|
+
if (asset) {
|
|
313
|
+
AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
|
|
314
|
+
if (videoTrack) {
|
|
315
|
+
CGSize size = videoTrack.naturalSize;
|
|
316
|
+
CMTime duration = asset.duration;
|
|
317
|
+
double durationSeconds = CMTimeGetSeconds(duration);
|
|
318
|
+
|
|
319
|
+
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:@{
|
|
320
|
+
@"width": @(size.width),
|
|
321
|
+
@"height": @(size.height),
|
|
322
|
+
@"size": fileSize,
|
|
323
|
+
@"duration": @(durationSeconds)
|
|
324
|
+
}];
|
|
325
|
+
|
|
326
|
+
// Try to get MIME type
|
|
327
|
+
NSArray *formats = [asset availableMediaCharacteristicsWithMediaSelectionOptions];
|
|
328
|
+
if (formats.count > 0) {
|
|
329
|
+
result[@"mimeType"] = @"video/mp4";
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
resolve(result);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
reject(@"UNSUPPORTED_FORMAT", @"Unable to determine media type", nil);
|
|
338
|
+
} @catch (NSException *exception) {
|
|
339
|
+
reject(@"ERROR", exception.reason, nil);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { NativeModules, Platform } from 'react-native';
|
|
4
|
+
|
|
5
|
+
// Lazy access to native module - check when function is called, not at module load
|
|
6
|
+
const getNativeModule = () => {
|
|
7
|
+
try {
|
|
8
|
+
// Try direct access
|
|
9
|
+
if (NativeModules.RnMediaCompressor) {
|
|
10
|
+
return NativeModules.RnMediaCompressor;
|
|
11
|
+
}
|
|
12
|
+
// Try accessing via the module name
|
|
13
|
+
const module = NativeModules['RnMediaCompressor'];
|
|
14
|
+
if (module) {
|
|
15
|
+
return module;
|
|
16
|
+
}
|
|
17
|
+
// Log available modules for debugging
|
|
18
|
+
if (__DEV__) {
|
|
19
|
+
console.log('Available native modules:', Object.keys(NativeModules));
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (__DEV__) {
|
|
24
|
+
console.error('Error accessing native module:', error);
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Compress an image file
|
|
31
|
+
* @param filePath - Path to the image file
|
|
32
|
+
* @param options - Compression options
|
|
33
|
+
* @returns Promise with compression result
|
|
34
|
+
*/
|
|
35
|
+
export async function compressImage(filePath, options = {}) {
|
|
36
|
+
const RnMediaCompressor = getNativeModule();
|
|
37
|
+
if (!RnMediaCompressor) {
|
|
38
|
+
const errorMessage = 'RnMediaCompressor native module is not available. ' + 'Make sure you have:\n' + '1. Run "yarn prepare" or "npm run prepare" to build the library\n' + '2. For iOS: Run "cd ios && pod install"\n' + '3. Rebuild your app (not just reload)\n' + '4. If using Expo: Use a development build (not Expo Go)';
|
|
39
|
+
throw new Error(errorMessage);
|
|
40
|
+
}
|
|
41
|
+
if (!filePath) {
|
|
42
|
+
throw new Error('File path is required');
|
|
43
|
+
}
|
|
44
|
+
return RnMediaCompressor.compressImage(filePath, options);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compress a video file
|
|
49
|
+
* @param filePath - Path to the video file
|
|
50
|
+
* @param options - Compression options
|
|
51
|
+
* @returns Promise with compression result
|
|
52
|
+
*/
|
|
53
|
+
export async function compressVideo(filePath, options = {}) {
|
|
54
|
+
const RnMediaCompressor = getNativeModule();
|
|
55
|
+
if (!RnMediaCompressor) {
|
|
56
|
+
const errorMessage = 'RnMediaCompressor native module is not available. ' + 'Make sure you have:\n' + '1. Run "yarn prepare" or "npm run prepare" to build the library\n' + '2. For iOS: Run "cd ios && pod install"\n' + '3. Rebuild your app (not just reload)\n' + '4. If using Expo: Use a development build (not Expo Go)';
|
|
57
|
+
throw new Error(errorMessage);
|
|
58
|
+
}
|
|
59
|
+
if (!filePath) {
|
|
60
|
+
throw new Error('File path is required');
|
|
61
|
+
}
|
|
62
|
+
if (Platform.OS === 'android') {
|
|
63
|
+
throw new Error('Video compression is not yet implemented for Android');
|
|
64
|
+
}
|
|
65
|
+
return RnMediaCompressor.compressVideo(filePath, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get media file information
|
|
70
|
+
* @param filePath - Path to the media file
|
|
71
|
+
* @returns Promise with media information
|
|
72
|
+
*/
|
|
73
|
+
export async function getMediaInfo(filePath) {
|
|
74
|
+
const RnMediaCompressor = getNativeModule();
|
|
75
|
+
if (!RnMediaCompressor) {
|
|
76
|
+
const errorMessage = 'RnMediaCompressor native module is not available. ' + 'Make sure you have:\n' + '1. Run "yarn prepare" or "npm run prepare" to build the library\n' + '2. For iOS: Run "cd ios && pod install"\n' + '3. Rebuild your app (not just reload)\n' + '4. If using Expo: Use a development build (not Expo Go)';
|
|
77
|
+
throw new Error(errorMessage);
|
|
78
|
+
}
|
|
79
|
+
if (!filePath) {
|
|
80
|
+
throw new Error('File path is required');
|
|
81
|
+
}
|
|
82
|
+
return RnMediaCompressor.getMediaInfo(filePath);
|
|
83
|
+
}
|
|
84
|
+
export default {
|
|
85
|
+
compressImage,
|
|
86
|
+
compressVideo,
|
|
87
|
+
getMediaInfo
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["NativeModules","Platform","getNativeModule","RnMediaCompressor","module","__DEV__","console","log","Object","keys","error","compressImage","filePath","options","errorMessage","Error","compressVideo","OS","getMediaInfo"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,aAAa,EAAEC,QAAQ,QAAQ,cAAc;;AAEtD;AACA,MAAMC,eAAe,GAAGA,CAAA,KAAM;EAC5B,IAAI;IACF;IACA,IAAIF,aAAa,CAACG,iBAAiB,EAAE;MACnC,OAAOH,aAAa,CAACG,iBAAiB;IACxC;IACA;IACA,MAAMC,MAAM,GAAGJ,aAAa,CAAC,mBAAmB,CAAC;IACjD,IAAII,MAAM,EAAE;MACV,OAAOA,MAAM;IACf;IACA;IACA,IAAIC,OAAO,EAAE;MACXC,OAAO,CAACC,GAAG,CAAC,2BAA2B,EAAEC,MAAM,CAACC,IAAI,CAACT,aAAa,CAAC,CAAC;IACtE;IACA,OAAO,IAAI;EACb,CAAC,CAAC,OAAOU,KAAK,EAAE;IACd,IAAIL,OAAO,EAAE;MACXC,OAAO,CAACI,KAAK,CAAC,gCAAgC,EAAEA,KAAK,CAAC;IACxD;IACA,OAAO,IAAI;EACb;AACF,CAAC;AAiCD;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeC,aAAaA,CACjCC,QAAgB,EAChBC,OAA6B,GAAG,CAAC,CAAC,EACN;EAC5B,MAAMV,iBAAiB,GAAGD,eAAe,CAAC,CAAC;EAE3C,IAAI,CAACC,iBAAiB,EAAE;IACtB,MAAMW,YAAY,GAChB,oDAAoD,GACpD,uBAAuB,GACvB,mEAAmE,GACnE,2CAA2C,GAC3C,yCAAyC,GACzC,yDAAyD;IAC3D,MAAM,IAAIC,KAAK,CAACD,YAAY,CAAC;EAC/B;EAEA,IAAI,CAACF,QAAQ,EAAE;IACb,MAAM,IAAIG,KAAK,CAAC,uBAAuB,CAAC;EAC1C;EAEA,OAAOZ,iBAAiB,CAACQ,aAAa,CAACC,QAAQ,EAAEC,OAAO,CAAC;AAC3D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeG,aAAaA,CACjCJ,QAAgB,EAChBC,OAA6B,GAAG,CAAC,CAAC,EACN;EAC5B,MAAMV,iBAAiB,GAAGD,eAAe,CAAC,CAAC;EAE3C,IAAI,CAACC,iBAAiB,EAAE;IACtB,MAAMW,YAAY,GAChB,oDAAoD,GACpD,uBAAuB,GACvB,mEAAmE,GACnE,2CAA2C,GAC3C,yCAAyC,GACzC,yDAAyD;IAC3D,MAAM,IAAIC,KAAK,CAACD,YAAY,CAAC;EAC/B;EAEA,IAAI,CAACF,QAAQ,EAAE;IACb,MAAM,IAAIG,KAAK,CAAC,uBAAuB,CAAC;EAC1C;EAEA,IAAId,QAAQ,CAACgB,EAAE,KAAK,SAAS,EAAE;IAC7B,MAAM,IAAIF,KAAK,CAAC,sDAAsD,CAAC;EACzE;EAEA,OAAOZ,iBAAiB,CAACa,aAAa,CAACJ,QAAQ,EAAEC,OAAO,CAAC;AAC3D;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeK,YAAYA,CAACN,QAAgB,EAAsB;EACvE,MAAMT,iBAAiB,GAAGD,eAAe,CAAC,CAAC;EAE3C,IAAI,CAACC,iBAAiB,EAAE;IACtB,MAAMW,YAAY,GAChB,oDAAoD,GACpD,uBAAuB,GACvB,mEAAmE,GACnE,2CAA2C,GAC3C,yCAAyC,GACzC,yDAAyD;IAC3D,MAAM,IAAIC,KAAK,CAACD,YAAY,CAAC;EAC/B;EAEA,IAAI,CAACF,QAAQ,EAAE;IACb,MAAM,IAAIG,KAAK,CAAC,uBAAuB,CAAC;EAC1C;EAEA,OAAOZ,iBAAiB,CAACe,YAAY,CAACN,QAAQ,CAAC;AACjD;AAEA,eAAe;EACbD,aAAa;EACbK,aAAa;EACbE;AACF,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface CompressImageOptions {
|
|
2
|
+
maxWidth?: number;
|
|
3
|
+
maxHeight?: number;
|
|
4
|
+
quality?: number;
|
|
5
|
+
outputFormat?: 'jpeg' | 'png';
|
|
6
|
+
}
|
|
7
|
+
export interface CompressVideoOptions {
|
|
8
|
+
maxWidth?: number;
|
|
9
|
+
maxHeight?: number;
|
|
10
|
+
quality?: number;
|
|
11
|
+
bitrate?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface CompressionResult {
|
|
14
|
+
path: string;
|
|
15
|
+
size: number;
|
|
16
|
+
originalSize: number;
|
|
17
|
+
compressionRatio: number;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
}
|
|
21
|
+
export interface MediaInfo {
|
|
22
|
+
size: number;
|
|
23
|
+
width?: number;
|
|
24
|
+
height?: number;
|
|
25
|
+
duration?: number;
|
|
26
|
+
mimeType?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Compress an image file
|
|
30
|
+
* @param filePath - Path to the image file
|
|
31
|
+
* @param options - Compression options
|
|
32
|
+
* @returns Promise with compression result
|
|
33
|
+
*/
|
|
34
|
+
export declare function compressImage(filePath: string, options?: CompressImageOptions): Promise<CompressionResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Compress a video file
|
|
37
|
+
* @param filePath - Path to the video file
|
|
38
|
+
* @param options - Compression options
|
|
39
|
+
* @returns Promise with compression result
|
|
40
|
+
*/
|
|
41
|
+
export declare function compressVideo(filePath: string, options?: CompressVideoOptions): Promise<CompressionResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Get media file information
|
|
44
|
+
* @param filePath - Path to the media file
|
|
45
|
+
* @returns Promise with media information
|
|
46
|
+
*/
|
|
47
|
+
export declare function getMediaInfo(filePath: string): Promise<MediaInfo>;
|
|
48
|
+
declare const _default: {
|
|
49
|
+
compressImage: typeof compressImage;
|
|
50
|
+
compressVideo: typeof compressVideo;
|
|
51
|
+
getMediaInfo: typeof getMediaInfo;
|
|
52
|
+
};
|
|
53
|
+
export default _default;
|
|
54
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AA2BA,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,iBAAiB,CAAC,CAmB5B;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,iBAAiB,CAAC,CAuB5B;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAmBvE;;;;;;AAED,wBAIE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-rn-media-compressor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "This package will reduce the size of media file before uplaoding it to server",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"android",
|
|
19
|
+
"ios",
|
|
20
|
+
"cpp",
|
|
21
|
+
"*.podspec",
|
|
22
|
+
"react-native.config.js",
|
|
23
|
+
"!ios/build",
|
|
24
|
+
"!android/build",
|
|
25
|
+
"!android/gradle",
|
|
26
|
+
"!android/gradlew",
|
|
27
|
+
"!android/gradlew.bat",
|
|
28
|
+
"!android/local.properties",
|
|
29
|
+
"!**/__tests__",
|
|
30
|
+
"!**/__fixtures__",
|
|
31
|
+
"!**/__mocks__",
|
|
32
|
+
"!**/.*"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"example": "yarn workspace react-native-rn-media-compressor-example",
|
|
36
|
+
"clean": "del-cli lib",
|
|
37
|
+
"prepare": "bob build",
|
|
38
|
+
"typecheck": "tsc",
|
|
39
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
40
|
+
"test": "jest",
|
|
41
|
+
"release": "release-it --only-version"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"react-native",
|
|
45
|
+
"ios",
|
|
46
|
+
"android"
|
|
47
|
+
],
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/Siddesh7972/react-native-rn-media-compressor.git"
|
|
51
|
+
},
|
|
52
|
+
"author": "Siddesh7972 <siddesh.gawande@mindbowser.com> (https://github.com/Siddesh7972)",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/Siddesh7972/react-native-rn-media-compressor/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/Siddesh7972/react-native-rn-media-compressor#readme",
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"registry": "https://registry.npmjs.org/"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
63
|
+
"@eslint/compat": "^1.3.2",
|
|
64
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
65
|
+
"@eslint/js": "^9.35.0",
|
|
66
|
+
"@react-native/babel-preset": "0.83.0",
|
|
67
|
+
"@react-native/eslint-config": "0.83.0",
|
|
68
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
69
|
+
"@types/jest": "^29.5.14",
|
|
70
|
+
"@types/react": "^19.1.12",
|
|
71
|
+
"commitlint": "^19.8.1",
|
|
72
|
+
"del-cli": "^6.0.0",
|
|
73
|
+
"eslint": "^9.35.0",
|
|
74
|
+
"eslint-config-prettier": "^10.1.8",
|
|
75
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
76
|
+
"jest": "^29.7.0",
|
|
77
|
+
"lefthook": "^2.0.3",
|
|
78
|
+
"prettier": "^2.8.8",
|
|
79
|
+
"react": "19.1.0",
|
|
80
|
+
"react-native": "0.81.5",
|
|
81
|
+
"react-native-builder-bob": "^0.40.17",
|
|
82
|
+
"release-it": "^19.0.4",
|
|
83
|
+
"typescript": "^5.9.2"
|
|
84
|
+
},
|
|
85
|
+
"peerDependencies": {
|
|
86
|
+
"react": "*",
|
|
87
|
+
"react-native": "*"
|
|
88
|
+
},
|
|
89
|
+
"workspaces": [
|
|
90
|
+
"example"
|
|
91
|
+
],
|
|
92
|
+
"packageManager": "yarn@4.11.0",
|
|
93
|
+
"react-native-builder-bob": {
|
|
94
|
+
"source": "src",
|
|
95
|
+
"output": "lib",
|
|
96
|
+
"targets": [
|
|
97
|
+
[
|
|
98
|
+
"module",
|
|
99
|
+
{
|
|
100
|
+
"esm": true
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
[
|
|
104
|
+
"typescript",
|
|
105
|
+
{
|
|
106
|
+
"project": "tsconfig.build.json"
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
"prettier": {
|
|
112
|
+
"quoteProps": "consistent",
|
|
113
|
+
"singleQuote": true,
|
|
114
|
+
"tabWidth": 2,
|
|
115
|
+
"trailingComma": "es5",
|
|
116
|
+
"useTabs": false
|
|
117
|
+
},
|
|
118
|
+
"jest": {
|
|
119
|
+
"preset": "react-native",
|
|
120
|
+
"modulePathIgnorePatterns": [
|
|
121
|
+
"<rootDir>/example/node_modules",
|
|
122
|
+
"<rootDir>/lib/"
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
"commitlint": {
|
|
126
|
+
"extends": [
|
|
127
|
+
"@commitlint/config-conventional"
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
"release-it": {
|
|
131
|
+
"git": {
|
|
132
|
+
"commitMessage": "chore: release ${version}",
|
|
133
|
+
"tagName": "v${version}"
|
|
134
|
+
},
|
|
135
|
+
"npm": {
|
|
136
|
+
"publish": true
|
|
137
|
+
},
|
|
138
|
+
"github": {
|
|
139
|
+
"release": true
|
|
140
|
+
},
|
|
141
|
+
"plugins": {
|
|
142
|
+
"@release-it/conventional-changelog": {
|
|
143
|
+
"preset": {
|
|
144
|
+
"name": "angular"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"create-react-native-library": {
|
|
150
|
+
"type": "library",
|
|
151
|
+
"languages": "js",
|
|
152
|
+
"tools": [
|
|
153
|
+
"eslint",
|
|
154
|
+
"jest",
|
|
155
|
+
"lefthook",
|
|
156
|
+
"release-it"
|
|
157
|
+
],
|
|
158
|
+
"version": "0.56.0"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "react-native-rn-media-compressor"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.description = <<-DESC
|
|
10
|
+
This package will reduce the size of media file before uploading it to server
|
|
11
|
+
DESC
|
|
12
|
+
s.homepage = package["homepage"]
|
|
13
|
+
s.license = package["license"]
|
|
14
|
+
s.authors = package["author"]
|
|
15
|
+
|
|
16
|
+
s.platforms = { :ios => "13.4" }
|
|
17
|
+
s.source = { :git => package["repository"]["url"], :tag => "#{s.version}" }
|
|
18
|
+
|
|
19
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
20
|
+
|
|
21
|
+
s.dependency "React-Core"
|
|
22
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
dependency: {
|
|
3
|
+
platforms: {
|
|
4
|
+
android: {
|
|
5
|
+
sourceDir: '../android/',
|
|
6
|
+
packageImportPath: 'import com.rnmediacompressor.RnMediaCompressorPackage;',
|
|
7
|
+
},
|
|
8
|
+
ios: {
|
|
9
|
+
podspecPath: '../react-native-rn-media-compressor.podspec',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { NativeModules, Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
// Lazy access to native module - check when function is called, not at module load
|
|
4
|
+
const getNativeModule = () => {
|
|
5
|
+
try {
|
|
6
|
+
// Try direct access
|
|
7
|
+
if (NativeModules.RnMediaCompressor) {
|
|
8
|
+
return NativeModules.RnMediaCompressor;
|
|
9
|
+
}
|
|
10
|
+
// Try accessing via the module name
|
|
11
|
+
const module = NativeModules['RnMediaCompressor'];
|
|
12
|
+
if (module) {
|
|
13
|
+
return module;
|
|
14
|
+
}
|
|
15
|
+
// Log available modules for debugging
|
|
16
|
+
if (__DEV__) {
|
|
17
|
+
console.log('Available native modules:', Object.keys(NativeModules));
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (__DEV__) {
|
|
22
|
+
console.error('Error accessing native module:', error);
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export interface CompressImageOptions {
|
|
29
|
+
maxWidth?: number;
|
|
30
|
+
maxHeight?: number;
|
|
31
|
+
quality?: number; // 0.0 - 1.0
|
|
32
|
+
outputFormat?: 'jpeg' | 'png';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CompressVideoOptions {
|
|
36
|
+
maxWidth?: number;
|
|
37
|
+
maxHeight?: number;
|
|
38
|
+
quality?: number; // 0.0 - 1.0
|
|
39
|
+
bitrate?: number; // bits per second
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CompressionResult {
|
|
43
|
+
path: string;
|
|
44
|
+
size: number;
|
|
45
|
+
originalSize: number;
|
|
46
|
+
compressionRatio: number;
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface MediaInfo {
|
|
52
|
+
size: number;
|
|
53
|
+
width?: number;
|
|
54
|
+
height?: number;
|
|
55
|
+
duration?: number; // in seconds
|
|
56
|
+
mimeType?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Compress an image file
|
|
61
|
+
* @param filePath - Path to the image file
|
|
62
|
+
* @param options - Compression options
|
|
63
|
+
* @returns Promise with compression result
|
|
64
|
+
*/
|
|
65
|
+
export async function compressImage(
|
|
66
|
+
filePath: string,
|
|
67
|
+
options: CompressImageOptions = {}
|
|
68
|
+
): Promise<CompressionResult> {
|
|
69
|
+
const RnMediaCompressor = getNativeModule();
|
|
70
|
+
|
|
71
|
+
if (!RnMediaCompressor) {
|
|
72
|
+
const errorMessage =
|
|
73
|
+
'RnMediaCompressor native module is not available. ' +
|
|
74
|
+
'Make sure you have:\n' +
|
|
75
|
+
'1. Run "yarn prepare" or "npm run prepare" to build the library\n' +
|
|
76
|
+
'2. For iOS: Run "cd ios && pod install"\n' +
|
|
77
|
+
'3. Rebuild your app (not just reload)\n' +
|
|
78
|
+
'4. If using Expo: Use a development build (not Expo Go)';
|
|
79
|
+
throw new Error(errorMessage);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!filePath) {
|
|
83
|
+
throw new Error('File path is required');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return RnMediaCompressor.compressImage(filePath, options);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compress a video file
|
|
91
|
+
* @param filePath - Path to the video file
|
|
92
|
+
* @param options - Compression options
|
|
93
|
+
* @returns Promise with compression result
|
|
94
|
+
*/
|
|
95
|
+
export async function compressVideo(
|
|
96
|
+
filePath: string,
|
|
97
|
+
options: CompressVideoOptions = {}
|
|
98
|
+
): Promise<CompressionResult> {
|
|
99
|
+
const RnMediaCompressor = getNativeModule();
|
|
100
|
+
|
|
101
|
+
if (!RnMediaCompressor) {
|
|
102
|
+
const errorMessage =
|
|
103
|
+
'RnMediaCompressor native module is not available. ' +
|
|
104
|
+
'Make sure you have:\n' +
|
|
105
|
+
'1. Run "yarn prepare" or "npm run prepare" to build the library\n' +
|
|
106
|
+
'2. For iOS: Run "cd ios && pod install"\n' +
|
|
107
|
+
'3. Rebuild your app (not just reload)\n' +
|
|
108
|
+
'4. If using Expo: Use a development build (not Expo Go)';
|
|
109
|
+
throw new Error(errorMessage);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!filePath) {
|
|
113
|
+
throw new Error('File path is required');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Platform.OS === 'android') {
|
|
117
|
+
throw new Error('Video compression is not yet implemented for Android');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return RnMediaCompressor.compressVideo(filePath, options);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get media file information
|
|
125
|
+
* @param filePath - Path to the media file
|
|
126
|
+
* @returns Promise with media information
|
|
127
|
+
*/
|
|
128
|
+
export async function getMediaInfo(filePath: string): Promise<MediaInfo> {
|
|
129
|
+
const RnMediaCompressor = getNativeModule();
|
|
130
|
+
|
|
131
|
+
if (!RnMediaCompressor) {
|
|
132
|
+
const errorMessage =
|
|
133
|
+
'RnMediaCompressor native module is not available. ' +
|
|
134
|
+
'Make sure you have:\n' +
|
|
135
|
+
'1. Run "yarn prepare" or "npm run prepare" to build the library\n' +
|
|
136
|
+
'2. For iOS: Run "cd ios && pod install"\n' +
|
|
137
|
+
'3. Rebuild your app (not just reload)\n' +
|
|
138
|
+
'4. If using Expo: Use a development build (not Expo Go)';
|
|
139
|
+
throw new Error(errorMessage);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!filePath) {
|
|
143
|
+
throw new Error('File path is required');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return RnMediaCompressor.getMediaInfo(filePath);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default {
|
|
150
|
+
compressImage,
|
|
151
|
+
compressVideo,
|
|
152
|
+
getMediaInfo,
|
|
153
|
+
};
|