rns-mediapicker 0.1.4 → 3.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/README.md +162 -47
- package/android/src/main/java/com/rnsmediapicker/FastMediaPickerModule.kt +118 -56
- package/index.d.ts +8 -4
- package/index.js +13 -5
- package/ios/FastMediaPicker.m +5 -6
- package/ios/FastMediaPicker.swift +80 -27
- package/package.json +1 -1
- package/withMediaPicker.js +22 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# 🎵 / 🖼️ / 🎥 rns-mediapicker
|
|
2
2
|
|
|
3
3
|
A **high-performance native media picker** for **React Native** and **Expo**.
|
|
4
|
-
Supports **audio, image,
|
|
4
|
+
Supports **audio, image, video, `.adp` project files** from **camera or library**, automatic JPEG compression, EXIF-safe dimensions, and iterative quality targeting.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -29,6 +29,7 @@ Add the plugin to your `app.json` or `app.config.js`.
|
|
|
29
29
|
This automatically configures:
|
|
30
30
|
|
|
31
31
|
- iOS permissions (`Info.plist`)
|
|
32
|
+
- iOS `.adp` custom UTType declaration
|
|
32
33
|
- Android intent queries
|
|
33
34
|
- Android `FileProvider`
|
|
34
35
|
|
|
@@ -44,87 +45,201 @@ This automatically configures:
|
|
|
44
45
|
|
|
45
46
|
## 🧑💻 Usage
|
|
46
47
|
|
|
48
|
+
### Pick from library (image or video)
|
|
49
|
+
|
|
47
50
|
```js
|
|
48
51
|
import MediaPicker from 'rns-mediapicker';
|
|
49
52
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
53
|
+
const result = await MediaPicker.pick(
|
|
54
|
+
false, // useCamera
|
|
55
|
+
'both', // mediaType
|
|
56
|
+
'back', // env
|
|
57
|
+
0 // targetKB — 0 means no compression
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
console.log(result.uri); // file:///...
|
|
61
|
+
console.log(result.type); // 'image' | 'video' | 'audio' | 'adp'
|
|
62
|
+
console.log(result.width); // number (0 for audio/adp)
|
|
63
|
+
console.log(result.height); // number (0 for audio/adp)
|
|
64
|
+
console.log(result.size); // KB (images only — field is absent for video/audio/adp)
|
|
65
|
+
console.log(result.isVideo);
|
|
66
|
+
console.log(result.isAudio);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### Open camera
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
// Photo with back camera
|
|
75
|
+
const result = await MediaPicker.pick(true, 'image', 'back', 0);
|
|
76
|
+
|
|
77
|
+
// Video with front camera
|
|
78
|
+
const result = await MediaPicker.pick(true, 'video', 'front', 0);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### Pick audio
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
const result = await MediaPicker.pick(false, 'audio', 'back', 0);
|
|
87
|
+
|
|
88
|
+
console.log(result.uri); // file:///...
|
|
89
|
+
console.log(result.type); // 'audio'
|
|
90
|
+
console.log(result.isAudio); // true
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### Pick with image compression (`targetKB`)
|
|
96
|
+
|
|
97
|
+
Compresses the image iteratively until it fits within the target size.
|
|
98
|
+
Quality steps down by 10% per iteration (from 100% → minimum 10%).
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
// Pick an image and compress to max 200KB
|
|
102
|
+
const result = await MediaPicker.pick(false, 'image', 'back', 200);
|
|
103
|
+
|
|
104
|
+
console.log(result.size); // Actual size in KB after compression
|
|
105
|
+
console.log(result.uri); // Compressed JPEG file URI
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> `targetKB = 0` disables compression and returns the full-quality image.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### Pick a `.adp` project file
|
|
113
|
+
|
|
114
|
+
`.adp` is a custom Audipella project file format. This opens the file browser
|
|
115
|
+
filtered to `.adp` files only (iOS) or a general file browser (Android).
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
const result = await MediaPicker.pick(false, 'adp', 'back', 0);
|
|
119
|
+
|
|
120
|
+
console.log(result.uri); // file:///...path/to/file.adp
|
|
121
|
+
console.log(result.type); // 'adp'
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### Error handling
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
try {
|
|
130
|
+
const result = await MediaPicker.pick(false, 'image', 'back', 0);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
switch (error.code) {
|
|
133
|
+
case 'E_CANCELLED':
|
|
134
|
+
console.log('User cancelled');
|
|
135
|
+
break;
|
|
136
|
+
case 'E_PERMISSION_DENIED':
|
|
137
|
+
console.log('Permission denied');
|
|
138
|
+
break;
|
|
139
|
+
case 'E_CAMERA_UNAVAILABLE':
|
|
140
|
+
console.log('Camera not available on this device');
|
|
141
|
+
break;
|
|
142
|
+
case 'E_PROCESS':
|
|
143
|
+
console.log('Failed to process media:', error.message);
|
|
144
|
+
break;
|
|
145
|
+
default:
|
|
68
146
|
console.error('Picker error:', error.message);
|
|
69
|
-
}
|
|
70
147
|
}
|
|
71
|
-
}
|
|
148
|
+
}
|
|
72
149
|
```
|
|
73
150
|
|
|
74
151
|
---
|
|
75
152
|
|
|
76
153
|
## 🧩 API Reference
|
|
77
154
|
|
|
78
|
-
### `pick(useCamera, mediaType, env)`
|
|
155
|
+
### `pick(useCamera, mediaType, env, targetKB)`
|
|
79
156
|
|
|
80
|
-
| Parameter
|
|
81
|
-
|
|
82
|
-
| useCamera | boolean | `true` opens the camera, `false` opens the media picker |
|
|
83
|
-
| mediaType | string | `'
|
|
84
|
-
| env | string | `'front'` or `'back'`
|
|
157
|
+
| Parameter | Type | Default | Description |
|
|
158
|
+
|-------------|----------|------------|-------------|
|
|
159
|
+
| `useCamera` | boolean | — | `true` opens the camera, `false` opens the file/media picker |
|
|
160
|
+
| `mediaType` | string | — | `'image'`, `'video'`, `'both'`, `'audio'`, or `'adp'` |
|
|
161
|
+
| `env` | string | `'back'` | `'front'` or `'back'` — camera only, best-effort on Android |
|
|
162
|
+
| `targetKB` | number | `0` | Max image size in KB. `0` = no compression. Images only |
|
|
163
|
+
|
|
164
|
+
All four parameters must be passed. Use `0` for `targetKB` to disable compression, and `'back'` for `env` when camera direction is not relevant.
|
|
85
165
|
|
|
86
166
|
---
|
|
87
167
|
|
|
88
168
|
## 📦 Response Object
|
|
89
169
|
|
|
90
|
-
```
|
|
170
|
+
```ts
|
|
91
171
|
{
|
|
92
|
-
uri: string; // Local file URI
|
|
93
|
-
width: number; //
|
|
94
|
-
height: number; //
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
172
|
+
uri: string; // Local file URI (file://...)
|
|
173
|
+
width: number; // Width in pixels (0 for audio and adp)
|
|
174
|
+
height: number; // Height in pixels (0 for audio and adp)
|
|
175
|
+
size?: number; // File size in KB — present for images only, absent for video/audio/adp
|
|
176
|
+
type: 'image' | 'video' | 'audio' | 'adp';
|
|
177
|
+
isVideo: boolean;
|
|
178
|
+
isAudio: boolean;
|
|
98
179
|
}
|
|
99
180
|
```
|
|
100
181
|
|
|
101
182
|
---
|
|
102
183
|
|
|
184
|
+
## 🗂️ Media Type Reference
|
|
185
|
+
|
|
186
|
+
| `mediaType` | Opens | Returns |
|
|
187
|
+
|-------------|-------|---------|
|
|
188
|
+
| `'image'` | Photo library / camera | `type: 'image'` |
|
|
189
|
+
| `'video'` | Video library / camera | `type: 'video'` |
|
|
190
|
+
| `'both'` | Photo + video library / camera | `type: 'image'` or `'video'` |
|
|
191
|
+
| `'audio'` | File browser (audio files) | `type: 'audio'` |
|
|
192
|
+
| `'adp'` | File browser (`.adp` files on iOS; all files on Android) | `type: 'adp'` |
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 🚨 Error Codes
|
|
197
|
+
|
|
198
|
+
| Code | Description |
|
|
199
|
+
|------|-------------|
|
|
200
|
+
| `E_CANCELLED` | User dismissed the picker or cancelled |
|
|
201
|
+
| `E_PERMISSION_DENIED` | Camera or storage permission denied |
|
|
202
|
+
| `E_NOT_PERMISSION_AWARE` | Android activity does not implement `PermissionAwareActivity` |
|
|
203
|
+
| `E_CAMERA_UNAVAILABLE` | Device has no camera (iOS) |
|
|
204
|
+
| `E_NO_ACTIVITY` | Android activity not found |
|
|
205
|
+
| `E_NO_URI` | No media URI returned from picker |
|
|
206
|
+
| `E_PROCESS` | Failed to decode, compress, or copy the file |
|
|
207
|
+
| `E_AUDIO_COPY` | Failed to copy audio/adp file to temp location |
|
|
208
|
+
| `E_VIDEO_COPY` | Failed to copy video file to temp location |
|
|
209
|
+
| `E_IMAGE_WRITE` | Failed to encode or write image to disk |
|
|
210
|
+
| `E_LOAD` | Failed to load media from PHPicker (iOS) |
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
103
214
|
## ✨ Features
|
|
104
215
|
|
|
105
|
-
- 🎵 Audio /
|
|
106
|
-
- 📷 Camera and 📁 Library selection
|
|
107
|
-
- 🎬 Camera video capture (Android & iOS)
|
|
108
|
-
- 🗜️
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
- 🍎 iOS
|
|
216
|
+
- 🖼️ Image / 🎥 Video / 🎵 Audio / 📁 `.adp` project file support
|
|
217
|
+
- 📷 Camera and 📁 Library selection
|
|
218
|
+
- 🎬 Camera video capture (Android & iOS)
|
|
219
|
+
- 🗜️ Iterative JPEG compression with `targetKB` target
|
|
220
|
+
- 🔄 EXIF-safe rotation and accurate dimensions
|
|
221
|
+
- 📂 Scoped-storage safe temporary files
|
|
222
|
+
- 🤖 Android 11+ intent queries handled automatically
|
|
223
|
+
- 🔐 Android `FileProvider` preconfigured
|
|
224
|
+
- 🍎 iOS swipe-to-dismiss cancellation handling
|
|
225
|
+
- 🍎 iOS `.adp` UTType auto-registered via Expo plugin
|
|
115
226
|
- 🧹 Clean promise lifecycle (no leaks)
|
|
116
227
|
|
|
117
228
|
---
|
|
118
229
|
|
|
119
230
|
## 📝 Notes
|
|
120
231
|
|
|
121
|
-
- All returned files are copied into the app
|
|
122
|
-
- Audio files preserve original encoding
|
|
232
|
+
- All returned files are copied into the app's **temporary cache directory**
|
|
233
|
+
- Audio files preserve their original encoding and extension
|
|
234
|
+
- `.adp` files are copied as-is — no processing applied
|
|
235
|
+
- `targetKB` compression only applies to **images**, not video or audio
|
|
236
|
+
- The `size` field is only present in the response for images; it is absent (not `0`) for video, audio, and adp results
|
|
123
237
|
- Front camera selection is **best-effort** on Android
|
|
238
|
+
- On Android, `.adp` picker opens a general file browser since Android has no built-in MIME type for `.adp`; the file is identified by its extension after selection
|
|
124
239
|
- No external native dependencies required
|
|
125
240
|
|
|
126
241
|
---
|
|
127
242
|
|
|
128
243
|
## 📄 License
|
|
129
244
|
|
|
130
|
-
MIT
|
|
245
|
+
MIT
|
|
@@ -8,6 +8,7 @@ import android.media.ExifInterface
|
|
|
8
8
|
import android.media.MediaMetadataRetriever
|
|
9
9
|
import android.net.Uri
|
|
10
10
|
import android.os.Build
|
|
11
|
+
import android.provider.OpenableColumns
|
|
11
12
|
import android.provider.MediaStore
|
|
12
13
|
import android.webkit.MimeTypeMap
|
|
13
14
|
import androidx.core.content.FileProvider
|
|
@@ -30,11 +31,12 @@ class FastMediaPickerModule(
|
|
|
30
31
|
private var pendingFrontCamera = false
|
|
31
32
|
private var pendingUseCamera = false
|
|
32
33
|
private var pendingMediaType: String? = null
|
|
34
|
+
private var pendingTargetKB: Int = 0 // ← new
|
|
35
|
+
|
|
33
36
|
|
|
34
37
|
companion object {
|
|
35
38
|
private const val PICKER_REQUEST_CODE = 4123
|
|
36
39
|
private const val PERMISSION_REQUEST = 9123
|
|
37
|
-
private const val JPEG_QUALITY = 85
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
init {
|
|
@@ -52,10 +54,11 @@ class FastMediaPickerModule(
|
|
|
52
54
|
useCamera: Boolean,
|
|
53
55
|
mediaType: String,
|
|
54
56
|
env: String?,
|
|
57
|
+
targetKB: Int, // ← matches iOS pick:mediaType:env:targetKB:resolver:rejecter:
|
|
55
58
|
promise: Promise,
|
|
56
59
|
) {
|
|
57
60
|
val activity =
|
|
58
|
-
currentActivity ?: run {
|
|
61
|
+
reactApplicationContext.currentActivity ?: run {
|
|
59
62
|
promise.reject("E_NO_ACTIVITY", "Activity not found")
|
|
60
63
|
return
|
|
61
64
|
}
|
|
@@ -65,6 +68,8 @@ class FastMediaPickerModule(
|
|
|
65
68
|
pendingUseCamera = useCamera
|
|
66
69
|
pendingMediaType = mediaType.lowercase()
|
|
67
70
|
pendingFrontCamera = env?.lowercase() == "front"
|
|
71
|
+
pendingTargetKB = targetKB // ← new
|
|
72
|
+
|
|
68
73
|
|
|
69
74
|
val permissions = mutableListOf<String>()
|
|
70
75
|
if (useCamera) {
|
|
@@ -104,6 +109,19 @@ class FastMediaPickerModule(
|
|
|
104
109
|
activity: Activity,
|
|
105
110
|
type: String,
|
|
106
111
|
) {
|
|
112
|
+
|
|
113
|
+
// 1a. ADP: Custom project file — use */* so the file manager shows all files,
|
|
114
|
+
// Android has no built-in MIME type for .adp so we can't filter more tightly here.
|
|
115
|
+
if (type == "adp") {
|
|
116
|
+
val intent =
|
|
117
|
+
Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
118
|
+
addCategory(Intent.CATEGORY_OPENABLE)
|
|
119
|
+
this.type = "*/*"
|
|
120
|
+
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
|
|
121
|
+
}
|
|
122
|
+
activity.startActivityForResult(intent, PICKER_REQUEST_CODE)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
107
125
|
// 1. AUDIO: Use GET_CONTENT but without the chooser for faster return
|
|
108
126
|
if (type == "audio") {
|
|
109
127
|
val intent =
|
|
@@ -186,7 +204,7 @@ class FastMediaPickerModule(
|
|
|
186
204
|
}
|
|
187
205
|
|
|
188
206
|
override fun onActivityResult(
|
|
189
|
-
activity: Activity
|
|
207
|
+
activity: Activity,
|
|
190
208
|
requestCode: Int,
|
|
191
209
|
resultCode: Int,
|
|
192
210
|
data: Intent?,
|
|
@@ -215,54 +233,87 @@ class FastMediaPickerModule(
|
|
|
215
233
|
resultCode: Int,
|
|
216
234
|
data: Intent?,
|
|
217
235
|
) {
|
|
218
|
-
|
|
236
|
+
reactApplicationContext.currentActivity?.let { activity ->
|
|
237
|
+
onActivityResult(activity, requestCode, resultCode, data)
|
|
238
|
+
}
|
|
219
239
|
}
|
|
220
240
|
|
|
221
241
|
private fun processMedia(uri: Uri) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
242
|
+
// Mirrors iOS: heavy processing (decode, compress, copy) happens off the main thread.
|
|
243
|
+
// Only the promise resolve/reject lands back via the RN bridge.
|
|
244
|
+
Thread {
|
|
245
|
+
val resolver = reactContext.contentResolver
|
|
246
|
+
val mime = resolver.getType(uri) ?: "image/jpeg"
|
|
247
|
+
val displayName = getDisplayName(uri)
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
|
|
251
|
+
val isAdpFile = uri.toString().endsWith(".adp", ignoreCase = true) ||
|
|
252
|
+
mime.equals("application/adp", ignoreCase = true) ||
|
|
253
|
+
displayName.endsWith(".adp", ignoreCase = true)
|
|
254
|
+
|
|
226
255
|
when {
|
|
227
|
-
|
|
228
|
-
val
|
|
229
|
-
val file = createTempFile(ext)
|
|
230
|
-
resolver.openInputStream(uri)?.use { it.copyTo(FileOutputStream(file)) }
|
|
231
|
-
resolveResult(Uri.fromFile(file).toString(), false, true, 0, 0)
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
mime.startsWith("video") -> {
|
|
235
|
-
val file = createTempFile("mp4")
|
|
256
|
+
isAdpFile -> {
|
|
257
|
+
val file = createTempFile("adp")
|
|
236
258
|
resolver.openInputStream(uri)?.use { it.copyTo(FileOutputStream(file)) }
|
|
237
|
-
|
|
238
|
-
val retriever = MediaMetadataRetriever()
|
|
239
|
-
retriever.setDataSource(reactContext, uri)
|
|
240
|
-
val w = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0
|
|
241
|
-
val h = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0
|
|
242
|
-
retriever.release()
|
|
243
|
-
|
|
244
|
-
resolveResult(Uri.fromFile(file).toString(), true, false, w, h)
|
|
259
|
+
resolveResult(Uri.fromFile(file).toString(), false, false, 0, 0, typeOverride = "adp")
|
|
245
260
|
}
|
|
246
261
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
262
|
+
mime.startsWith("audio") -> {
|
|
263
|
+
val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "mp3"
|
|
264
|
+
val file = createTempFile(ext)
|
|
265
|
+
resolver.openInputStream(uri)?.use { it.copyTo(FileOutputStream(file)) }
|
|
266
|
+
resolveResult(Uri.fromFile(file).toString(), false, true, 0, 0)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
mime.startsWith("video") -> {
|
|
270
|
+
val file = createTempFile("mp4")
|
|
271
|
+
resolver.openInputStream(uri)?.use { it.copyTo(FileOutputStream(file)) }
|
|
272
|
+
|
|
273
|
+
val retriever = MediaMetadataRetriever()
|
|
274
|
+
retriever.setDataSource(reactContext, uri)
|
|
275
|
+
val w = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0
|
|
276
|
+
val h = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0
|
|
277
|
+
retriever.release()
|
|
278
|
+
|
|
279
|
+
resolveResult(Uri.fromFile(file).toString(), true, false, w, h)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
else -> {
|
|
283
|
+
val file = createTempFile("jpg")
|
|
284
|
+
val bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri))
|
|
285
|
+
val rotated = rotateIfNeeded(bitmap, uri)
|
|
286
|
+
|
|
287
|
+
var quality = 100
|
|
288
|
+
var outStream = java.io.ByteArrayOutputStream()
|
|
289
|
+
rotated.compress(Bitmap.CompressFormat.JPEG, quality, outStream)
|
|
290
|
+
|
|
291
|
+
if (pendingTargetKB > 0) {
|
|
292
|
+
val targetBytes = pendingTargetKB * 1024
|
|
293
|
+
while (outStream.size() > targetBytes && quality > 10) {
|
|
294
|
+
quality -= 10
|
|
295
|
+
outStream = java.io.ByteArrayOutputStream()
|
|
296
|
+
rotated.compress(Bitmap.CompressFormat.JPEG, quality, outStream)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
FileOutputStream(file).use { it.write(outStream.toByteArray()) }
|
|
301
|
+
resolveResult(
|
|
302
|
+
Uri.fromFile(file).toString(),
|
|
303
|
+
false,
|
|
304
|
+
false,
|
|
305
|
+
rotated.width,
|
|
306
|
+
rotated.height,
|
|
307
|
+
outStream.size() / 1024,
|
|
308
|
+
)
|
|
309
|
+
}
|
|
259
310
|
}
|
|
311
|
+
} catch (e: Exception) {
|
|
312
|
+
pickerPromise?.reject("E_PROCESS", e.message)
|
|
313
|
+
} finally {
|
|
314
|
+
cleanup()
|
|
260
315
|
}
|
|
261
|
-
}
|
|
262
|
-
pickerPromise?.reject("E_PROCESS", e.message)
|
|
263
|
-
} finally {
|
|
264
|
-
cleanup()
|
|
265
|
-
}
|
|
316
|
+
}.start()
|
|
266
317
|
}
|
|
267
318
|
|
|
268
319
|
private fun rotateIfNeeded(
|
|
@@ -289,36 +340,45 @@ class FastMediaPickerModule(
|
|
|
289
340
|
isAudio: Boolean,
|
|
290
341
|
w: Int,
|
|
291
342
|
h: Int,
|
|
343
|
+
sizeKB: Int = 0,
|
|
344
|
+
typeOverride: String? = null, // ← for custom types like "adp"
|
|
292
345
|
) {
|
|
293
346
|
pickerPromise?.resolve(
|
|
294
347
|
Arguments.createMap().apply {
|
|
295
348
|
putString("uri", uri)
|
|
296
349
|
putBoolean("isVideo", isVideo)
|
|
297
350
|
putBoolean("isAudio", isAudio)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
putString(
|
|
301
|
-
|
|
302
|
-
if (isAudio) {
|
|
303
|
-
"audio"
|
|
304
|
-
} else if (isVideo) {
|
|
305
|
-
"video"
|
|
306
|
-
} else {
|
|
307
|
-
"image"
|
|
308
|
-
},
|
|
309
|
-
)
|
|
351
|
+
putDouble("width", w.toDouble())
|
|
352
|
+
putDouble("height", h.toDouble())
|
|
353
|
+
putString("type", typeOverride ?: if (isAudio) "audio" else if (isVideo) "video" else "image")
|
|
354
|
+
if (!isAudio && !isVideo && sizeKB > 0) putInt("size", sizeKB)
|
|
310
355
|
},
|
|
311
356
|
)
|
|
312
357
|
}
|
|
313
358
|
|
|
314
359
|
private fun createTempFile(ext: String) = File(reactContext.cacheDir, "fast-${UUID.randomUUID()}.$ext")
|
|
315
360
|
|
|
361
|
+
private fun getDisplayName(uri: Uri): String {
|
|
362
|
+
return runCatching {
|
|
363
|
+
reactContext.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)
|
|
364
|
+
?.use { cursor ->
|
|
365
|
+
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
366
|
+
if (nameIndex >= 0 && cursor.moveToFirst()) {
|
|
367
|
+
cursor.getString(nameIndex) ?: ""
|
|
368
|
+
} else {
|
|
369
|
+
""
|
|
370
|
+
}
|
|
371
|
+
} ?: ""
|
|
372
|
+
}.getOrDefault("")
|
|
373
|
+
}
|
|
374
|
+
|
|
316
375
|
private fun cleanup() {
|
|
317
376
|
pickerPromise = null
|
|
318
377
|
cameraCaptureUri = null
|
|
319
378
|
pendingFrontCamera = false
|
|
320
379
|
pendingUseCamera = false
|
|
321
380
|
pendingMediaType = null
|
|
381
|
+
pendingTargetKB = 0 // ← new
|
|
322
382
|
}
|
|
323
383
|
|
|
324
384
|
override fun onRequestPermissionsResult(
|
|
@@ -334,9 +394,11 @@ class FastMediaPickerModule(
|
|
|
334
394
|
return true
|
|
335
395
|
}
|
|
336
396
|
|
|
337
|
-
currentActivity?.let {
|
|
397
|
+
reactApplicationContext.currentActivity?.let { activity ->
|
|
398
|
+
launch(activity)
|
|
399
|
+
}
|
|
338
400
|
return true
|
|
339
401
|
}
|
|
340
402
|
|
|
341
|
-
override fun onNewIntent(intent: Intent
|
|
403
|
+
override fun onNewIntent(intent: Intent) {}
|
|
342
404
|
}
|
package/index.d.ts
CHANGED
|
@@ -7,21 +7,25 @@ declare module 'rns-mediapicker' {
|
|
|
7
7
|
uri: string;
|
|
8
8
|
width: number;
|
|
9
9
|
height: number;
|
|
10
|
-
|
|
10
|
+
size: number;
|
|
11
|
+
type: 'image' | 'video' | 'audio' | 'adp'; // ← adp added
|
|
11
12
|
isVideo: boolean;
|
|
13
|
+
isAudio: boolean;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export interface MediaPickerType {
|
|
15
17
|
/**
|
|
16
18
|
* Launch the media picker or camera.
|
|
17
19
|
* @param useCamera Set to true to open the camera directly.
|
|
18
|
-
* @param mediaType Specify 'image', 'video', or '
|
|
20
|
+
* @param mediaType Specify 'image', 'video', 'both', or 'audio'.
|
|
19
21
|
* @param env Use 'front' or 'back'.
|
|
22
|
+
* @param targetKB Max file size in KB (for image compression).
|
|
20
23
|
*/
|
|
21
24
|
pick(
|
|
22
25
|
useCamera?: boolean,
|
|
23
|
-
mediaType?: 'image' | 'video' | 'both' | 'audio',
|
|
24
|
-
env?: 'front' | 'back' | string
|
|
26
|
+
mediaType?: 'image' | 'video' | 'both' | 'audio' | 'adp', // ← adp added
|
|
27
|
+
env?: 'front' | 'back' | string,
|
|
28
|
+
targetKB?: number
|
|
25
29
|
): Promise<PickerResponse>;
|
|
26
30
|
}
|
|
27
31
|
|
package/index.js
CHANGED
|
@@ -5,17 +5,25 @@ const NativeModule = NativeModules.FastMediaPicker
|
|
|
5
5
|
: TurboModuleRegistry.get('FastMediaPicker');
|
|
6
6
|
|
|
7
7
|
const MediaPicker = {
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {boolean} useCamera
|
|
10
|
+
* @param {'image' | 'video' | 'both' | 'audio'} mediaType
|
|
11
|
+
* @param {'front' | 'back'} env
|
|
12
|
+
* @param {number} targetKB - Max size in KB (0 for no compression)
|
|
13
|
+
*/
|
|
14
|
+
pick: async (useCamera = false, mediaType = 'both', env = 'back', targetKB = 0) => {
|
|
9
15
|
if (!NativeModule) {
|
|
10
16
|
throw new Error("MediaPicker: Native module not found.");
|
|
11
17
|
}
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
const validTypes = ['image', 'video', 'both', 'audio', 'adp']; // ← adp added
|
|
20
|
+
const safeMediaType = validTypes.includes(mediaType) ? mediaType : 'both';
|
|
21
|
+
|
|
15
22
|
return await NativeModule.pick(
|
|
16
23
|
!!useCamera,
|
|
17
|
-
mediaType || 'both'
|
|
18
|
-
env || 'back'
|
|
24
|
+
safeMediaType, // ← was: mediaType || 'both' (didn't validate)
|
|
25
|
+
env || 'back',
|
|
26
|
+
targetKB || 0
|
|
19
27
|
);
|
|
20
28
|
}
|
|
21
29
|
};
|
package/ios/FastMediaPicker.m
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#import <React/RCTBridgeModule.h>
|
|
2
2
|
|
|
3
|
-
@interface RCT_EXTERN_MODULE(FastMediaPicker, NSObject)
|
|
3
|
+
@interface RCT_EXTERN_MODULE (FastMediaPicker, NSObject)
|
|
4
4
|
|
|
5
|
-
RCT_EXTERN_METHOD(pick:(BOOL)useCamera
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
rejecter:(RCTPromiseRejectBlock)reject)
|
|
5
|
+
RCT_EXTERN_METHOD(pick : (BOOL)useCamera mediaType : (NSString *)
|
|
6
|
+
mediaType env : (NSString *)env targetKB : (NSInteger)
|
|
7
|
+
targetKB resolver : (RCTPromiseResolveBlock)
|
|
8
|
+
resolve rejecter : (RCTPromiseRejectBlock)reject)
|
|
10
9
|
|
|
11
10
|
@end
|
|
@@ -1,38 +1,55 @@
|
|
|
1
1
|
import AVFoundation
|
|
2
2
|
import Photos
|
|
3
3
|
import PhotosUI
|
|
4
|
+
import React
|
|
4
5
|
import UIKit
|
|
5
6
|
import UniformTypeIdentifiers
|
|
6
|
-
import React
|
|
7
7
|
|
|
8
8
|
@objc(FastMediaPicker)
|
|
9
9
|
class FastMediaPicker: NSObject,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
UIImagePickerControllerDelegate,
|
|
11
|
+
UINavigationControllerDelegate,
|
|
12
|
+
UIAdaptivePresentationControllerDelegate,
|
|
13
|
+
PHPickerViewControllerDelegate,
|
|
14
|
+
UIDocumentPickerDelegate,
|
|
15
|
+
RCTBridgeModule
|
|
16
|
+
{
|
|
16
17
|
|
|
17
18
|
static func moduleName() -> String! { return "FastMediaPicker" }
|
|
18
19
|
static func requiresMainQueueSetup() -> Bool { return true }
|
|
19
20
|
|
|
20
21
|
private var resolve: RCTPromiseResolveBlock?
|
|
21
22
|
private var reject: RCTPromiseRejectBlock?
|
|
23
|
+
// Added to store the target size across picker callbacks
|
|
24
|
+
private var targetKB: Int = 0
|
|
22
25
|
|
|
23
|
-
@objc(pick:mediaType:env:resolver:rejecter:)
|
|
26
|
+
@objc(pick:mediaType:env:targetKB:resolver:rejecter:)
|
|
24
27
|
func pick(
|
|
25
28
|
_ useCamera: Bool,
|
|
26
29
|
mediaType: String,
|
|
27
30
|
env: String?,
|
|
31
|
+
targetKB: Int,
|
|
28
32
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
29
33
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
30
34
|
) {
|
|
31
35
|
self.resolve = resolve
|
|
32
36
|
self.reject = reject
|
|
37
|
+
self.targetKB = targetKB
|
|
33
38
|
let typeLower = mediaType.lowercased()
|
|
34
39
|
|
|
35
40
|
DispatchQueue.main.async {
|
|
41
|
+
// 1a. ADP (Custom project file)
|
|
42
|
+
if typeLower == "adp" {
|
|
43
|
+
// UTType(filenameExtension:) resolves our custom .adp type at runtime.
|
|
44
|
+
// Falls back to .data (generic binary) if not declared in Info.plist yet.
|
|
45
|
+
let adpType = UTType(filenameExtension: "adp") ?? .data
|
|
46
|
+
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [adpType])
|
|
47
|
+
picker.delegate = self
|
|
48
|
+
picker.allowsMultipleSelection = false
|
|
49
|
+
self.topMostViewController()?.present(picker, animated: true)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
// 1. AUDIO (Document Picker)
|
|
37
54
|
if typeLower == "audio" {
|
|
38
55
|
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.audio])
|
|
@@ -75,9 +92,13 @@ class FastMediaPicker: NSObject,
|
|
|
75
92
|
var config = PHPickerConfiguration()
|
|
76
93
|
config.selectionLimit = 1
|
|
77
94
|
|
|
78
|
-
if typeLower == "image" {
|
|
79
|
-
|
|
80
|
-
else
|
|
95
|
+
if typeLower == "image" {
|
|
96
|
+
config.filter = .images
|
|
97
|
+
} else if typeLower == "video" {
|
|
98
|
+
config.filter = .videos
|
|
99
|
+
} else {
|
|
100
|
+
config.filter = .any(of: [.images, .videos])
|
|
101
|
+
}
|
|
81
102
|
|
|
82
103
|
let picker = PHPickerViewController(configuration: config)
|
|
83
104
|
picker.delegate = self
|
|
@@ -101,7 +122,8 @@ class FastMediaPicker: NSObject,
|
|
|
101
122
|
|
|
102
123
|
// Check for Video
|
|
103
124
|
if provider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
|
104
|
-
provider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) {
|
|
125
|
+
provider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) {
|
|
126
|
+
[weak self] url, error in
|
|
105
127
|
if let url = url {
|
|
106
128
|
self?.handlePickedVideo(url: url)
|
|
107
129
|
} else {
|
|
@@ -122,12 +144,14 @@ class FastMediaPicker: NSObject,
|
|
|
122
144
|
}
|
|
123
145
|
return
|
|
124
146
|
}
|
|
125
|
-
|
|
147
|
+
|
|
126
148
|
safeReject("E_NO_MEDIA", "Unsupported media type")
|
|
127
149
|
}
|
|
128
150
|
|
|
129
151
|
// MARK: - Document Picker (Audio)
|
|
130
|
-
func documentPicker(
|
|
152
|
+
func documentPicker(
|
|
153
|
+
_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]
|
|
154
|
+
) {
|
|
131
155
|
guard let url = urls.first else {
|
|
132
156
|
safeReject("E_CANCELLED", "No file selected")
|
|
133
157
|
return
|
|
@@ -145,12 +169,22 @@ class FastMediaPicker: NSObject,
|
|
|
145
169
|
}
|
|
146
170
|
try FileManager.default.copyItem(at: url, to: fileURL)
|
|
147
171
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
172
|
+
// Distinguish .adp project files from regular audio by extension
|
|
173
|
+
if ext == "adp" {
|
|
174
|
+
self.safeResolve([
|
|
175
|
+
"uri": fileURL.absoluteString,
|
|
176
|
+
"type": "adp",
|
|
177
|
+
"isAudio": false,
|
|
178
|
+
"isVideo": false,
|
|
179
|
+
])
|
|
180
|
+
} else {
|
|
181
|
+
self.safeResolve([
|
|
182
|
+
"uri": fileURL.absoluteString,
|
|
183
|
+
"type": "audio",
|
|
184
|
+
"isAudio": true,
|
|
185
|
+
"isVideo": false,
|
|
186
|
+
])
|
|
187
|
+
}
|
|
154
188
|
} catch {
|
|
155
189
|
safeReject("E_AUDIO_COPY", error.localizedDescription)
|
|
156
190
|
}
|
|
@@ -161,8 +195,10 @@ class FastMediaPicker: NSObject,
|
|
|
161
195
|
}
|
|
162
196
|
|
|
163
197
|
// MARK: - UIImagePicker (Camera)
|
|
164
|
-
func imagePickerController(
|
|
165
|
-
|
|
198
|
+
func imagePickerController(
|
|
199
|
+
_ picker: UIImagePickerController,
|
|
200
|
+
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
|
|
201
|
+
) {
|
|
166
202
|
picker.dismiss(animated: true)
|
|
167
203
|
|
|
168
204
|
if let mediaURL = info[.mediaURL] as? URL {
|
|
@@ -188,21 +224,38 @@ class FastMediaPicker: NSObject,
|
|
|
188
224
|
private func processPickedImage(_ image: UIImage) {
|
|
189
225
|
let fileURL = makeTempURL(ext: "jpg")
|
|
190
226
|
|
|
191
|
-
// Standardize
|
|
192
|
-
|
|
227
|
+
// 1. Initial attempt at high quality (Standardize orientation automatically via jpegData)
|
|
228
|
+
var compression: CGFloat = 1.0
|
|
229
|
+
guard var data = image.jpegData(compressionQuality: compression) else {
|
|
193
230
|
safeReject("E_IMAGE_WRITE", "Image encoding failed")
|
|
194
231
|
return
|
|
195
232
|
}
|
|
196
233
|
|
|
234
|
+
// 2. Iterative compression logic
|
|
235
|
+
if targetKB > 0 {
|
|
236
|
+
let targetBytes = targetKB * 1024
|
|
237
|
+
|
|
238
|
+
// If the initial file is already larger than target, reduce quality
|
|
239
|
+
if data.count > targetBytes {
|
|
240
|
+
while data.count > targetBytes && compression > 0.1 {
|
|
241
|
+
compression -= 0.1
|
|
242
|
+
if let compressedData = image.jpegData(compressionQuality: compression) {
|
|
243
|
+
data = compressedData
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
197
249
|
do {
|
|
198
250
|
try data.write(to: fileURL)
|
|
199
251
|
self.safeResolve([
|
|
200
252
|
"uri": fileURL.absoluteString,
|
|
201
253
|
"width": image.size.width,
|
|
202
254
|
"height": image.size.height,
|
|
255
|
+
"size": data.count / 1024, // Size in KB
|
|
203
256
|
"type": "image",
|
|
204
257
|
"isAudio": false,
|
|
205
|
-
"isVideo": false
|
|
258
|
+
"isVideo": false,
|
|
206
259
|
])
|
|
207
260
|
} catch {
|
|
208
261
|
safeReject("E_IMAGE_WRITE", error.localizedDescription)
|
|
@@ -227,7 +280,7 @@ class FastMediaPicker: NSObject,
|
|
|
227
280
|
"height": dims.height,
|
|
228
281
|
"type": "video",
|
|
229
282
|
"isAudio": false,
|
|
230
|
-
"isVideo": true
|
|
283
|
+
"isVideo": true,
|
|
231
284
|
])
|
|
232
285
|
} catch {
|
|
233
286
|
safeReject("E_VIDEO_COPY", error.localizedDescription)
|
|
@@ -284,4 +337,4 @@ class FastMediaPicker: NSObject,
|
|
|
284
337
|
let size = track.naturalSize.applying(track.preferredTransform)
|
|
285
338
|
return (abs(size.width), abs(size.height))
|
|
286
339
|
}
|
|
287
|
-
}
|
|
340
|
+
}
|
package/package.json
CHANGED
package/withMediaPicker.js
CHANGED
|
@@ -138,6 +138,28 @@ const withIosConfig = (config) => {
|
|
|
138
138
|
plist.NSMicrophoneUsageDescription ||
|
|
139
139
|
'Allow access to your microphone when recording videos.';
|
|
140
140
|
|
|
141
|
+
// Register .adp as a known custom type so UIDocumentPickerViewController
|
|
142
|
+
// can filter to it exclusively instead of falling back to showing all files.
|
|
143
|
+
const adpTypeIdentifier = 'com.audipella.adp';
|
|
144
|
+
const existingImports = plist.UTImportedTypeDeclarations || [];
|
|
145
|
+
const alreadyDeclared = existingImports.some(
|
|
146
|
+
(t) => t.UTTypeIdentifier === adpTypeIdentifier
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (!alreadyDeclared) {
|
|
150
|
+
plist.UTImportedTypeDeclarations = [
|
|
151
|
+
...existingImports,
|
|
152
|
+
{
|
|
153
|
+
UTTypeIdentifier: adpTypeIdentifier,
|
|
154
|
+
UTTypeDescription: 'Audipella Project',
|
|
155
|
+
UTTypeConformsTo: ['public.data'],
|
|
156
|
+
UTTypeTagSpecification: {
|
|
157
|
+
'public.filename-extension': ['adp'],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
|
|
141
163
|
return config;
|
|
142
164
|
});
|
|
143
165
|
};
|