@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,761 @@
1
+ package com.technotoil.image_videoeditor
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.graphics.ImageFormat
6
+ import android.graphics.SurfaceTexture
7
+ import android.hardware.camera2.*
8
+ import android.media.ImageReader
9
+ import android.media.MediaMetadataRetriever
10
+ import android.media.MediaRecorder
11
+ import android.net.Uri
12
+ import android.os.Handler
13
+ import android.os.HandlerThread
14
+ import android.util.Log
15
+ import android.view.Surface
16
+ import android.view.TextureView
17
+ import android.widget.FrameLayout
18
+ import com.facebook.react.bridge.*
19
+ import com.facebook.react.uimanager.SimpleViewManager
20
+ import com.facebook.react.uimanager.ThemedReactContext
21
+ import com.facebook.react.uimanager.UIManagerHelper
22
+ import com.facebook.react.uimanager.UIManagerModule
23
+ import com.facebook.react.uimanager.annotations.ReactProp
24
+ import java.io.File
25
+ import java.io.FileOutputStream
26
+
27
+ @SuppressLint("MissingPermission")
28
+ class RNCameraView(context: Context) : FrameLayout(context) {
29
+ private val textureView = TextureView(context)
30
+ var facing: String = "front"
31
+ set(value) {
32
+ if (field != value) {
33
+ field = value
34
+ if (isCameraOpen) {
35
+ reopenCamera()
36
+ }
37
+ }
38
+ }
39
+
40
+ private var cameraDevice: CameraDevice? = null
41
+ private var captureSession: CameraCaptureSession? = null
42
+ private var imageReader: ImageReader? = null
43
+ private var mediaRecorder: MediaRecorder? = null
44
+ private var isCameraOpen = false
45
+
46
+ private var backgroundThread: HandlerThread? = null
47
+ private var backgroundHandler: Handler? = null
48
+
49
+ private var photoPromise: Promise? = null
50
+ private var videoRecordPromise: Promise? = null
51
+ private var videoStopPromise: Promise? = null
52
+ private var currentVideoFile: File? = null
53
+ private var isRecording = false
54
+ private var flashMode = "off"
55
+ private var previewBuilder: CaptureRequest.Builder? = null
56
+
57
+ private var currentPreviewSize: android.util.Size = android.util.Size(1920, 1080)
58
+
59
+ private val textureListener = object : TextureView.SurfaceTextureListener {
60
+ override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {
61
+ openCamera()
62
+ }
63
+ override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) {}
64
+ override fun onSurfaceTextureDestroyed(texture: SurfaceTexture): Boolean {
65
+ closeCamera()
66
+ return true
67
+ }
68
+ override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {}
69
+ }
70
+
71
+ private val stateCallback = object : CameraDevice.StateCallback() {
72
+ override fun onOpened(camera: CameraDevice) {
73
+ cameraDevice = camera
74
+ isCameraOpen = true
75
+ createCameraPreview()
76
+ }
77
+
78
+ override fun onDisconnected(camera: CameraDevice) {
79
+ camera.close()
80
+ cameraDevice = null
81
+ isCameraOpen = false
82
+ }
83
+
84
+ override fun onError(camera: CameraDevice, error: Int) {
85
+ camera.close()
86
+ cameraDevice = null
87
+ isCameraOpen = false
88
+ Log.e("RNCameraView", "CameraDevice Error: $error")
89
+ }
90
+ }
91
+
92
+ private val imageAvailableListener = ImageReader.OnImageAvailableListener { reader ->
93
+ Log.w("RNCameraView", "imageAvailableListener triggered")
94
+ var image: android.media.Image? = null
95
+ try {
96
+ image = reader.acquireNextImage()
97
+ if (image == null) {
98
+ Log.e("RNCameraView", "No image acquired from reader")
99
+ return@OnImageAvailableListener
100
+ }
101
+ val buffer = image.planes[0].buffer
102
+ val bytes = ByteArray(buffer.remaining())
103
+ buffer.get(bytes)
104
+
105
+ // Save to cache dir
106
+ val file = File(context.cacheDir, "photo_${System.currentTimeMillis()}.jpg")
107
+ Log.w("RNCameraView", "Saving captured photo to path: ${file.absolutePath}")
108
+ FileOutputStream(file).use { it.write(bytes) }
109
+
110
+ val options = android.graphics.BitmapFactory.Options().apply { inJustDecodeBounds = true }
111
+ android.graphics.BitmapFactory.decodeFile(file.absolutePath, options)
112
+
113
+ Log.w("RNCameraView", "Decoded bounds: width=${options.outWidth}, height=${options.outHeight}")
114
+ val map = Arguments.createMap().apply {
115
+ putString("uri", Uri.fromFile(file).toString())
116
+ putInt("width", options.outWidth)
117
+ putInt("height", options.outHeight)
118
+ }
119
+
120
+ Log.w("RNCameraView", "Resolving photoPromise with URI: ${Uri.fromFile(file)}")
121
+ photoPromise?.resolve(map)
122
+ photoPromise = null
123
+ } catch (e: Exception) {
124
+ Log.e("RNCameraView", "Error in imageAvailableListener: ${e.message}")
125
+ photoPromise?.reject("save_error", e.message)
126
+ photoPromise = null
127
+ } finally {
128
+ image?.close()
129
+ }
130
+ }
131
+
132
+ private fun getOptimalPreviewSize(sizes: Array<android.util.Size>?): android.util.Size {
133
+ if (sizes.isNullOrEmpty()) return android.util.Size(1920, 1080)
134
+
135
+ val targetRatio = 16.0 / 9.0
136
+ val tolerance = 0.1
137
+
138
+ val matches = sizes.filter {
139
+ val ratio = it.width.toFloat() / it.height.toFloat()
140
+ Math.abs(ratio - targetRatio) < tolerance || Math.abs((1.0 / ratio) - targetRatio) < tolerance
141
+ }
142
+
143
+ if (matches.isNotEmpty()) {
144
+ return matches.minByOrNull {
145
+ Math.abs(it.width - 1920) + Math.abs(it.height - 1080)
146
+ } ?: matches[0]
147
+ }
148
+
149
+ return sizes.minByOrNull {
150
+ Math.abs(it.width - 1920) + Math.abs(it.height - 1080)
151
+ } ?: sizes[0]
152
+ }
153
+
154
+ private fun adjustAspectRatio(viewWidth: Int, viewHeight: Int) {
155
+ if (viewWidth == 0 || viewHeight == 0) return
156
+ val previewSize = currentPreviewSize
157
+
158
+ val previewAspect = previewSize.height.toFloat() / previewSize.width.toFloat()
159
+ val viewAspect = viewWidth.toFloat() / viewHeight.toFloat()
160
+
161
+ val matrix = android.graphics.Matrix()
162
+
163
+ var scaleX = 1f
164
+ var scaleY = 1f
165
+
166
+ if (viewAspect > previewAspect) {
167
+ scaleY = viewAspect / previewAspect
168
+ } else {
169
+ scaleX = previewAspect / viewAspect
170
+ }
171
+
172
+ matrix.setScale(scaleX, scaleY, viewWidth / 2f, viewHeight / 2f)
173
+
174
+ val reactContext = context as? com.facebook.react.bridge.ReactContext
175
+ if (reactContext != null) {
176
+ reactContext.runOnUiQueueThread {
177
+ textureView.setTransform(matrix)
178
+ }
179
+ } else {
180
+ textureView.setTransform(matrix)
181
+ }
182
+ }
183
+
184
+ init {
185
+ addView(textureView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
186
+ textureView.surfaceTextureListener = textureListener
187
+ textureView.addOnLayoutChangeListener { _, left, top, right, bottom, _, _, _, _ ->
188
+ adjustAspectRatio(right - left, bottom - top)
189
+ }
190
+ }
191
+
192
+ override fun onAttachedToWindow() {
193
+ super.onAttachedToWindow()
194
+ startBackgroundThread()
195
+ if (textureView.isAvailable) {
196
+ openCamera()
197
+ }
198
+ }
199
+
200
+ override fun onDetachedFromWindow() {
201
+ closeCamera()
202
+ stopBackgroundThread()
203
+ super.onDetachedFromWindow()
204
+ }
205
+
206
+ private fun startBackgroundThread() {
207
+ backgroundThread = HandlerThread("CameraBackground").also { it.start() }
208
+ backgroundHandler = Handler(backgroundThread!!.looper)
209
+ }
210
+
211
+ private fun stopBackgroundThread() {
212
+ backgroundThread?.quitSafely()
213
+ try {
214
+ backgroundThread?.join()
215
+ backgroundThread = null
216
+ backgroundHandler = null
217
+ } catch (e: InterruptedException) {
218
+ e.printStackTrace()
219
+ }
220
+ }
221
+
222
+ private fun openCamera() {
223
+ val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
224
+ try {
225
+ val cameraId = manager.cameraIdList.firstOrNull { id ->
226
+ val chars = manager.getCameraCharacteristics(id)
227
+ val facingChar = chars.get(CameraCharacteristics.LENS_FACING)
228
+ if (facing == "back") facingChar == CameraMetadata.LENS_FACING_BACK
229
+ else facingChar == CameraMetadata.LENS_FACING_FRONT
230
+ } ?: manager.cameraIdList.firstOrNull() ?: return
231
+
232
+ manager.openCamera(cameraId, stateCallback, backgroundHandler)
233
+ } catch (e: Exception) {
234
+ Log.e("RNCameraView", "openCamera failed: ${e.message}")
235
+ }
236
+ }
237
+
238
+ private fun closeCamera() {
239
+ try {
240
+ captureSession?.close()
241
+ captureSession = null
242
+ cameraDevice?.close()
243
+ cameraDevice = null
244
+ isCameraOpen = false
245
+ imageReader?.close()
246
+ imageReader = null
247
+
248
+ mediaRecorder?.let {
249
+ try {
250
+ it.reset()
251
+ } catch (e: Exception) {}
252
+ try {
253
+ it.release()
254
+ } catch (e: Exception) {}
255
+ }
256
+ mediaRecorder = null
257
+ isRecording = false
258
+ } catch (e: Exception) {
259
+ e.printStackTrace()
260
+ }
261
+ }
262
+
263
+ private fun reopenCamera() {
264
+ closeCamera()
265
+ openCamera()
266
+ }
267
+
268
+ private fun createCameraPreview() {
269
+ val device = cameraDevice ?: return
270
+ val texture = textureView.surfaceTexture ?: return
271
+ val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
272
+ try {
273
+ val chars = manager.getCameraCharacteristics(device.id)
274
+ val map = chars.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
275
+ val previewSize = getOptimalPreviewSize(map?.getOutputSizes(SurfaceTexture::class.java))
276
+ currentPreviewSize = previewSize
277
+ texture.setDefaultBufferSize(previewSize.width, previewSize.height)
278
+ val reactContext = context as? com.facebook.react.bridge.ReactContext
279
+ reactContext?.runOnUiQueueThread {
280
+ adjustAspectRatio(textureView.width, textureView.height)
281
+ }
282
+
283
+ val surface = Surface(texture)
284
+
285
+ // Image reader setup
286
+ val readerSizes = map?.getOutputSizes(ImageFormat.JPEG) ?: emptyArray()
287
+ val photoSize = readerSizes.firstOrNull() ?: android.util.Size(1920, 1080)
288
+ imageReader = ImageReader.newInstance(photoSize.width, photoSize.height, ImageFormat.JPEG, 2)
289
+ imageReader?.setOnImageAvailableListener(imageAvailableListener, backgroundHandler)
290
+
291
+ val builder = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
292
+ builder.addTarget(surface)
293
+ val hasFlash = chars.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false
294
+ if (hasFlash) {
295
+ if (flashMode == "on") {
296
+ builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH)
297
+ } else {
298
+ builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF)
299
+ }
300
+ }
301
+ previewBuilder = builder
302
+
303
+ device.createCaptureSession(listOf(surface, imageReader!!.surface), object : CameraCaptureSession.StateCallback() {
304
+ override fun onConfigured(session: CameraCaptureSession) {
305
+ if (cameraDevice == null) return
306
+ captureSession = session
307
+ builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)
308
+ try {
309
+ session.setRepeatingRequest(builder.build(), null, backgroundHandler)
310
+ } catch (e: Exception) {
311
+ e.printStackTrace()
312
+ }
313
+ }
314
+
315
+ override fun onConfigureFailed(session: CameraCaptureSession) {
316
+ Log.e("RNCameraView", "Preview session configuration failed")
317
+ }
318
+ }, backgroundHandler)
319
+ } catch (e: Exception) {
320
+ e.printStackTrace()
321
+ }
322
+ }
323
+
324
+ fun capturePhoto(promise: Promise) {
325
+ Log.w("RNCameraView", "capturePhoto called")
326
+ val device = cameraDevice ?: run {
327
+ Log.e("RNCameraView", "capturePhoto error: Camera not ready")
328
+ return promise.reject("camera_error", "Camera not ready")
329
+ }
330
+ val reader = imageReader ?: run {
331
+ Log.e("RNCameraView", "capturePhoto error: ImageReader not ready")
332
+ return promise.reject("camera_error", "ImageReader not ready")
333
+ }
334
+ val session = captureSession ?: run {
335
+ Log.e("RNCameraView", "capturePhoto error: Session not ready")
336
+ return promise.reject("camera_error", "Session not ready")
337
+ }
338
+
339
+ photoPromise = promise
340
+
341
+ try {
342
+ val captureBuilder = device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
343
+ captureBuilder.addTarget(reader.surface)
344
+ captureBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)
345
+
346
+ val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
347
+ val chars = manager.getCameraCharacteristics(device.id)
348
+ val sensorOrientation = chars.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 90
349
+ captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, sensorOrientation)
350
+
351
+ val hasFlash = chars.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false
352
+ if (hasFlash) {
353
+ if (flashMode == "on") {
354
+ captureBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH)
355
+ } else {
356
+ captureBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF)
357
+ }
358
+ }
359
+
360
+ // session.stopRepeating()
361
+ Log.w("RNCameraView", "Calling session.capture")
362
+ session.capture(captureBuilder.build(), object : CameraCaptureSession.CaptureCallback() {
363
+ override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {
364
+ Log.w("RNCameraView", "onCaptureStarted: timestamp=$timestamp, frameNumber=$frameNumber")
365
+ }
366
+
367
+ override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) {
368
+ Log.e("RNCameraView", "onCaptureFailed: reason=${failure.reason}, wasImageCaptured=${failure.wasImageCaptured()}")
369
+ photoPromise?.reject("capture_failed", "Capture failed: reason=${failure.reason}")
370
+ photoPromise = null
371
+ }
372
+
373
+ override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
374
+ Log.w("RNCameraView", "onCaptureCompleted triggered")
375
+ }
376
+ }, backgroundHandler)
377
+ } catch (e: Exception) {
378
+ Log.e("RNCameraView", "capturePhoto exception: ${e.message}")
379
+ promise.reject("capture_error", e.message)
380
+ photoPromise = null
381
+ }
382
+ }
383
+
384
+ fun startRecording(promise: Promise) {
385
+ if (isRecording) {
386
+ return promise.reject("already_recording", "Camera is already recording video.")
387
+ }
388
+ val recordAudioPermission = androidx.core.content.ContextCompat.checkSelfPermission(
389
+ context,
390
+ android.Manifest.permission.RECORD_AUDIO
391
+ )
392
+ if (recordAudioPermission != android.content.pm.PackageManager.PERMISSION_GRANTED) {
393
+ return promise.reject("permission_denied", "Microphone permission is not granted.")
394
+ }
395
+ val cameraPermission = androidx.core.content.ContextCompat.checkSelfPermission(
396
+ context,
397
+ android.Manifest.permission.CAMERA
398
+ )
399
+ if (cameraPermission != android.content.pm.PackageManager.PERMISSION_GRANTED) {
400
+ return promise.reject("permission_denied", "Camera permission is not granted.")
401
+ }
402
+ val device = cameraDevice ?: return promise.reject("camera_error", "Camera not ready")
403
+ val texture = textureView.surfaceTexture ?: return promise.reject("camera_error", "Texture not ready")
404
+
405
+ videoRecordPromise = promise
406
+ currentVideoFile = File(context.cacheDir, "video_${System.currentTimeMillis()}.mp4")
407
+
408
+ try {
409
+ closeCamera() // Close standard preview first
410
+ openCamera() // Wait for camera to open and prepare recorder
411
+
412
+ // Wait for cameraDevice is ready to record
413
+ backgroundHandler?.post {
414
+ while (cameraDevice == null) {
415
+ Thread.sleep(50)
416
+ }
417
+
418
+ val recordDevice = cameraDevice!!
419
+ val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
420
+ val chars = manager.getCameraCharacteristics(recordDevice.id)
421
+ val sensorOrientation = chars.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 90
422
+
423
+ mediaRecorder = MediaRecorder(context).apply {
424
+ setAudioSource(MediaRecorder.AudioSource.MIC)
425
+ setVideoSource(MediaRecorder.VideoSource.SURFACE)
426
+ setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
427
+ setOutputFile(currentVideoFile!!.absolutePath)
428
+ setVideoEncodingBitRate(10000000)
429
+ setVideoFrameRate(30)
430
+ setVideoSize(1280, 720)
431
+ setVideoEncoder(MediaRecorder.VideoEncoder.H264)
432
+ setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
433
+
434
+ // Front camera video needs correct rotation
435
+ setOrientationHint(sensorOrientation)
436
+ prepare()
437
+ }
438
+
439
+ val map = chars.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
440
+ val previewSize = getOptimalPreviewSize(map?.getOutputSizes(SurfaceTexture::class.java))
441
+ currentPreviewSize = previewSize
442
+ texture.setDefaultBufferSize(previewSize.width, previewSize.height)
443
+ val reactContext = context as? com.facebook.react.bridge.ReactContext
444
+ reactContext?.runOnUiQueueThread {
445
+ adjustAspectRatio(textureView.width, textureView.height)
446
+ }
447
+
448
+ val previewSurface = Surface(texture)
449
+ val recorderSurface = mediaRecorder!!.surface
450
+
451
+ val recordBuilder = recordDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD)
452
+ recordBuilder.addTarget(previewSurface)
453
+ recordBuilder.addTarget(recorderSurface)
454
+
455
+ val hasFlash = chars.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false
456
+ if (hasFlash) {
457
+ if (flashMode == "on") {
458
+ recordBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH)
459
+ } else {
460
+ recordBuilder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF)
461
+ }
462
+ }
463
+
464
+ recordDevice.createCaptureSession(listOf(previewSurface, recorderSurface), object : CameraCaptureSession.StateCallback() {
465
+ override fun onConfigured(session: CameraCaptureSession) {
466
+ captureSession = session
467
+ recordBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)
468
+ try {
469
+ session.setRepeatingRequest(recordBuilder.build(), null, backgroundHandler)
470
+ mediaRecorder!!.start()
471
+ isRecording = true
472
+ videoRecordPromise?.resolve(null)
473
+ videoRecordPromise = null
474
+ } catch (e: Exception) {
475
+ videoRecordPromise?.reject("record_error", e.message)
476
+ videoRecordPromise = null
477
+ }
478
+ }
479
+
480
+ override fun onConfigureFailed(session: CameraCaptureSession) {
481
+ videoRecordPromise?.reject("record_error", "Video Session configure failed")
482
+ videoRecordPromise = null
483
+ }
484
+ }, backgroundHandler)
485
+ }
486
+ } catch (e: Exception) {
487
+ promise.reject("record_error", e.message)
488
+ videoRecordPromise = null
489
+ }
490
+ }
491
+
492
+ fun stopRecording(promise: Promise) {
493
+ if (!isRecording || mediaRecorder == null) {
494
+ return promise.reject("not_recording", "Camera is not recording.")
495
+ }
496
+
497
+ videoStopPromise = promise
498
+
499
+ backgroundHandler?.post {
500
+ try {
501
+ mediaRecorder!!.stop()
502
+ mediaRecorder!!.reset()
503
+ mediaRecorder = null
504
+ isRecording = false
505
+
506
+ val file = currentVideoFile ?: return@post promise.reject("error", "Recorded video file is missing")
507
+
508
+ val retriever = MediaMetadataRetriever()
509
+ retriever.setDataSource(file.absolutePath)
510
+ val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() ?: 0L
511
+ val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() ?: 1280
512
+ val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: 720
513
+ retriever.release()
514
+
515
+ val map = Arguments.createMap().apply {
516
+ putString("uri", Uri.fromFile(file).toString())
517
+ putDouble("durationMs", duration.toDouble())
518
+ putInt("width", width)
519
+ putInt("height", height)
520
+ }
521
+
522
+ // Reopen normal camera preview
523
+ reopenCamera()
524
+
525
+ videoStopPromise?.resolve(map)
526
+ videoStopPromise = null
527
+ } catch (e: Exception) {
528
+ videoStopPromise?.reject("stop_error", e.message)
529
+ videoStopPromise = null
530
+ reopenCamera()
531
+ }
532
+ }
533
+ }
534
+
535
+ class CameraEvent(surfaceId: Int, viewId: Int, private val name: String, private val data: WritableMap?) :
536
+ com.facebook.react.uimanager.events.Event<CameraEvent>(surfaceId, viewId) {
537
+
538
+ override fun getEventName(): String = name
539
+ override fun getEventData(): WritableMap? = data
540
+ }
541
+
542
+ private fun emitEvent(eventName: String, eventData: WritableMap?) {
543
+ Log.w("RNCameraView", "emitEvent called: eventName=$eventName, eventData=$eventData")
544
+ val reactContext = context as? ReactContext ?: return
545
+ try {
546
+ val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
547
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
548
+ if (dispatcher != null) {
549
+ Log.w("RNCameraView", "emitEvent: using dispatcher")
550
+ dispatcher.dispatchEvent(CameraEvent(surfaceId, id, eventName, eventData))
551
+ } else {
552
+ Log.w("RNCameraView", "emitEvent: dispatcher is null, using RCTEventEmitter")
553
+ reactContext.getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
554
+ ?.receiveEvent(id, eventName, eventData)
555
+ }
556
+ } catch (e: Exception) {
557
+ Log.w("RNCameraView", "emitEvent: first attempt failed, trying fallback: ${e.message}")
558
+ try {
559
+ reactContext.getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
560
+ ?.receiveEvent(id, eventName, eventData)
561
+ } catch (e2: Exception) {
562
+ Log.e("RNCameraView", "emitEvent error: ${e2.message}")
563
+ e2.printStackTrace()
564
+ }
565
+ }
566
+ }
567
+
568
+ class EventPromise(
569
+ private val eventName: String,
570
+ private val emit: (String, WritableMap?) -> Unit
571
+ ) : Promise {
572
+ override fun resolve(value: Any?) {
573
+ val map = value as? WritableMap
574
+ emit(eventName, map)
575
+ }
576
+
577
+ override fun reject(code: String?, message: String?) {
578
+ val map = Arguments.createMap().apply {
579
+ putString("error", message ?: "Unknown error")
580
+ }
581
+ emit(eventName, map)
582
+ }
583
+
584
+ override fun reject(code: String?, throwable: Throwable?) {
585
+ reject(code, throwable?.message)
586
+ }
587
+
588
+ override fun reject(code: String?, message: String?, throwable: Throwable?) {
589
+ reject(code, message)
590
+ }
591
+
592
+ override fun reject(throwable: Throwable) {
593
+ reject("error", throwable.message)
594
+ }
595
+
596
+ override fun reject(throwable: Throwable, userInfo: WritableMap) {
597
+ reject("error", throwable.message)
598
+ }
599
+
600
+ override fun reject(code: String?, userInfo: WritableMap) {
601
+ reject(code, "error")
602
+ }
603
+
604
+ override fun reject(code: String?, throwable: Throwable?, userInfo: WritableMap) {
605
+ reject(code, throwable?.message)
606
+ }
607
+
608
+ override fun reject(code: String?, message: String?, userInfo: WritableMap) {
609
+ reject(code, message)
610
+ }
611
+
612
+ override fun reject(code: String?, message: String?, throwable: Throwable?, userInfo: WritableMap?) {
613
+ reject(code, message)
614
+ }
615
+
616
+ override fun reject(message: String) {
617
+ reject("error", message)
618
+ }
619
+ }
620
+
621
+ fun setPhotoTrigger(trigger: String?) {
622
+ Log.w("RNCameraView", "setPhotoTrigger called with: $trigger")
623
+ if (trigger.isNullOrEmpty()) return
624
+ val promise = EventPromise("topPhotoCaptured", ::emitEvent)
625
+ capturePhoto(promise)
626
+ }
627
+
628
+ fun setRecordTrigger(trigger: String?) {
629
+ if (trigger == "start") {
630
+ val startPromise = EventPromise("topRecordStarted", ::emitEvent)
631
+ startRecording(startPromise)
632
+ } else if (trigger == "stop") {
633
+ val stopPromise = EventPromise("topRecordStopped", ::emitEvent)
634
+ stopRecording(stopPromise)
635
+ }
636
+ }
637
+
638
+ fun setFlashMode(flash: String?) {
639
+ val newFlash = flash ?: "off"
640
+ if (flashMode != newFlash) {
641
+ flashMode = newFlash
642
+ applyFlashToPreview()
643
+ }
644
+ }
645
+
646
+ private fun applyFlashToPreview() {
647
+ val session = captureSession ?: return
648
+ val builder = previewBuilder ?: return
649
+ val device = cameraDevice ?: return
650
+ try {
651
+ val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
652
+ val chars = manager.getCameraCharacteristics(device.id)
653
+ val hasFlash = chars.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false
654
+ if (hasFlash) {
655
+ if (flashMode == "on") {
656
+ builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH)
657
+ } else {
658
+ builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF)
659
+ }
660
+ session.setRepeatingRequest(builder.build(), null, backgroundHandler)
661
+ }
662
+ } catch (e: Exception) {
663
+ e.printStackTrace()
664
+ }
665
+ }
666
+ }
667
+
668
+ class RNCameraViewManager(private val reactContext: ReactApplicationContext) :
669
+ SimpleViewManager<RNCameraView>() {
670
+
671
+ override fun getName(): String = "RNCameraView"
672
+
673
+ override fun createViewInstance(reactContext: ThemedReactContext): RNCameraView {
674
+ return RNCameraView(reactContext)
675
+ }
676
+
677
+ @ReactProp(name = "facing")
678
+ fun setFacing(view: RNCameraView, facing: String?) {
679
+ view.facing = facing ?: "front"
680
+ }
681
+
682
+ @ReactProp(name = "flashMode")
683
+ fun setFlashMode(view: RNCameraView, flashMode: String?) {
684
+ view.setFlashMode(flashMode)
685
+ }
686
+
687
+ @ReactProp(name = "photoTrigger")
688
+ fun setPhotoTrigger(view: RNCameraView, photoTrigger: String?) {
689
+ view.setPhotoTrigger(photoTrigger)
690
+ }
691
+
692
+ @ReactProp(name = "recordTrigger")
693
+ fun setRecordTrigger(view: RNCameraView, recordTrigger: String?) {
694
+ view.setRecordTrigger(recordTrigger)
695
+ }
696
+
697
+ override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> {
698
+ return com.facebook.react.common.MapBuilder.builder<String, Any>()
699
+ .put("topPhotoCaptured", com.facebook.react.common.MapBuilder.of("registrationName", "onPhotoCaptured"))
700
+ .put("topRecordStarted", com.facebook.react.common.MapBuilder.of("registrationName", "onRecordStarted"))
701
+ .put("topRecordStopped", com.facebook.react.common.MapBuilder.of("registrationName", "onRecordStopped"))
702
+ .build()
703
+ }
704
+ }
705
+
706
+ class RNCameraModule(private val reactContext: ReactApplicationContext) :
707
+ ReactContextBaseJavaModule(reactContext) {
708
+
709
+ override fun getName(): String = "RNCameraModule"
710
+
711
+ @ReactMethod
712
+ fun capturePhoto(reactTag: Int, promise: Promise) {
713
+ val uiManager = reactContext.getNativeModule(UIManagerModule::class.java)
714
+ reactContext.runOnUiQueueThread {
715
+ try {
716
+ val view = uiManager?.resolveView(reactTag) as? RNCameraView
717
+ if (view != null) {
718
+ view.capturePhoto(promise)
719
+ } else {
720
+ promise.reject("error", "Camera view not found")
721
+ }
722
+ } catch (e: Exception) {
723
+ promise.reject("error", e.message)
724
+ }
725
+ }
726
+ }
727
+
728
+ @ReactMethod
729
+ fun startRecording(reactTag: Int, promise: Promise) {
730
+ val uiManager = reactContext.getNativeModule(UIManagerModule::class.java)
731
+ reactContext.runOnUiQueueThread {
732
+ try {
733
+ val view = uiManager?.resolveView(reactTag) as? RNCameraView
734
+ if (view != null) {
735
+ view.startRecording(promise)
736
+ } else {
737
+ promise.reject("error", "Camera view not found")
738
+ }
739
+ } catch (e: Exception) {
740
+ promise.reject("error", e.message)
741
+ }
742
+ }
743
+ }
744
+
745
+ @ReactMethod
746
+ fun stopRecording(reactTag: Int, promise: Promise) {
747
+ val uiManager = reactContext.getNativeModule(UIManagerModule::class.java)
748
+ reactContext.runOnUiQueueThread {
749
+ try {
750
+ val view = uiManager?.resolveView(reactTag) as? RNCameraView
751
+ if (view != null) {
752
+ view.stopRecording(promise)
753
+ } else {
754
+ promise.reject("error", "Camera view not found")
755
+ }
756
+ } catch (e: Exception) {
757
+ promise.reject("error", e.message)
758
+ }
759
+ }
760
+ }
761
+ }