@technotoil/image-video-editor 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.
Files changed (99) hide show
  1. package/ImageVideoEditor.podspec +21 -0
  2. package/README.md +136 -0
  3. package/android/build.gradle +76 -0
  4. package/android/gradle.properties +5 -0
  5. package/android/src/main/AndroidManifest.xml +13 -0
  6. package/android/src/main/java/com/technotoil/image_videoeditor/FrameGrabberModule.kt +67 -0
  7. package/android/src/main/java/com/technotoil/image_videoeditor/MediaEditorModule.kt +548 -0
  8. package/android/src/main/java/com/technotoil/image_videoeditor/MediaFileUtils.kt +29 -0
  9. package/android/src/main/java/com/technotoil/image_videoeditor/MediaLibraryModule.kt +305 -0
  10. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPackage.kt +26 -0
  11. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPickerModule.kt +111 -0
  12. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPlayerModule.kt +34 -0
  13. package/android/src/main/java/com/technotoil/image_videoeditor/RNCameraViewManager.kt +761 -0
  14. package/android/src/main/java/com/technotoil/image_videoeditor/RNVideoPreviewManager.kt +317 -0
  15. package/ios/PrivacyInfo.xcprivacy +38 -0
  16. package/ios/RNCameraViewManager.m +420 -0
  17. package/ios/RNFrameGrabber.m +61 -0
  18. package/ios/RNMediaEditor.m +905 -0
  19. package/ios/RNMediaLibrary.m +389 -0
  20. package/ios/RNMediaPicker.m +144 -0
  21. package/ios/RNMediaPlayer.m +73 -0
  22. package/ios/RNVideoPreviewManager.m +263 -0
  23. package/ios/frames/film_vintage.png +0 -0
  24. package/ios/frames/floral_gold.png +0 -0
  25. package/ios/frames/minimal_double.png +0 -0
  26. package/ios/frames/polaroid_white.png +0 -0
  27. package/ios/frames/watercolor_floral.png +0 -0
  28. package/lib/module/assets/frames/film_vintage.png +0 -0
  29. package/lib/module/assets/frames/floral_gold.png +0 -0
  30. package/lib/module/assets/frames/minimal_double.png +0 -0
  31. package/lib/module/assets/frames/polaroid_white.png +0 -0
  32. package/lib/module/assets/frames/watercolor_floral.png +0 -0
  33. package/lib/module/components/VideoEditor.js +156 -0
  34. package/lib/module/components/VideoEditor.js.map +1 -0
  35. package/lib/module/index.js +4 -0
  36. package/lib/module/index.js.map +1 -0
  37. package/lib/module/native/CameraView.js +104 -0
  38. package/lib/module/native/CameraView.js.map +1 -0
  39. package/lib/module/native/FrameGrabber.js +13 -0
  40. package/lib/module/native/FrameGrabber.js.map +1 -0
  41. package/lib/module/native/MediaEditor.js +19 -0
  42. package/lib/module/native/MediaEditor.js.map +1 -0
  43. package/lib/module/native/MediaLibrary.js +37 -0
  44. package/lib/module/native/MediaLibrary.js.map +1 -0
  45. package/lib/module/native/MediaPicker.js +13 -0
  46. package/lib/module/native/MediaPicker.js.map +1 -0
  47. package/lib/module/native/MediaPlayer.js +13 -0
  48. package/lib/module/native/MediaPlayer.js.map +1 -0
  49. package/lib/module/native/VideoPreview.js +12 -0
  50. package/lib/module/native/VideoPreview.js.map +1 -0
  51. package/lib/module/package.json +1 -0
  52. package/lib/module/screens/CropScreen.js +1211 -0
  53. package/lib/module/screens/CropScreen.js.map +1 -0
  54. package/lib/module/screens/EditorScreen.js +5752 -0
  55. package/lib/module/screens/EditorScreen.js.map +1 -0
  56. package/lib/module/screens/ExportScreen.js +289 -0
  57. package/lib/module/screens/ExportScreen.js.map +1 -0
  58. package/lib/module/screens/GalleryScreen.js +505 -0
  59. package/lib/module/screens/GalleryScreen.js.map +1 -0
  60. package/lib/module/screens/PickScreen.js +1195 -0
  61. package/lib/module/screens/PickScreen.js.map +1 -0
  62. package/lib/module/types.js +2 -0
  63. package/lib/module/types.js.map +1 -0
  64. package/lib/typescript/src/components/VideoEditor.d.ts +13 -0
  65. package/lib/typescript/src/index.d.ts +2 -0
  66. package/lib/typescript/src/native/CameraView.d.ts +23 -0
  67. package/lib/typescript/src/native/FrameGrabber.d.ts +2 -0
  68. package/lib/typescript/src/native/MediaEditor.d.ts +3 -0
  69. package/lib/typescript/src/native/MediaLibrary.d.ts +16 -0
  70. package/lib/typescript/src/native/MediaPicker.d.ts +2 -0
  71. package/lib/typescript/src/native/MediaPlayer.d.ts +1 -0
  72. package/lib/typescript/src/native/VideoPreview.d.ts +19 -0
  73. package/lib/typescript/src/screens/CropScreen.d.ts +9 -0
  74. package/lib/typescript/src/screens/EditorScreen.d.ts +10 -0
  75. package/lib/typescript/src/screens/ExportScreen.d.ts +9 -0
  76. package/lib/typescript/src/screens/GalleryScreen.d.ts +8 -0
  77. package/lib/typescript/src/screens/PickScreen.d.ts +13 -0
  78. package/lib/typescript/src/types.d.ts +58 -0
  79. package/package.json +101 -0
  80. package/src/assets/frames/film_vintage.png +0 -0
  81. package/src/assets/frames/floral_gold.png +0 -0
  82. package/src/assets/frames/minimal_double.png +0 -0
  83. package/src/assets/frames/polaroid_white.png +0 -0
  84. package/src/assets/frames/watercolor_floral.png +0 -0
  85. package/src/components/VideoEditor.tsx +182 -0
  86. package/src/index.tsx +2 -0
  87. package/src/native/CameraView.tsx +95 -0
  88. package/src/native/FrameGrabber.ts +21 -0
  89. package/src/native/MediaEditor.ts +33 -0
  90. package/src/native/MediaLibrary.ts +69 -0
  91. package/src/native/MediaPicker.ts +17 -0
  92. package/src/native/MediaPlayer.ts +16 -0
  93. package/src/native/VideoPreview.tsx +20 -0
  94. package/src/screens/CropScreen.tsx +968 -0
  95. package/src/screens/EditorScreen.tsx +4517 -0
  96. package/src/screens/ExportScreen.tsx +282 -0
  97. package/src/screens/GalleryScreen.tsx +412 -0
  98. package/src/screens/PickScreen.tsx +1094 -0
  99. package/src/types.ts +58 -0
