@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.
- package/ImageVideoEditor.podspec +21 -0
- package/README.md +136 -0
- package/android/build.gradle +76 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +13 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/FrameGrabberModule.kt +67 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaEditorModule.kt +548 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaFileUtils.kt +29 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaLibraryModule.kt +305 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaPackage.kt +26 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaPickerModule.kt +111 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaPlayerModule.kt +34 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/RNCameraViewManager.kt +761 -0
- package/android/src/main/java/com/technotoil/image_videoeditor/RNVideoPreviewManager.kt +317 -0
- package/ios/PrivacyInfo.xcprivacy +38 -0
- package/ios/RNCameraViewManager.m +420 -0
- package/ios/RNFrameGrabber.m +61 -0
- package/ios/RNMediaEditor.m +905 -0
- package/ios/RNMediaLibrary.m +389 -0
- package/ios/RNMediaPicker.m +144 -0
- package/ios/RNMediaPlayer.m +73 -0
- package/ios/RNVideoPreviewManager.m +263 -0
- package/ios/frames/film_vintage.png +0 -0
- package/ios/frames/floral_gold.png +0 -0
- package/ios/frames/minimal_double.png +0 -0
- package/ios/frames/polaroid_white.png +0 -0
- package/ios/frames/watercolor_floral.png +0 -0
- package/lib/module/assets/frames/film_vintage.png +0 -0
- package/lib/module/assets/frames/floral_gold.png +0 -0
- package/lib/module/assets/frames/minimal_double.png +0 -0
- package/lib/module/assets/frames/polaroid_white.png +0 -0
- package/lib/module/assets/frames/watercolor_floral.png +0 -0
- package/lib/module/components/VideoEditor.js +156 -0
- package/lib/module/components/VideoEditor.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native/CameraView.js +104 -0
- package/lib/module/native/CameraView.js.map +1 -0
- package/lib/module/native/FrameGrabber.js +13 -0
- package/lib/module/native/FrameGrabber.js.map +1 -0
- package/lib/module/native/MediaEditor.js +19 -0
- package/lib/module/native/MediaEditor.js.map +1 -0
- package/lib/module/native/MediaLibrary.js +37 -0
- package/lib/module/native/MediaLibrary.js.map +1 -0
- package/lib/module/native/MediaPicker.js +13 -0
- package/lib/module/native/MediaPicker.js.map +1 -0
- package/lib/module/native/MediaPlayer.js +13 -0
- package/lib/module/native/MediaPlayer.js.map +1 -0
- package/lib/module/native/VideoPreview.js +12 -0
- package/lib/module/native/VideoPreview.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/screens/CropScreen.js +1211 -0
- package/lib/module/screens/CropScreen.js.map +1 -0
- package/lib/module/screens/EditorScreen.js +5752 -0
- package/lib/module/screens/EditorScreen.js.map +1 -0
- package/lib/module/screens/ExportScreen.js +289 -0
- package/lib/module/screens/ExportScreen.js.map +1 -0
- package/lib/module/screens/GalleryScreen.js +505 -0
- package/lib/module/screens/GalleryScreen.js.map +1 -0
- package/lib/module/screens/PickScreen.js +1195 -0
- package/lib/module/screens/PickScreen.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/src/components/VideoEditor.d.ts +13 -0
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/native/CameraView.d.ts +23 -0
- package/lib/typescript/src/native/FrameGrabber.d.ts +2 -0
- package/lib/typescript/src/native/MediaEditor.d.ts +3 -0
- package/lib/typescript/src/native/MediaLibrary.d.ts +16 -0
- package/lib/typescript/src/native/MediaPicker.d.ts +2 -0
- package/lib/typescript/src/native/MediaPlayer.d.ts +1 -0
- package/lib/typescript/src/native/VideoPreview.d.ts +19 -0
- package/lib/typescript/src/screens/CropScreen.d.ts +9 -0
- package/lib/typescript/src/screens/EditorScreen.d.ts +10 -0
- package/lib/typescript/src/screens/ExportScreen.d.ts +9 -0
- package/lib/typescript/src/screens/GalleryScreen.d.ts +8 -0
- package/lib/typescript/src/screens/PickScreen.d.ts +13 -0
- package/lib/typescript/src/types.d.ts +58 -0
- package/package.json +101 -0
- package/src/assets/frames/film_vintage.png +0 -0
- package/src/assets/frames/floral_gold.png +0 -0
- package/src/assets/frames/minimal_double.png +0 -0
- package/src/assets/frames/polaroid_white.png +0 -0
- package/src/assets/frames/watercolor_floral.png +0 -0
- package/src/components/VideoEditor.tsx +182 -0
- package/src/index.tsx +2 -0
- package/src/native/CameraView.tsx +95 -0
- package/src/native/FrameGrabber.ts +21 -0
- package/src/native/MediaEditor.ts +33 -0
- package/src/native/MediaLibrary.ts +69 -0
- package/src/native/MediaPicker.ts +17 -0
- package/src/native/MediaPlayer.ts +16 -0
- package/src/native/VideoPreview.tsx +20 -0
- package/src/screens/CropScreen.tsx +968 -0
- package/src/screens/EditorScreen.tsx +4517 -0
- package/src/screens/ExportScreen.tsx +282 -0
- package/src/screens/GalleryScreen.tsx +412 -0
- package/src/screens/PickScreen.tsx +1094 -0
- 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
|
+
}
|