@technotoil/image-video-editor 0.1.0 → 0.1.2
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 +17 -3
- package/android/src/main/java/com/technotoil/image_videoeditor/FrameGrabberModule.kt +2 -6
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaEditorModule.kt +75 -35
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaLibraryModule.kt +51 -35
- package/android/src/main/java/com/technotoil/image_videoeditor/RNVideoPreviewManager.kt +98 -117
- package/ios/RNMediaEditor.m +38 -7
- package/ios/RNMediaLibrary.m +19 -15
- package/ios/RNVideoPreviewManager.m +2 -0
- package/lib/commonjs/assets/frames/film_vintage.png +0 -0
- package/lib/commonjs/assets/frames/floral_gold.png +0 -0
- package/lib/commonjs/assets/frames/minimal_double.png +0 -0
- package/lib/commonjs/assets/frames/polaroid_white.png +0 -0
- package/lib/commonjs/assets/frames/watercolor_floral.png +0 -0
- package/lib/commonjs/components/VideoEditor.js +235 -0
- package/lib/commonjs/components/VideoEditor.js.map +1 -0
- package/lib/commonjs/index.js +14 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/native/CameraView.js +109 -0
- package/lib/commonjs/native/CameraView.js.map +1 -0
- package/lib/commonjs/native/FrameGrabber.js +17 -0
- package/lib/commonjs/native/FrameGrabber.js.map +1 -0
- package/lib/commonjs/native/MediaEditor.js +24 -0
- package/lib/commonjs/native/MediaEditor.js.map +1 -0
- package/lib/commonjs/native/MediaLibrary.js +45 -0
- package/lib/commonjs/native/MediaLibrary.js.map +1 -0
- package/lib/commonjs/native/MediaPicker.js +17 -0
- package/lib/commonjs/native/MediaPicker.js.map +1 -0
- package/lib/commonjs/native/MediaPlayer.js +17 -0
- package/lib/commonjs/native/MediaPlayer.js.map +1 -0
- package/lib/commonjs/native/VideoPreview.js +17 -0
- package/lib/commonjs/native/VideoPreview.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/screens/CropScreen.js +1233 -0
- package/lib/commonjs/screens/CropScreen.js.map +1 -0
- package/lib/commonjs/screens/EditorScreen.js +6043 -0
- package/lib/commonjs/screens/EditorScreen.js.map +1 -0
- package/lib/commonjs/screens/ExportScreen.js +294 -0
- package/lib/commonjs/screens/ExportScreen.js.map +1 -0
- package/lib/commonjs/screens/GalleryScreen.js +510 -0
- package/lib/commonjs/screens/GalleryScreen.js.map +1 -0
- package/lib/commonjs/screens/PickScreen.js +1353 -0
- package/lib/commonjs/screens/PickScreen.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/components/VideoEditor.js +104 -31
- package/lib/module/components/VideoEditor.js.map +1 -1
- package/lib/module/screens/CropScreen.js +26 -9
- package/lib/module/screens/CropScreen.js.map +1 -1
- package/lib/module/screens/EditorScreen.js +371 -86
- package/lib/module/screens/EditorScreen.js.map +1 -1
- package/lib/module/screens/PickScreen.js +245 -93
- package/lib/module/screens/PickScreen.js.map +1 -1
- package/lib/typescript/src/components/VideoEditor.d.ts +18 -2
- package/lib/typescript/src/screens/CropScreen.d.ts +3 -1
- package/lib/typescript/src/screens/EditorScreen.d.ts +2 -1
- package/lib/typescript/src/screens/PickScreen.d.ts +6 -1
- package/lib/typescript/src/types.d.ts +1 -0
- package/package.json +17 -8
- package/src/components/VideoEditor.tsx +82 -11
- package/src/screens/CropScreen.tsx +54 -33
- package/src/screens/EditorScreen.tsx +366 -106
- package/src/screens/PickScreen.tsx +231 -76
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@ A high-performance, feature-rich React Native image and video editor. This libra
|
|
|
6
6
|
* 📹 **Native Camera View**: High-performance thread-safe AVCaptureSession wrapper for iOS and custom camera on Android.
|
|
7
7
|
* ✂️ **Video Trimming**: Seamless interactive trimming of video files.
|
|
8
8
|
* 🖼️ **Custom Photo Frames**: Layer beautiful custom frames on top of images.
|
|
9
|
-
* 🎵 **Audio Mixing**:
|
|
10
|
-
* 🎨 **Real-time Filters**: High-performance shaders/filters for photo and video formats.
|
|
9
|
+
* 🎵 **Audio Mixing & Carousel Sync**: Add background music tracks to your videos. Applies custom audio seamlessly across all videos in a multi-item carousel, automatically pausing original video sound.
|
|
11
10
|
* 📦 **Modular Export**: Triggers callbacks with local filesystem URIs suitable for server uploads.
|
|
11
|
+
* 📱 **Multi-Select & Swiping**: Select up to 5 items simultaneously and edit them smoothly via horizontal swiping. UI state and trimming configurations perfectly sync across items.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -44,11 +44,17 @@ Ensure the following keys are added to your `Info.plist`:
|
|
|
44
44
|
<string>We need access to your microphone to record audio for videos.</string>
|
|
45
45
|
<key>NSPhotoLibraryUsageDescription</key>
|
|
46
46
|
<string>We need access to your photo library to pick and save media files.</string>
|
|
47
|
+
<key>UIAppFonts</key>
|
|
48
|
+
<array>
|
|
49
|
+
<string>Ionicons.ttf</string>
|
|
50
|
+
</array>
|
|
47
51
|
```
|
|
48
52
|
|
|
49
53
|
#### Android Installation
|
|
50
|
-
Make sure to add the FFmpeg kit dependency in your app's `android/app/build.gradle`:
|
|
54
|
+
Make sure to add the FFmpeg kit dependency in your app's `android/app/build.gradle`. You will also need to link the fonts from `react-native-vector-icons`:
|
|
51
55
|
```groovy
|
|
56
|
+
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
|
57
|
+
|
|
52
58
|
dependencies {
|
|
53
59
|
implementation("io.github.maitrungduc1410:ffmpeg-kit-min:6.0.1")
|
|
54
60
|
}
|
|
@@ -92,6 +98,9 @@ export default function App() {
|
|
|
92
98
|
headerTitle="Create New Post"
|
|
93
99
|
cameraModes={['POST', 'STORY', 'REEL']}
|
|
94
100
|
defaultCameraMode="REEL"
|
|
101
|
+
mediaType="any"
|
|
102
|
+
mediaTabs={['GALLERY', 'PHOTO', 'VIDEO']}
|
|
103
|
+
maxVideoDurationMs={30000} // Force trim videos longer than 30s
|
|
95
104
|
onCancelPress={() => setEditorVisible(false)}
|
|
96
105
|
onFinishExport={(editedMedia, paths, editedArray, cameraMode) => {
|
|
97
106
|
console.log('Export completed successfully!');
|
|
@@ -126,6 +135,11 @@ const styles = StyleSheet.create({
|
|
|
126
135
|
| `cameraModes` | `Array<'POST' \| 'STORY' \| 'REEL'>` | `['POST', 'STORY', 'REEL']` | Active shooting modes allowed. |
|
|
127
136
|
| `defaultCameraMode` | `'POST' \| 'STORY' \| 'REEL'` | `"REEL"` | Initial shooting mode when opening. |
|
|
128
137
|
| `musicList` | `MusicTrack[]` | `[]` | List of audio tracks available to overlay on videos. |
|
|
138
|
+
| `maxSelection` | `number` | `1` | Maximum number of media items user can select. Max allowed is 5. |
|
|
139
|
+
| `aspectRatio` | `'1:1' \| '4:3' \| '4:5' \| '16:9' \| '9:16' \| 'free'` | `'free'` | Enforce a fixed aspect ratio for image/video preview. |
|
|
140
|
+
| `mediaType` | `'photo' \| 'video' \| 'any'` | `'any'` | Filter the library to only show photos, videos, or both. |
|
|
141
|
+
| `mediaTabs` | `Array<'GALLERY' \| 'PHOTO' \| 'VIDEO'>` | `['GALLERY', 'PHOTO', 'VIDEO']` | Control which selection tabs are visible at the bottom of the picker screen. Defaults to the first array item. |
|
|
142
|
+
| `maxVideoDurationMs` | `number` | `undefined` | Maximum video duration allowed in milliseconds. If a video exceeds this, the trim editor will automatically force the user to trim. |
|
|
129
143
|
| `onCancelPress` | `() => void` | `undefined` | Callback fired when user cancels or leaves the editor. |
|
|
130
144
|
| `onFinishExport` | `(editedMedia: any, paths: string[], editedArray: any[], cameraMode: string) => void` | `undefined` | Fired when edits finish exporting. Fills `paths` with target video/image file URIs. |
|
|
131
145
|
|
|
@@ -34,15 +34,11 @@ class FrameGrabberModule(private val reactContext: ReactApplicationContext) :
|
|
|
34
34
|
|
|
35
35
|
var bitmap: Bitmap? = null
|
|
36
36
|
try {
|
|
37
|
-
bitmap = retriever.getFrameAtTime(timeUs, MediaMetadataRetriever.
|
|
37
|
+
bitmap = retriever.getFrameAtTime(timeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
|
38
38
|
} catch (e: Exception) {
|
|
39
|
-
// Fallback
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (bitmap == null) {
|
|
43
39
|
try {
|
|
44
40
|
bitmap = retriever.getFrameAtTime(timeUs)
|
|
45
|
-
} catch (
|
|
41
|
+
} catch (e2: Exception) {
|
|
46
42
|
// Fallback
|
|
47
43
|
}
|
|
48
44
|
}
|
|
@@ -38,11 +38,11 @@ class MediaEditorModule(private val reactContext: ReactApplicationContext) :
|
|
|
38
38
|
return uri
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
private fun downloadToCache(urlString: String): File {
|
|
41
|
+
private fun downloadToCache(urlString: String, prefix: String = "music_", ext: String = ".mp3"): File {
|
|
42
42
|
val url = java.net.URL(urlString)
|
|
43
43
|
val connection = url.openConnection()
|
|
44
44
|
connection.connect()
|
|
45
|
-
val cacheFile = File.createTempFile(
|
|
45
|
+
val cacheFile = File.createTempFile(prefix, ext, reactContext.cacheDir)
|
|
46
46
|
url.openStream().use { input ->
|
|
47
47
|
FileOutputStream(cacheFile).use { output ->
|
|
48
48
|
input.copyTo(output)
|
|
@@ -158,18 +158,27 @@ class MediaEditorModule(private val reactContext: ReactApplicationContext) :
|
|
|
158
158
|
// Frame drawing
|
|
159
159
|
if (hasFrame) {
|
|
160
160
|
try {
|
|
161
|
-
val
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
val frameUriString = if (options.hasKey("frameUri")) options.getString("frameUri") else null
|
|
162
|
+
var frameFile: File? = null
|
|
163
|
+
if (!frameUriString.isNullOrEmpty()) {
|
|
164
|
+
frameFile = if (frameUriString.startsWith("http://") || frameUriString.startsWith("https://")) {
|
|
165
|
+
downloadToCache(frameUriString, "frame_", ".png")
|
|
166
|
+
} else {
|
|
167
|
+
MediaFileUtils.copyToCache(reactContext, Uri.parse(frameUriString), "frame_")
|
|
168
|
+
}
|
|
169
|
+
}
|
|
165
170
|
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
if (frameFile != null) {
|
|
172
|
+
val frameBitmap = BitmapFactory.decodeFile(frameFile.absolutePath)
|
|
173
|
+
if (frameBitmap != null) {
|
|
174
|
+
android.util.Log.d("RNMediaEditor", "Frame loaded: $rawFrameKey ${frameBitmap.width}x${frameBitmap.height}")
|
|
175
|
+
val destRect = Rect(0, 0, baseImage.width, baseImage.height)
|
|
176
|
+
baseCanvas.drawBitmap(frameBitmap, null, destRect, null)
|
|
177
|
+
frameBitmap.recycle()
|
|
178
|
+
} else {
|
|
179
|
+
android.util.Log.w("RNMediaEditor", "Frame bitmap decode returned null for: $rawFrameKey")
|
|
180
|
+
}
|
|
181
|
+
try { if (frameFile.exists()) frameFile.delete() } catch (_: Exception) {}
|
|
173
182
|
}
|
|
174
183
|
} catch (e: Exception) {
|
|
175
184
|
android.util.Log.e("RNMediaEditor", "Frame load FAILED for: $rawFrameKey — ${e.message}", e)
|
|
@@ -322,14 +331,18 @@ class MediaEditorModule(private val reactContext: ReactApplicationContext) :
|
|
|
322
331
|
if (!editedDir.exists()) editedDir.mkdirs()
|
|
323
332
|
val outFile = File.createTempFile("edited_video_", ".mp4", editedDir)
|
|
324
333
|
|
|
325
|
-
//
|
|
334
|
+
// Download/copy frame png from uri
|
|
326
335
|
var frameFile: File? = null
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
336
|
+
val frameUriString = if (options.hasKey("frameUri")) options.getString("frameUri") else null
|
|
337
|
+
if (hasFrame && !frameUriString.isNullOrEmpty()) {
|
|
338
|
+
try {
|
|
339
|
+
frameFile = if (frameUriString!!.startsWith("http://") || frameUriString.startsWith("https://")) {
|
|
340
|
+
downloadToCache(frameUriString, "frame_", ".png")
|
|
341
|
+
} else {
|
|
342
|
+
MediaFileUtils.copyToCache(reactContext, Uri.parse(frameUriString), "frame_")
|
|
332
343
|
}
|
|
344
|
+
} catch (e: Exception) {
|
|
345
|
+
android.util.Log.e("RNMediaEditor", "Failed to get frame file: ${e.message}")
|
|
333
346
|
}
|
|
334
347
|
}
|
|
335
348
|
|
|
@@ -338,7 +351,7 @@ class MediaEditorModule(private val reactContext: ReactApplicationContext) :
|
|
|
338
351
|
if (hasMusic) {
|
|
339
352
|
try {
|
|
340
353
|
musicFile = if (musicUri!!.startsWith("http://") || musicUri.startsWith("https://")) {
|
|
341
|
-
downloadToCache(musicUri)
|
|
354
|
+
downloadToCache(musicUri, "music_", ".mp3")
|
|
342
355
|
} else {
|
|
343
356
|
MediaFileUtils.copyToCache(reactContext, Uri.parse(musicUri), "music")
|
|
344
357
|
}
|
|
@@ -429,10 +442,14 @@ class MediaEditorModule(private val reactContext: ReactApplicationContext) :
|
|
|
429
442
|
}
|
|
430
443
|
}
|
|
431
444
|
|
|
445
|
+
val hasVisualFilters = filterSteps.isNotEmpty() || isImage
|
|
446
|
+
|
|
432
447
|
// Final output label assignment - ensure even dimensions for the encoder
|
|
433
|
-
if (
|
|
434
|
-
|
|
435
|
-
|
|
448
|
+
if (hasVisualFilters) {
|
|
449
|
+
if (currentLabel != "[vout]") {
|
|
450
|
+
filterSteps.add("${currentLabel}scale=trunc(iw/2)*2:trunc(ih/2)*2[vout]")
|
|
451
|
+
currentLabel = "[vout]"
|
|
452
|
+
}
|
|
436
453
|
}
|
|
437
454
|
|
|
438
455
|
// Audio configuration
|
|
@@ -451,6 +468,8 @@ class MediaEditorModule(private val reactContext: ReactApplicationContext) :
|
|
|
451
468
|
|
|
452
469
|
val musicInputIndex = if (hasFrame) 2 else 1
|
|
453
470
|
|
|
471
|
+
val musicOffsetMs = if (options.hasKey("musicOffsetMs")) options.getDouble("musicOffsetMs") else 0.0
|
|
472
|
+
|
|
454
473
|
val audioArgsList = if (hasMusic && musicFile != null) {
|
|
455
474
|
if (hasVideoAudio && !mute) {
|
|
456
475
|
// Mix audio tracks: [0:a] and [$musicInputIndex:a]
|
|
@@ -501,24 +520,45 @@ class MediaEditorModule(private val reactContext: ReactApplicationContext) :
|
|
|
501
520
|
}
|
|
502
521
|
|
|
503
522
|
if (hasMusic && musicFile != null) {
|
|
523
|
+
if (musicOffsetMs > 0) {
|
|
524
|
+
cmdList.add("-ss")
|
|
525
|
+
cmdList.add(f(musicOffsetMs / 1000.0))
|
|
526
|
+
}
|
|
504
527
|
cmdList.add("-i")
|
|
505
528
|
cmdList.add(musicFile.absolutePath)
|
|
506
529
|
}
|
|
507
530
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
531
|
+
if (filterComplex.isNotEmpty()) {
|
|
532
|
+
cmdList.add("-filter_complex")
|
|
533
|
+
cmdList.add(filterComplex)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (hasVisualFilters) {
|
|
537
|
+
cmdList.add("-map")
|
|
538
|
+
cmdList.add("[vout]")
|
|
539
|
+
} else {
|
|
540
|
+
cmdList.add("-map")
|
|
541
|
+
cmdList.add("0:v")
|
|
542
|
+
}
|
|
543
|
+
|
|
512
544
|
cmdList.addAll(audioArgsList)
|
|
513
545
|
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
cmdList.add(
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
546
|
+
// Force the output duration to match the intended trim duration exactly
|
|
547
|
+
cmdList.add("-t")
|
|
548
|
+
cmdList.add(ttString)
|
|
549
|
+
|
|
550
|
+
if (hasVisualFilters) {
|
|
551
|
+
cmdList.add("-c:v")
|
|
552
|
+
cmdList.add("h264_mediacodec")
|
|
553
|
+
cmdList.add("-b:v")
|
|
554
|
+
cmdList.add("5M")
|
|
555
|
+
cmdList.add("-pix_fmt")
|
|
556
|
+
cmdList.add("yuv420p")
|
|
557
|
+
} else {
|
|
558
|
+
cmdList.add("-c:v")
|
|
559
|
+
cmdList.add("copy")
|
|
560
|
+
}
|
|
561
|
+
|
|
522
562
|
cmdList.add(outFile.absolutePath)
|
|
523
563
|
|
|
524
564
|
val cmdArray = cmdList.toTypedArray()
|
|
@@ -26,11 +26,6 @@ class MediaLibraryModule(private val reactContext: ReactApplicationContext) :
|
|
|
26
26
|
|
|
27
27
|
@ReactMethod
|
|
28
28
|
fun requestAccess(promise: Promise) {
|
|
29
|
-
val activity = getCurrentActivity()
|
|
30
|
-
if (activity == null) {
|
|
31
|
-
promise.resolve(false)
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
29
|
val permissions = if (android.os.Build.VERSION.SDK_INT >= 33) {
|
|
35
30
|
arrayOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO)
|
|
36
31
|
} else {
|
|
@@ -43,6 +38,13 @@ class MediaLibraryModule(private val reactContext: ReactApplicationContext) :
|
|
|
43
38
|
promise.resolve(true)
|
|
44
39
|
return
|
|
45
40
|
}
|
|
41
|
+
|
|
42
|
+
val activity = getCurrentActivity()
|
|
43
|
+
if (activity == null) {
|
|
44
|
+
promise.resolve(false)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
46
48
|
if (activity is com.facebook.react.modules.core.PermissionAwareActivity) {
|
|
47
49
|
activity.requestPermissions(permissions, 4422,
|
|
48
50
|
com.facebook.react.modules.core.PermissionListener { _: Int, _: Array<String>, grantResults: IntArray ->
|
|
@@ -82,7 +84,7 @@ class MediaLibraryModule(private val reactContext: ReactApplicationContext) :
|
|
|
82
84
|
while (cursor.moveToNext()) {
|
|
83
85
|
val id = cursor.getString(idCol)
|
|
84
86
|
val name = cursor.getString(nameCol) ?: "Unknown"
|
|
85
|
-
if (!seenIds.contains(id)) {
|
|
87
|
+
if (id != null && !seenIds.contains(id)) {
|
|
86
88
|
val map = Arguments.createMap()
|
|
87
89
|
map.putString("id", id)
|
|
88
90
|
map.putString("title", name)
|
|
@@ -143,7 +145,7 @@ class MediaLibraryModule(private val reactContext: ReactApplicationContext) :
|
|
|
143
145
|
}
|
|
144
146
|
val id = cursor.getLong(idCol)
|
|
145
147
|
val mediaType = cursor.getInt(typeCol)
|
|
146
|
-
|
|
148
|
+
var duration = cursor.getLong(durationCol)
|
|
147
149
|
|
|
148
150
|
val uri = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO) {
|
|
149
151
|
ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id)
|
|
@@ -151,6 +153,18 @@ class MediaLibraryModule(private val reactContext: ReactApplicationContext) :
|
|
|
151
153
|
ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO && duration <= 0) {
|
|
157
|
+
try {
|
|
158
|
+
val retriever = android.media.MediaMetadataRetriever()
|
|
159
|
+
retriever.setDataSource(reactContext, uri)
|
|
160
|
+
val durStr = retriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_DURATION)
|
|
161
|
+
if (durStr != null) {
|
|
162
|
+
duration = durStr.toLong()
|
|
163
|
+
}
|
|
164
|
+
retriever.release()
|
|
165
|
+
} catch (e: Exception) {}
|
|
166
|
+
}
|
|
167
|
+
|
|
154
168
|
val thumbUri = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO) {
|
|
155
169
|
createVideoThumbnail(uri, id)
|
|
156
170
|
} else {
|
|
@@ -202,42 +216,44 @@ class MediaLibraryModule(private val reactContext: ReactApplicationContext) :
|
|
|
202
216
|
|
|
203
217
|
@ReactMethod
|
|
204
218
|
fun exportAsset(localId: String, promise: Promise) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// Check if it is a video first
|
|
211
|
-
val videoUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id)
|
|
212
|
-
var cursor = resolver.query(videoUri, arrayOf(MediaStore.Video.Media._ID), null, null, null)
|
|
213
|
-
if (cursor != null) {
|
|
214
|
-
if (cursor.moveToFirst()) {
|
|
215
|
-
uri = videoUri
|
|
216
|
-
}
|
|
217
|
-
cursor.close()
|
|
218
|
-
}
|
|
219
|
+
Thread {
|
|
220
|
+
try {
|
|
221
|
+
val id = localId.toLong()
|
|
222
|
+
val resolver = reactContext.contentResolver
|
|
223
|
+
var uri: Uri? = null
|
|
219
224
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
cursor = resolver.query(imageUri, arrayOf(MediaStore.Images.Media._ID), null, null, null)
|
|
225
|
+
// Check if it is a video first
|
|
226
|
+
val videoUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id)
|
|
227
|
+
var cursor = resolver.query(videoUri, arrayOf(MediaStore.Video.Media._ID), null, null, null)
|
|
224
228
|
if (cursor != null) {
|
|
225
229
|
if (cursor.moveToFirst()) {
|
|
226
|
-
uri =
|
|
230
|
+
uri = videoUri
|
|
227
231
|
}
|
|
228
232
|
cursor.close()
|
|
229
233
|
}
|
|
230
|
-
}
|
|
231
234
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
// If not a video, check if it's an image
|
|
236
|
+
if (uri == null) {
|
|
237
|
+
val imageUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
|
238
|
+
cursor = resolver.query(imageUri, arrayOf(MediaStore.Images.Media._ID), null, null, null)
|
|
239
|
+
if (cursor != null) {
|
|
240
|
+
if (cursor.moveToFirst()) {
|
|
241
|
+
uri = imageUri
|
|
242
|
+
}
|
|
243
|
+
cursor.close()
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (uri != null) {
|
|
248
|
+
val cacheFile = MediaFileUtils.copyToCache(reactContext, uri, "export")
|
|
249
|
+
promise.resolve(Uri.fromFile(cacheFile).toString())
|
|
250
|
+
} else {
|
|
251
|
+
promise.reject("not_found", "Asset not found in Video or Image MediaStore")
|
|
252
|
+
}
|
|
253
|
+
} catch (e: Exception) {
|
|
254
|
+
promise.reject("export_failed", e.message, e)
|
|
237
255
|
}
|
|
238
|
-
}
|
|
239
|
-
promise.reject("export_failed", e.message, e)
|
|
240
|
-
}
|
|
256
|
+
}.start()
|
|
241
257
|
}
|
|
242
258
|
|
|
243
259
|
@ReactMethod
|