@@ -0,0 +1,305 @@
1
+ package com.technotoil.image_videoeditor
2
+
3
+ import android.Manifest
4
+ import android.content.ContentUris
5
+ import android.content.pm.PackageManager
6
+ import android.graphics.Bitmap
7
+ import android.media.ThumbnailUtils
8
+ import android.net.Uri
9
+ import android.os.Build
10
+ import android.provider.MediaStore
11
+ import android.util.Size
12
+ import androidx.core.app.ActivityCompat
13
+ import androidx.core.content.ContextCompat
14
+ import com.facebook.react.bridge.Arguments
15
+ import com.facebook.react.bridge.Promise
16
+ import com.facebook.react.bridge.ReactApplicationContext
17
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
18
+ import com.facebook.react.bridge.ReactMethod
19
+ import java.io.File
20
+ import java.io.FileOutputStream
21
+
22
+ class MediaLibraryModule(private val reactContext: ReactApplicationContext) :
23
+ ReactContextBaseJavaModule(reactContext) {
24
+
25
+ override fun getName(): String = "RNMediaLibrary"
26
+
27
+ @ReactMethod
28
+ fun requestAccess(promise: Promise) {
29
+ val activity = getCurrentActivity()
30
+ if (activity == null) {
31
+ promise.resolve(false)
32
+ return
33
+ }
34
+ val permissions = if (android.os.Build.VERSION.SDK_INT >= 33) {
35
+ arrayOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO)
36
+ } else {
37
+ arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
38
+ }
39
+ val allGranted = permissions.all {
40
+ ContextCompat.checkSelfPermission(reactContext, it) == PackageManager.PERMISSION_GRANTED
41
+ }
42
+ if (allGranted) {
43
+ promise.resolve(true)
44
+ return
45
+ }
46
+ if (activity is com.facebook.react.modules.core.PermissionAwareActivity) {
47
+ activity.requestPermissions(permissions, 4422,
48
+ com.facebook.react.modules.core.PermissionListener { _: Int, _: Array<String>, grantResults: IntArray ->
49
+ val granted = grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }
50
+ promise.resolve(granted)
51
+ granted
52
+ }
53
+ )
54
+ } else {
55
+ ActivityCompat.requestPermissions(activity, permissions, 4422)
56
+ promise.resolve(false)
57
+ }
58
+ }
59
+
60
+ @ReactMethod
61
+ fun listAlbums(promise: Promise) {
62
+ try {
63
+ val albums = Arguments.createArray()
64
+ val projection = arrayOf(
65
+ MediaStore.Files.FileColumns.BUCKET_ID,
66
+ MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME
67
+ )
68
+
69
+ val cursor = reactContext.contentResolver.query(
70
+ MediaStore.Files.getContentUri("external"),
71
+ projection,
72
+ null,
73
+ null,
74
+ "${MediaStore.Files.FileColumns.DATE_ADDED} DESC"
75
+ )
76
+
77
+ val seenIds = mutableSetOf<String>()
78
+ if (cursor != null) {
79
+ val idCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_ID)
80
+ val nameCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME)
81
+
82
+ while (cursor.moveToNext()) {
83
+ val id = cursor.getString(idCol)
84
+ val name = cursor.getString(nameCol) ?: "Unknown"
85
+ if (!seenIds.contains(id)) {
86
+ val map = Arguments.createMap()
87
+ map.putString("id", id)
88
+ map.putString("title", name)
89
+ albums.pushMap(map)
90
+ seenIds.add(id)
91
+ }
92
+ }
93
+ cursor.close()
94
+ }
95
+ promise.resolve(albums)
96
+ } catch (e: Exception) {
97
+ promise.reject("albums_failed", e.message, e)
98
+ }
99
+ }
100
+
101
+ @ReactMethod
102
+ fun listMedia(options: com.facebook.react.bridge.ReadableMap, promise: Promise) {
103
+ try {
104
+ val limit = if (options.hasKey("limit")) options.getInt("limit") else 200
105
+ val offset = if (options.hasKey("offset")) options.getInt("offset") else 0
106
+ val type = if (options.hasKey("type")) options.getString("type") else "all"
107
+ val albumId = if (options.hasKey("albumId")) options.getString("albumId") else null
108
+
109
+ val resolver = reactContext.contentResolver
110
+ val items = Arguments.createArray()
111
+
112
+ val projection = arrayOf(
113
+ MediaStore.Files.FileColumns._ID,
114
+ MediaStore.Files.FileColumns.MEDIA_TYPE,
115
+ MediaStore.Files.FileColumns.DURATION,
116
+ )
117
+
118
+ var selection = when (type) {
119
+ "image" -> "${MediaStore.Files.FileColumns.MEDIA_TYPE}=${MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE}"
120
+ "video" -> "${MediaStore.Files.FileColumns.MEDIA_TYPE}=${MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO}"
121
+ else -> "${MediaStore.Files.FileColumns.MEDIA_TYPE} IN (${MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE}, ${MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO})"
122
+ }
123
+
124
+ if (albumId != null) {
125
+ selection += " AND ${MediaStore.Files.FileColumns.BUCKET_ID} = '$albumId'"
126
+ }
127
+
128
+ val sortOrder = "${MediaStore.Files.FileColumns.DATE_ADDED} DESC"
129
+
130
+ val queryUri = MediaStore.Files.getContentUri("external")
131
+ val cursor = resolver.query(queryUri, projection, selection, null, sortOrder)
132
+
133
+ if (cursor != null) {
134
+ val idCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
135
+ val typeCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
136
+ val durationCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DURATION)
137
+
138
+ var skipped = 0
139
+ while (cursor.moveToNext() && items.size() < limit) {
140
+ if (skipped < offset) {
141
+ skipped++
142
+ continue
143
+ }
144
+ val id = cursor.getLong(idCol)
145
+ val mediaType = cursor.getInt(typeCol)
146
+ val duration = cursor.getLong(durationCol)
147
+
148
+ val uri = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO) {
149
+ ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id)
150
+ } else {
151
+ ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
152
+ }
153
+
154
+ val thumbUri = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO) {
155
+ createVideoThumbnail(uri, id)
156
+ } else {
157
+ uri.toString()
158
+ }
159
+
160
+ val map = Arguments.createMap()
161
+ map.putString("id", id.toString())
162
+ map.putString("uri", uri.toString())
163
+ map.putString("thumbnailUri", thumbUri ?: "")
164
+ map.putString("type", if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO) "video" else "image")
165
+ if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO) {
166
+ map.putDouble("durationMs", duration.toDouble())
167
+ }
168
+
169
+ items.pushMap(map)
170
+ }
171
+ cursor.close()
172
+ }
173
+
174
+ promise.resolve(items)
175
+ } catch (e: Exception) {
176
+ promise.reject("list_failed", e.message, e)
177
+ }
178
+ }
179
+
180
+ private fun createVideoThumbnail(uri: Uri, id: Long): String? {
181
+ return try {
182
+ val bitmap: Bitmap? = if (Build.VERSION.SDK_INT >= 29) {
183
+ reactContext.contentResolver.loadThumbnail(uri, Size(240, 240), null)
184
+ } else {
185
+ MediaStore.Video.Thumbnails.getThumbnail(
186
+ reactContext.contentResolver,
187
+ id,
188
+ MediaStore.Video.Thumbnails.MINI_KIND,
189
+ null
190
+ )
191
+ }
192
+ if (bitmap == null) return null
193
+ val file = File.createTempFile("vthumb_", ".jpg", reactContext.cacheDir)
194
+ FileOutputStream(file).use { out ->
195
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out)
196
+ }
197
+ Uri.fromFile(file).toString()
198
+ } catch (_: Exception) {
199
+ null
200
+ }
201
+ }
202
+
203
+ @ReactMethod
204
+ fun exportAsset(localId: String, promise: Promise) {
205
+ try {
206
+ val id = localId.toLong()
207
+ val resolver = reactContext.contentResolver
208
+ var uri: Uri? = null
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
+
220
+ // If not a video, check if it's an image
221
+ if (uri == null) {
222
+ val imageUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
223
+ cursor = resolver.query(imageUri, arrayOf(MediaStore.Images.Media._ID), null, null, null)
224
+ if (cursor != null) {
225
+ if (cursor.moveToFirst()) {
226
+ uri = imageUri
227
+ }
228
+ cursor.close()
229
+ }
230
+ }
231
+
232
+ if (uri != null) {
233
+ val cacheFile = MediaFileUtils.copyToCache(reactContext, uri, "export")
234
+ promise.resolve(Uri.fromFile(cacheFile).toString())
235
+ } else {
236
+ promise.reject("not_found", "Asset not found in Video or Image MediaStore")
237
+ }
238
+ } catch (e: Exception) {
239
+ promise.reject("export_failed", e.message, e)
240
+ }
241
+ }
242
+
243
+ @ReactMethod
244
+ fun saveToGallery(uriString: String, type: String, promise: Promise) {
245
+ try {
246
+ val uri = Uri.parse(uriString)
247
+ val resolver = reactContext.contentResolver
248
+ val currentTime = System.currentTimeMillis()
249
+ val extension = if (type == "video") ".mp4" else ".jpg"
250
+ val mimeType = if (type == "video") "video/mp4" else "image/jpeg"
251
+ val displayName = "Edited_$currentTime$extension"
252
+
253
+ val values = android.content.ContentValues().apply {
254
+ put(MediaStore.Files.FileColumns.DISPLAY_NAME, displayName)
255
+ put(MediaStore.Files.FileColumns.DATE_ADDED, currentTime / 1000)
256
+ put(MediaStore.Files.FileColumns.DATE_TAKEN, currentTime)
257
+ put(MediaStore.Files.FileColumns.MIME_TYPE, mimeType)
258
+ if (Build.VERSION.SDK_INT >= 29) {
259
+ put(MediaStore.Files.FileColumns.IS_PENDING, 1)
260
+ val relativePath = if (type == "video") "Movies/VideoEditor" else "Pictures/VideoEditor"
261
+ put(MediaStore.Files.FileColumns.RELATIVE_PATH, relativePath)
262
+ }
263
+ }
264
+
265
+ val targetUri = if (type == "video") {
266
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI
267
+ } else {
268
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI
269
+ }
270
+
271
+ val insertedUri = resolver.insert(targetUri, values)
272
+ if (insertedUri == null) {
273
+ promise.reject("insert_failed", "Could not create gallery entry")
274
+ return
275
+ }
276
+
277
+ try {
278
+ resolver.openInputStream(uri).use { input ->
279
+ resolver.openOutputStream(insertedUri).use { output ->
280
+ if (input != null && output != null) {
281
+ input.copyTo(output)
282
+ } else {
283
+ promise.reject("io_failed", "Could not copy file to gallery")
284
+ return
285
+ }
286
+ }
287
+ }
288
+
289
+ if (Build.VERSION.SDK_INT >= 29) {
290
+ values.clear()
291
+ values.put(MediaStore.Files.FileColumns.IS_PENDING, 0)
292
+ resolver.update(insertedUri, values, null, null)
293
+ }
294
+
295
+ promise.resolve(insertedUri.toString())
296
+ } catch (e: Exception) {
297
+ // Clean up the failed insert
298
+ resolver.delete(insertedUri, null, null)
299
+ throw e
300
+ }
301
+ } catch (e: Exception) {
302
+ promise.reject("save_failed", e.message, e)
303
+ }
304
+ }
305
+ }
@@ -0,0 +1,26 @@
1
+ package com.technotoil.image_videoeditor
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class MediaPackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
10
+ return listOf(
11
+ MediaPickerModule(reactContext),
12
+ MediaEditorModule(reactContext),
13
+ MediaLibraryModule(reactContext),
14
+ MediaPlayerModule(reactContext),
15
+ FrameGrabberModule(reactContext),
16
+ RNCameraModule(reactContext)
17
+ )
18
+ }
19
+
20
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
21
+ return listOf(
22
+ RNVideoPreviewManager(reactContext),
23
+ RNCameraViewManager(reactContext)
24
+ )
25
+ }
26
+ }
@@ -0,0 +1,111 @@
1
+ package com.technotoil.image_videoeditor
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.media.MediaMetadataRetriever
6
+ import android.net.Uri
7
+ import com.facebook.react.bridge.ActivityEventListener
8
+ import com.facebook.react.bridge.Arguments
9
+ import com.facebook.react.bridge.Promise
10
+ import com.facebook.react.bridge.ReactApplicationContext
11
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
12
+ import com.facebook.react.bridge.ReactMethod
13
+ import com.facebook.react.bridge.ReadableArray
14
+ import com.facebook.react.bridge.WritableArray
15
+ import com.facebook.react.modules.core.DeviceEventManagerModule
16
+
17
+ class MediaPickerModule(private val reactContext: ReactApplicationContext) :
18
+ ReactContextBaseJavaModule(reactContext), ActivityEventListener {
19
+
20
+ companion object {
21
+ private const val PICK_MEDIA_REQUEST = 5011
22
+ }
23
+
24
+ private var pendingPromise: Promise? = null
25
+
26
+ init {
27
+ reactContext.addActivityEventListener(this)
28
+ }
29
+
30
+ override fun getName(): String = "RNMediaPicker"
31
+
32
+ @ReactMethod
33
+ fun pickMedia(promise: Promise) {
34
+ val activity = getCurrentActivity()
35
+ if (activity == null) {
36
+ promise.reject("no_activity", "Current activity is null")
37
+ return
38
+ }
39
+ if (pendingPromise != null) {
40
+ promise.reject("in_progress", "Another picker request is in progress")
41
+ return
42
+ }
43
+ pendingPromise = promise
44
+
45
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
46
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
47
+ intent.type = "*/*"
48
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
49
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
50
+
51
+ activity.startActivityForResult(intent, PICK_MEDIA_REQUEST)
52
+ }
53
+
54
+ override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
55
+ if (requestCode != PICK_MEDIA_REQUEST) return
56
+
57
+ val promise = pendingPromise
58
+ pendingPromise = null
59
+
60
+ if (promise == null) return
61
+ if (resultCode != Activity.RESULT_OK || data == null) {
62
+ promise.resolve(Arguments.createArray())
63
+ return
64
+ }
65
+
66
+ val results: WritableArray = Arguments.createArray()
67
+ val resolver = reactApplicationContext.contentResolver
68
+
69
+ val uris: MutableList<Uri> = mutableListOf()
70
+ val clipData = data.clipData
71
+ if (clipData != null) {
72
+ for (i in 0 until clipData.itemCount) {
73
+ uris.add(clipData.getItemAt(i).uri)
74
+ }
75
+ } else {
76
+ data.data?.let { uris.add(it) }
77
+ }
78
+
79
+ for (uri in uris) {
80
+ val mime = resolver.getType(uri) ?: ""
81
+ val type = if (mime.startsWith("video")) "video" else "image"
82
+ val file = MediaFileUtils.copyToCache(reactApplicationContext, uri, "pick")
83
+
84
+ val map = Arguments.createMap()
85
+ map.putString("id", java.util.UUID.randomUUID().toString())
86
+ map.putString("uri", Uri.fromFile(file).toString())
87
+ map.putString("type", type)
88
+
89
+ if (type == "video") {
90
+ val retriever = MediaMetadataRetriever()
91
+ try {
92
+ retriever.setDataSource(reactApplicationContext, Uri.fromFile(file))
93
+ val durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
94
+ durationStr?.toLongOrNull()?.let { map.putDouble("durationMs", it.toDouble()) }
95
+ } catch (_: Exception) {
96
+ // ignore metadata errors
97
+ } finally {
98
+ retriever.release()
99
+ }
100
+ }
101
+
102
+ results.pushMap(map)
103
+ }
104
+
105
+ promise.resolve(results)
106
+ }
107
+
108
+ override fun onNewIntent(intent: Intent) {
109
+ // no-op
110
+ }
111
+ }
@@ -0,0 +1,34 @@
1
+ package com.technotoil.image_videoeditor
2
+
3
+ import android.content.Intent
4
+ import android.net.Uri
5
+ import com.facebook.react.bridge.Promise
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
8
+ import com.facebook.react.bridge.ReactMethod
9
+
10
+ class MediaPlayerModule(private val reactContext: ReactApplicationContext) :
11
+ ReactContextBaseJavaModule(reactContext) {
12
+
13
+ override fun getName(): String = "RNMediaPlayer"
14
+
15
+ @ReactMethod
16
+ fun playVideo(uriString: String, promise: Promise) {
17
+ try {
18
+ val uri = Uri.parse(uriString)
19
+ val intent = Intent(Intent.ACTION_VIEW).apply {
20
+ setDataAndType(uri, "video/*")
21
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
22
+ }
23
+ val activity = getCurrentActivity()
24
+ if (activity != null) {
25
+ activity.startActivity(intent)
26
+ promise.resolve(true)
27
+ } else {
28
+ promise.resolve(false)
29
+ }
30
+ } catch (e: Exception) {
31
+ promise.reject("play_failed", e.message, e)
32
+ }
33
+ }
34
+ }