@succinctlabs/react-native-zcam1 0.3.0 → 0.4.0-alpha.1
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/Zcam1Sdk.podspec +2 -2
- package/android/CMakeLists.txt +114 -0
- package/android/build.gradle +213 -0
- package/android/cpp-adapter-proving.cpp +35 -0
- package/android/cpp-adapter.cpp +35 -0
- package/android/src/main/AndroidManifest.xml +5 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1CaptureModule.kt +156 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1CapturePackage.kt +38 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1ProvingModule.kt +43 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1ProvingPackage.kt +34 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1SdkModule.kt +43 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/Zcam1SdkPackage.kt +34 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/CameraUtils.kt +80 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/Zcam1CameraService.kt +588 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/Zcam1CameraView.kt +107 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/Zcam1CameraViewManager.kt +33 -0
- package/android/src/main/java/com/succinctlabs/zcam1sdk/camera/Zcam1OrientationManager.kt +73 -0
- package/cpp/generated/zcam1_c2pa_utils.cpp +170 -365
- package/cpp/generated/zcam1_c2pa_utils.hpp +0 -4
- package/cpp/generated/zcam1_certs_utils.cpp +121 -250
- package/cpp/generated/zcam1_common.cpp +1871 -0
- package/cpp/generated/zcam1_common.hpp +52 -0
- package/cpp/generated/zcam1_verify_utils.cpp +138 -265
- package/cpp/generated/zcam1_verify_utils.hpp +2 -2
- package/cpp/proving/generated/zcam1_common.cpp +1871 -0
- package/cpp/proving/generated/zcam1_common.hpp +52 -0
- package/cpp/proving/generated/zcam1_proving_utils.cpp +355 -417
- package/cpp/proving/generated/zcam1_proving_utils.hpp +13 -17
- package/cpp/proving/zcam1-proving.cpp +2 -0
- package/cpp/zcam1-sdk.cpp +2 -0
- package/lib/module/bindings.js +4 -0
- package/lib/module/bindings.js.map +1 -1
- package/lib/module/camera.js +71 -13
- package/lib/module/camera.js.map +1 -1
- package/lib/module/capture.js +115 -38
- package/lib/module/capture.js.map +1 -1
- package/lib/module/common.js +18 -2
- package/lib/module/common.js.map +1 -1
- package/lib/module/generated/zcam1_c2pa_utils-ffi.js +4 -0
- package/lib/module/generated/zcam1_c2pa_utils-ffi.js.map +1 -1
- package/lib/module/generated/zcam1_c2pa_utils.js +117 -9
- package/lib/module/generated/zcam1_c2pa_utils.js.map +1 -1
- package/lib/module/generated/zcam1_certs_utils-ffi.js +4 -0
- package/lib/module/generated/zcam1_certs_utils-ffi.js.map +1 -1
- package/lib/module/generated/zcam1_certs_utils.js +6 -2
- package/lib/module/generated/zcam1_certs_utils.js.map +1 -1
- package/lib/module/generated/zcam1_common-ffi.js +47 -0
- package/lib/module/generated/zcam1_common-ffi.js.map +1 -0
- package/lib/module/generated/zcam1_common.js +60 -0
- package/lib/module/generated/zcam1_common.js.map +1 -0
- package/lib/module/generated/zcam1_proving_utils-ffi.js +4 -0
- package/lib/module/generated/zcam1_proving_utils-ffi.js.map +1 -1
- package/lib/module/generated/zcam1_proving_utils.js +53 -46
- package/lib/module/generated/zcam1_proving_utils.js.map +1 -1
- package/lib/module/generated/zcam1_verify_utils-ffi.js +4 -0
- package/lib/module/generated/zcam1_verify_utils-ffi.js.map +1 -1
- package/lib/module/generated/zcam1_verify_utils.js +70 -22
- package/lib/module/generated/zcam1_verify_utils.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/proving/NativeZcam1Proving.js +1 -1
- package/lib/module/proving/index.js +1 -1
- package/lib/module/proving/index.js.map +1 -1
- package/lib/module/proving/prove.js +14 -8
- package/lib/module/proving/prove.js.map +1 -1
- package/lib/module/utils.js +19 -14
- package/lib/module/utils.js.map +1 -1
- package/lib/module/verify.js +14 -22
- package/lib/module/verify.js.map +1 -1
- package/lib/typescript/src/bindings.d.ts +3 -0
- package/lib/typescript/src/bindings.d.ts.map +1 -1
- package/lib/typescript/src/camera.d.ts +15 -0
- package/lib/typescript/src/camera.d.ts.map +1 -1
- package/lib/typescript/src/capture.d.ts +40 -1
- package/lib/typescript/src/capture.d.ts.map +1 -1
- package/lib/typescript/src/common.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_c2pa_utils-ffi.d.ts +37 -46
- package/lib/typescript/src/generated/zcam1_c2pa_utils-ffi.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_c2pa_utils.d.ts +110 -8
- package/lib/typescript/src/generated/zcam1_c2pa_utils.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_certs_utils-ffi.d.ts +27 -32
- package/lib/typescript/src/generated/zcam1_certs_utils-ffi.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_certs_utils.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_common-ffi.d.ts +77 -0
- package/lib/typescript/src/generated/zcam1_common-ffi.d.ts.map +1 -0
- package/lib/typescript/src/generated/zcam1_common.d.ts +17 -0
- package/lib/typescript/src/generated/zcam1_common.d.ts.map +1 -0
- package/lib/typescript/src/generated/zcam1_proving_utils-ffi.d.ts +44 -51
- package/lib/typescript/src/generated/zcam1_proving_utils-ffi.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_proving_utils.d.ts +26 -26
- package/lib/typescript/src/generated/zcam1_proving_utils.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_verify_utils-ffi.d.ts +29 -34
- package/lib/typescript/src/generated/zcam1_verify_utils-ffi.d.ts.map +1 -1
- package/lib/typescript/src/generated/zcam1_verify_utils.d.ts +94 -14
- package/lib/typescript/src/generated/zcam1_verify_utils.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/proving/NativeZcam1Proving.d.ts +1 -1
- package/lib/typescript/src/proving/index.d.ts +1 -1
- package/lib/typescript/src/proving/index.d.ts.map +1 -1
- package/lib/typescript/src/proving/prove.d.ts +3 -3
- package/lib/typescript/src/proving/prove.d.ts.map +1 -1
- package/lib/typescript/src/utils.d.ts.map +1 -1
- package/lib/typescript/src/verify.d.ts +4 -3
- package/lib/typescript/src/verify.d.ts.map +1 -1
- package/package.json +13 -6
- package/react-native.config.js +11 -0
- package/src/bindings.tsx +4 -0
- package/src/camera.tsx +116 -11
- package/src/capture.tsx +150 -53
- package/src/common.tsx +22 -2
- package/src/generated/zcam1_c2pa_utils-ffi.ts +42 -56
- package/src/generated/zcam1_c2pa_utils.ts +224 -67
- package/src/generated/zcam1_certs_utils-ffi.ts +33 -36
- package/src/generated/zcam1_certs_utils.ts +27 -24
- package/src/generated/zcam1_common-ffi.ts +183 -0
- package/src/generated/zcam1_common.ts +116 -0
- package/src/generated/zcam1_proving_utils-ffi.ts +54 -67
- package/src/generated/zcam1_proving_utils.ts +133 -138
- package/src/generated/zcam1_verify_utils-ffi.ts +39 -40
- package/src/generated/zcam1_verify_utils.ts +109 -47
- package/src/index.ts +1 -1
- package/src/proving/NativeZcam1Proving.ts +2 -2
- package/src/proving/index.ts +1 -1
- package/src/proving/prove.tsx +22 -11
- package/src/utils.ts +26 -20
- package/src/verify.tsx +25 -42
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
package com.succinctlabs.zcam1sdk.camera
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.pm.PackageManager
|
|
6
|
+
import android.graphics.Bitmap
|
|
7
|
+
import android.graphics.BitmapFactory
|
|
8
|
+
import android.graphics.Canvas
|
|
9
|
+
import android.graphics.Color
|
|
10
|
+
import android.graphics.LinearGradient
|
|
11
|
+
import android.graphics.Paint
|
|
12
|
+
import android.graphics.Shader
|
|
13
|
+
import android.graphics.Typeface
|
|
14
|
+
import android.media.MediaMetadataRetriever
|
|
15
|
+
import android.os.Build
|
|
16
|
+
import android.util.Log
|
|
17
|
+
import java.text.SimpleDateFormat
|
|
18
|
+
import java.util.Date
|
|
19
|
+
import java.util.Locale
|
|
20
|
+
import kotlin.random.Random
|
|
21
|
+
import androidx.exifinterface.media.ExifInterface
|
|
22
|
+
import androidx.camera.camera2.interop.Camera2CameraInfo
|
|
23
|
+
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
|
|
24
|
+
import androidx.camera.core.*
|
|
25
|
+
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
26
|
+
import androidx.camera.video.*
|
|
27
|
+
import androidx.camera.video.VideoCapture
|
|
28
|
+
import androidx.core.content.ContextCompat
|
|
29
|
+
import androidx.lifecycle.LifecycleOwner
|
|
30
|
+
import com.facebook.react.bridge.Promise
|
|
31
|
+
import com.facebook.react.bridge.WritableNativeArray
|
|
32
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
33
|
+
import java.io.File
|
|
34
|
+
import java.util.concurrent.Executor
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Core camera service managing CameraX lifecycle, photo capture, video recording,
|
|
38
|
+
* zoom, flash, focus, and exposure. Created per camera view instance.
|
|
39
|
+
*/
|
|
40
|
+
class Zcam1CameraService {
|
|
41
|
+
|
|
42
|
+
companion object {
|
|
43
|
+
private const val TAG = "Zcam1CameraService"
|
|
44
|
+
private const val MAX_ZOOM_CAP = 20.0f
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Active instance used by Zcam1CaptureModule to delegate camera methods.
|
|
48
|
+
* Set when a Zcam1CameraView starts its camera, cleared when it stops.
|
|
49
|
+
*/
|
|
50
|
+
@Volatile
|
|
51
|
+
var activeInstance: Zcam1CameraService? = null
|
|
52
|
+
private set
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private var camera: Camera? = null
|
|
56
|
+
private var cameraProvider: ProcessCameraProvider? = null
|
|
57
|
+
private var imageCapture: ImageCapture? = null
|
|
58
|
+
private var videoCapture: VideoCapture<Recorder>? = null
|
|
59
|
+
private var preview: Preview? = null
|
|
60
|
+
|
|
61
|
+
private var currentLensFacing = CameraUtils.LENS_FACING_BACK
|
|
62
|
+
private var currentFlashMode = CameraUtils.FLASH_OFF
|
|
63
|
+
private var currentZoom = 1.0f
|
|
64
|
+
|
|
65
|
+
private var lifecycleOwner: LifecycleOwner? = null
|
|
66
|
+
private var surfaceProvider: Preview.SurfaceProvider? = null
|
|
67
|
+
private var executor: Executor? = null
|
|
68
|
+
private var context: Context? = null
|
|
69
|
+
|
|
70
|
+
// Video recording state
|
|
71
|
+
private var activeRecording: Recording? = null
|
|
72
|
+
private var recordingOutputFile: File? = null
|
|
73
|
+
private var stopRecordingPromise: Promise? = null
|
|
74
|
+
private var recordingHasAudio: Boolean = false
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Start camera preview and capture pipeline.
|
|
78
|
+
*/
|
|
79
|
+
fun startCamera(
|
|
80
|
+
context: Context,
|
|
81
|
+
lifecycleOwner: LifecycleOwner,
|
|
82
|
+
surfaceProvider: Preview.SurfaceProvider,
|
|
83
|
+
lensFacing: String = "back",
|
|
84
|
+
flashMode: String = "off"
|
|
85
|
+
) {
|
|
86
|
+
this.context = context
|
|
87
|
+
this.lifecycleOwner = lifecycleOwner
|
|
88
|
+
this.surfaceProvider = surfaceProvider
|
|
89
|
+
this.executor = ContextCompat.getMainExecutor(context)
|
|
90
|
+
this.currentLensFacing = CameraUtils.mapLensFacing(lensFacing)
|
|
91
|
+
this.currentFlashMode = CameraUtils.mapFlashMode(flashMode)
|
|
92
|
+
|
|
93
|
+
activeInstance = this
|
|
94
|
+
|
|
95
|
+
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
|
96
|
+
cameraProviderFuture.addListener({
|
|
97
|
+
try {
|
|
98
|
+
cameraProvider = cameraProviderFuture.get()
|
|
99
|
+
bindCameraUseCases()
|
|
100
|
+
} catch (e: Exception) {
|
|
101
|
+
Log.e(TAG, "Failed to get camera provider", e)
|
|
102
|
+
}
|
|
103
|
+
}, executor!!)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Stop camera and release resources.
|
|
108
|
+
*/
|
|
109
|
+
fun stopCamera() {
|
|
110
|
+
activeRecording?.stop()
|
|
111
|
+
activeRecording = null
|
|
112
|
+
cameraProvider?.unbindAll()
|
|
113
|
+
camera = null
|
|
114
|
+
imageCapture = null
|
|
115
|
+
videoCapture = null
|
|
116
|
+
preview = null
|
|
117
|
+
if (activeInstance == this) {
|
|
118
|
+
activeInstance = null
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private fun bindCameraUseCases() {
|
|
123
|
+
val provider = cameraProvider ?: return
|
|
124
|
+
val owner = lifecycleOwner ?: return
|
|
125
|
+
val surface = surfaceProvider ?: return
|
|
126
|
+
|
|
127
|
+
provider.unbindAll()
|
|
128
|
+
|
|
129
|
+
val cameraSelector = CameraSelector.Builder()
|
|
130
|
+
.requireLensFacing(currentLensFacing)
|
|
131
|
+
.build()
|
|
132
|
+
|
|
133
|
+
preview = Preview.Builder()
|
|
134
|
+
.build()
|
|
135
|
+
.also { it.setSurfaceProvider(surface) }
|
|
136
|
+
|
|
137
|
+
imageCapture = ImageCapture.Builder()
|
|
138
|
+
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
|
|
139
|
+
.setFlashMode(currentFlashMode)
|
|
140
|
+
.build()
|
|
141
|
+
|
|
142
|
+
val recorder = Recorder.Builder()
|
|
143
|
+
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
|
|
144
|
+
.build()
|
|
145
|
+
videoCapture = VideoCapture.withOutput(recorder)
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
camera = provider.bindToLifecycle(owner, cameraSelector, preview, imageCapture, videoCapture)
|
|
149
|
+
} catch (e: Exception) {
|
|
150
|
+
Log.w(TAG, "Failed to bind with VideoCapture, retrying without it: ${e.message}")
|
|
151
|
+
videoCapture = null
|
|
152
|
+
try {
|
|
153
|
+
camera = provider.bindToLifecycle(owner, cameraSelector, preview, imageCapture)
|
|
154
|
+
} catch (e2: Exception) {
|
|
155
|
+
Log.e(TAG, "Failed to bind camera use cases", e2)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (currentZoom != 1.0f) {
|
|
160
|
+
setZoom(currentZoom)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// === Camera Controls ===
|
|
165
|
+
|
|
166
|
+
fun setZoom(factor: Float) {
|
|
167
|
+
val cam = camera ?: return
|
|
168
|
+
val maxZoom = cam.cameraInfo.zoomState.value?.maxZoomRatio ?: 1.0f
|
|
169
|
+
val cappedMax = minOf(maxZoom, MAX_ZOOM_CAP)
|
|
170
|
+
currentZoom = CameraUtils.clampZoom(factor, cappedMax)
|
|
171
|
+
cam.cameraControl.setZoomRatio(currentZoom)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fun setZoomAnimated(factor: Float) {
|
|
175
|
+
// CameraX doesn't have a built-in smooth ramp like AVFoundation; use linear zoom for smooth transitions
|
|
176
|
+
val cam = camera ?: return
|
|
177
|
+
val zoomState = cam.cameraInfo.zoomState.value ?: return
|
|
178
|
+
val minZoom = zoomState.minZoomRatio
|
|
179
|
+
val maxZoom = minOf(zoomState.maxZoomRatio, MAX_ZOOM_CAP)
|
|
180
|
+
val clamped = factor.coerceIn(minZoom, maxZoom)
|
|
181
|
+
val linearZoom = if (maxZoom > minZoom) (clamped - minZoom) / (maxZoom - minZoom) else 0f
|
|
182
|
+
cam.cameraControl.setLinearZoom(linearZoom)
|
|
183
|
+
currentZoom = clamped
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fun setFlashMode(mode: String) {
|
|
187
|
+
currentFlashMode = CameraUtils.mapFlashMode(mode)
|
|
188
|
+
imageCapture?.flashMode = currentFlashMode
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fun getMinZoom(): Float {
|
|
192
|
+
return camera?.cameraInfo?.zoomState?.value?.minZoomRatio ?: 1.0f
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fun getMaxZoom(): Float {
|
|
196
|
+
val cam = camera ?: return 1.0f
|
|
197
|
+
val maxZoom = cam.cameraInfo.zoomState.value?.maxZoomRatio ?: 1.0f
|
|
198
|
+
return minOf(maxZoom, MAX_ZOOM_CAP)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fun focusAtPoint(x: Float, y: Float) {
|
|
202
|
+
val cam = camera ?: return
|
|
203
|
+
|
|
204
|
+
val factory = SurfaceOrientedMeteringPointFactory(1.0f, 1.0f)
|
|
205
|
+
val point = factory.createPoint(x, y)
|
|
206
|
+
val action = FocusMeteringAction.Builder(point).build()
|
|
207
|
+
|
|
208
|
+
cam.cameraControl.startFocusAndMetering(action)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// === Exposure ===
|
|
212
|
+
|
|
213
|
+
fun getExposureRange(): Pair<Float, Float> {
|
|
214
|
+
val state = camera?.cameraInfo?.exposureState ?: return Pair(-2.0f, 2.0f)
|
|
215
|
+
val range = state.exposureCompensationRange
|
|
216
|
+
val step = state.exposureCompensationStep.toFloat()
|
|
217
|
+
return Pair(range.lower * step, range.upper * step)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fun resetExposure() {
|
|
221
|
+
camera?.cameraControl?.setExposureCompensationIndex(0)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// === Emulator Helpers ===
|
|
225
|
+
|
|
226
|
+
private fun isEmulator(): Boolean {
|
|
227
|
+
|
|
228
|
+
return (Build.HARDWARE == "goldfish"
|
|
229
|
+
|| Build.HARDWARE == "ranchu"
|
|
230
|
+
|| Build.FINGERPRINT.startsWith("generic")
|
|
231
|
+
|| Build.FINGERPRINT.contains("emulator")
|
|
232
|
+
|| Build.MODEL.contains("Emulator")
|
|
233
|
+
|| Build.MODEL.contains("Android SDK built for")
|
|
234
|
+
|| Build.MANUFACTURER.contains("Genymotion")
|
|
235
|
+
|| Build.BRAND.startsWith("generic")
|
|
236
|
+
|| Build.DEVICE.startsWith("generic"))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private fun createTestImage(): Bitmap {
|
|
240
|
+
val width = 1920
|
|
241
|
+
val height = 1080
|
|
242
|
+
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
243
|
+
val canvas = Canvas(bitmap)
|
|
244
|
+
|
|
245
|
+
val rng = Random.Default
|
|
246
|
+
val hue = rng.nextFloat() * 360f
|
|
247
|
+
val saturation = 0.55f + rng.nextFloat() * 0.35f
|
|
248
|
+
val brightness1 = 0.5f + rng.nextFloat() * 0.25f
|
|
249
|
+
val brightness2 = (brightness1 + 0.15f + rng.nextFloat() * 0.2f).coerceAtMost(1.0f)
|
|
250
|
+
|
|
251
|
+
val color1 = Color.HSVToColor(floatArrayOf(hue, saturation, brightness1))
|
|
252
|
+
val color2 = Color.HSVToColor(floatArrayOf(hue, saturation, brightness2))
|
|
253
|
+
|
|
254
|
+
val gradientPaint = Paint()
|
|
255
|
+
gradientPaint.shader = LinearGradient(
|
|
256
|
+
0f, 0f, width.toFloat(), height.toFloat(),
|
|
257
|
+
color1, color2, Shader.TileMode.CLAMP
|
|
258
|
+
)
|
|
259
|
+
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gradientPaint)
|
|
260
|
+
|
|
261
|
+
val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
262
|
+
color = Color.argb(230, 255, 255, 255)
|
|
263
|
+
textSize = 96f
|
|
264
|
+
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
|
265
|
+
textAlign = Paint.Align.CENTER
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
val label = "EMULATOR TEST IMAGE"
|
|
269
|
+
canvas.drawText(label, width / 2f, height / 2f - 60f, textPaint)
|
|
270
|
+
|
|
271
|
+
val datePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
272
|
+
color = Color.argb(230, 255, 255, 255)
|
|
273
|
+
textSize = 72f
|
|
274
|
+
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
|
|
275
|
+
textAlign = Paint.Align.CENTER
|
|
276
|
+
}
|
|
277
|
+
val dateStr = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date())
|
|
278
|
+
canvas.drawText(dateStr, width / 2f, height / 2f + 80f, datePaint)
|
|
279
|
+
|
|
280
|
+
return bitmap
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// === Photo Capture ===
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Capture a photo and resolve the promise with path and format.
|
|
287
|
+
* The returned temp file is owned by the caller.
|
|
288
|
+
*/
|
|
289
|
+
fun takePhoto(format: String, flash: String, promise: Promise) {
|
|
290
|
+
if (format == "dng") {
|
|
291
|
+
promise.reject("UNSUPPORTED_FORMAT", "DNG format is not supported on Android")
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (isEmulator()) {
|
|
296
|
+
try {
|
|
297
|
+
val bitmap = createTestImage()
|
|
298
|
+
val tempFile = File.createTempFile("zcam1_emulator_", ".jpg")
|
|
299
|
+
tempFile.outputStream().use { out ->
|
|
300
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
|
301
|
+
}
|
|
302
|
+
bitmap.recycle()
|
|
303
|
+
|
|
304
|
+
val dateStr = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US).format(Date())
|
|
305
|
+
val exif = WritableNativeMap().apply {
|
|
306
|
+
putArray("ISOSpeedRatings", WritableNativeArray())
|
|
307
|
+
putInt("PixelXDimension", 1920)
|
|
308
|
+
putInt("PixelYDimension", 1080)
|
|
309
|
+
putDouble("ExposureTime", 0.0)
|
|
310
|
+
putDouble("FNumber", 1.0)
|
|
311
|
+
putDouble("FocalLength", 5.0)
|
|
312
|
+
}
|
|
313
|
+
val tiff = WritableNativeMap().apply {
|
|
314
|
+
putString("DateTime", dateStr)
|
|
315
|
+
putString("Make", "Android")
|
|
316
|
+
putString("Model", "Android Emulator")
|
|
317
|
+
putString("Software", "Android Emulator")
|
|
318
|
+
}
|
|
319
|
+
val metadata = WritableNativeMap().apply {
|
|
320
|
+
putMap("{Exif}", exif)
|
|
321
|
+
putMap("{TIFF}", tiff)
|
|
322
|
+
putInt("Orientation", 1)
|
|
323
|
+
}
|
|
324
|
+
val result = WritableNativeMap().apply {
|
|
325
|
+
putString("filePath", tempFile.absolutePath)
|
|
326
|
+
putString("format", "jpeg")
|
|
327
|
+
putMap("metadata", metadata)
|
|
328
|
+
}
|
|
329
|
+
promise.resolve(result)
|
|
330
|
+
} catch (e: Exception) {
|
|
331
|
+
promise.reject("CAPTURE_ERROR", "Failed to create emulator test image: ${e.message}", e)
|
|
332
|
+
}
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (camera == null) {
|
|
337
|
+
promise.reject(
|
|
338
|
+
"CAMERA_ERROR",
|
|
339
|
+
"Camera not bound — check that the device has a camera and permissions are granted"
|
|
340
|
+
)
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
val capture = imageCapture ?: run {
|
|
345
|
+
promise.reject("CAMERA_ERROR", "Camera not initialized")
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
val exec = executor ?: run {
|
|
349
|
+
promise.reject("CAMERA_ERROR", "Executor not available")
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
capture.flashMode = CameraUtils.mapFlashMode(flash)
|
|
354
|
+
capture.targetRotation = when (Zcam1OrientationManager.currentOrientation()) {
|
|
355
|
+
90 -> android.view.Surface.ROTATION_90
|
|
356
|
+
180 -> android.view.Surface.ROTATION_180
|
|
357
|
+
270 -> android.view.Surface.ROTATION_270
|
|
358
|
+
else -> android.view.Surface.ROTATION_0
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
val tempFile = File.createTempFile("zcam1_", ".jpg")
|
|
362
|
+
val outputOptions = ImageCapture.OutputFileOptions.Builder(tempFile).build()
|
|
363
|
+
|
|
364
|
+
capture.takePicture(
|
|
365
|
+
outputOptions,
|
|
366
|
+
exec,
|
|
367
|
+
object : ImageCapture.OnImageSavedCallback {
|
|
368
|
+
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
|
|
369
|
+
val exifData = try {
|
|
370
|
+
val exifInterface = ExifInterface(tempFile.absolutePath)
|
|
371
|
+
val exifMap = WritableNativeMap().apply {
|
|
372
|
+
val width = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0)
|
|
373
|
+
val height = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0)
|
|
374
|
+
if (width > 0) putInt("PixelXDimension", width)
|
|
375
|
+
if (height > 0) putInt("PixelYDimension", height)
|
|
376
|
+
exifInterface.getAttributeDouble(ExifInterface.TAG_EXPOSURE_TIME, Double.NaN)
|
|
377
|
+
.takeIf { !it.isNaN() }?.let { putDouble("ExposureTime", it) }
|
|
378
|
+
exifInterface.getAttributeDouble(ExifInterface.TAG_F_NUMBER, Double.NaN)
|
|
379
|
+
.takeIf { !it.isNaN() }?.let { putDouble("FNumber", it) }
|
|
380
|
+
exifInterface.getAttributeDouble(ExifInterface.TAG_FOCAL_LENGTH, Double.NaN)
|
|
381
|
+
.takeIf { !it.isNaN() }?.let { putDouble("FocalLength", it) }
|
|
382
|
+
exifInterface.getAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS)
|
|
383
|
+
?.let { putString("ISOSpeedRatings", it) }
|
|
384
|
+
}
|
|
385
|
+
val tiffMap = WritableNativeMap().apply {
|
|
386
|
+
exifInterface.getAttribute(ExifInterface.TAG_DATETIME)
|
|
387
|
+
?.let { putString("DateTime", it) }
|
|
388
|
+
exifInterface.getAttribute(ExifInterface.TAG_MAKE)
|
|
389
|
+
?.let { putString("Make", it) }
|
|
390
|
+
exifInterface.getAttribute(ExifInterface.TAG_MODEL)
|
|
391
|
+
?.let { putString("Model", it) }
|
|
392
|
+
exifInterface.getAttribute(ExifInterface.TAG_SOFTWARE)
|
|
393
|
+
?.let { putString("Software", it) }
|
|
394
|
+
}
|
|
395
|
+
val orientation = exifInterface.getAttributeInt(
|
|
396
|
+
ExifInterface.TAG_ORIENTATION,
|
|
397
|
+
ExifInterface.ORIENTATION_NORMAL
|
|
398
|
+
)
|
|
399
|
+
WritableNativeMap().apply {
|
|
400
|
+
putMap("{Exif}", exifMap)
|
|
401
|
+
putMap("{TIFF}", tiffMap)
|
|
402
|
+
putInt("Orientation", orientation)
|
|
403
|
+
}
|
|
404
|
+
} catch (e: Exception) {
|
|
405
|
+
Log.w(TAG, "Failed to read EXIF from captured image", e)
|
|
406
|
+
null
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
val result = WritableNativeMap().apply {
|
|
410
|
+
putString("filePath", tempFile.absolutePath)
|
|
411
|
+
putString("format", "jpeg")
|
|
412
|
+
if (exifData != null) putMap("metadata", exifData) else putNull("metadata")
|
|
413
|
+
}
|
|
414
|
+
promise.resolve(result)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
override fun onError(exception: ImageCaptureException) {
|
|
418
|
+
Log.e(TAG, "Photo capture failed", exception)
|
|
419
|
+
tempFile.delete()
|
|
420
|
+
promise.reject("CAPTURE_ERROR", exception.message, exception)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// === Video Recording ===
|
|
427
|
+
|
|
428
|
+
fun startVideoRecording(maxDurationSeconds: Double, promise: Promise) {
|
|
429
|
+
val vc = videoCapture ?: run {
|
|
430
|
+
promise.reject("CAMERA_ERROR", "Camera not initialized")
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
val ctx = context ?: run {
|
|
434
|
+
promise.reject("CAMERA_ERROR", "Context not available")
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
val exec = executor ?: run {
|
|
438
|
+
promise.reject("CAMERA_ERROR", "Executor not available")
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (activeRecording != null) {
|
|
443
|
+
promise.reject("RECORDING_ERROR", "A recording is already in progress")
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
val hasAudio = ContextCompat.checkSelfPermission(ctx, Manifest.permission.RECORD_AUDIO) ==
|
|
448
|
+
PackageManager.PERMISSION_GRANTED
|
|
449
|
+
recordingHasAudio = hasAudio
|
|
450
|
+
|
|
451
|
+
val outputFile = File.createTempFile("zcam1_video_", ".mp4")
|
|
452
|
+
recordingOutputFile = outputFile
|
|
453
|
+
|
|
454
|
+
val outputOptions = FileOutputOptions.Builder(outputFile).apply {
|
|
455
|
+
if (maxDurationSeconds > 0) {
|
|
456
|
+
setDurationLimitMillis((maxDurationSeconds * 1000).toLong())
|
|
457
|
+
}
|
|
458
|
+
}.build()
|
|
459
|
+
|
|
460
|
+
var pendingRecording = vc.output.prepareRecording(ctx, outputOptions)
|
|
461
|
+
if (hasAudio) {
|
|
462
|
+
pendingRecording = pendingRecording.withAudioEnabled()
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
activeRecording = pendingRecording.start(exec) { event ->
|
|
466
|
+
when (event) {
|
|
467
|
+
is VideoRecordEvent.Finalize -> {
|
|
468
|
+
val stopPromise = stopRecordingPromise
|
|
469
|
+
stopRecordingPromise = null
|
|
470
|
+
activeRecording = null
|
|
471
|
+
|
|
472
|
+
if (event.hasError()) {
|
|
473
|
+
Log.e(TAG, "Video recording finalized with error: ${event.error}")
|
|
474
|
+
outputFile.delete()
|
|
475
|
+
stopPromise?.reject("RECORDING_ERROR", "Recording failed: ${event.error}")
|
|
476
|
+
} else {
|
|
477
|
+
stopPromise?.resolve(buildStopResult(outputFile, hasAudio))
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
else -> { /* ignore Start, Status events */
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
val startResult = WritableNativeMap().apply {
|
|
487
|
+
putString("status", "recording")
|
|
488
|
+
putString("filePath", outputFile.absolutePath)
|
|
489
|
+
putString("format", "mov")
|
|
490
|
+
putBoolean("hasAudio", hasAudio)
|
|
491
|
+
}
|
|
492
|
+
promise.resolve(startResult)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
fun stopVideoRecording(promise: Promise) {
|
|
496
|
+
val recording = activeRecording ?: run {
|
|
497
|
+
promise.reject("RECORDING_ERROR", "No active recording")
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
stopRecordingPromise = promise
|
|
501
|
+
recording.stop()
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private fun buildStopResult(outputFile: File, hasAudio: Boolean): WritableNativeMap {
|
|
505
|
+
val retriever = MediaMetadataRetriever()
|
|
506
|
+
try {
|
|
507
|
+
retriever.setDataSource(outputFile.absolutePath)
|
|
508
|
+
|
|
509
|
+
val durationMs =
|
|
510
|
+
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() ?: 0L
|
|
511
|
+
val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() ?: 0
|
|
512
|
+
val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: 0
|
|
513
|
+
val rotation =
|
|
514
|
+
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull() ?: 0
|
|
515
|
+
val frameRate =
|
|
516
|
+
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE)?.toDoubleOrNull()
|
|
517
|
+
?: 0.0
|
|
518
|
+
|
|
519
|
+
return WritableNativeMap().apply {
|
|
520
|
+
putString("filePath", outputFile.absolutePath)
|
|
521
|
+
putString("format", "mov")
|
|
522
|
+
putBoolean("hasAudio", hasAudio)
|
|
523
|
+
putString("deviceMake", Build.MANUFACTURER)
|
|
524
|
+
putString("deviceModel", Build.MODEL)
|
|
525
|
+
putString("softwareVersion", Build.VERSION.RELEASE)
|
|
526
|
+
putDouble("durationSeconds", durationMs / 1000.0)
|
|
527
|
+
putDouble("fileSizeBytes", outputFile.length().toDouble())
|
|
528
|
+
putInt("width", width)
|
|
529
|
+
putInt("height", height)
|
|
530
|
+
putInt("rotationDegrees", rotation)
|
|
531
|
+
putDouble("frameRate", frameRate)
|
|
532
|
+
}
|
|
533
|
+
} catch (e: Exception) {
|
|
534
|
+
Log.e(TAG, "Failed to extract video metadata", e)
|
|
535
|
+
return WritableNativeMap().apply {
|
|
536
|
+
putString("filePath", outputFile.absolutePath)
|
|
537
|
+
putString("format", "mov")
|
|
538
|
+
putBoolean("hasAudio", hasAudio)
|
|
539
|
+
putString("deviceMake", Build.MANUFACTURER)
|
|
540
|
+
putString("deviceModel", Build.MODEL)
|
|
541
|
+
putString("softwareVersion", Build.VERSION.RELEASE)
|
|
542
|
+
putDouble("durationSeconds", 0.0)
|
|
543
|
+
putDouble("fileSizeBytes", outputFile.length().toDouble())
|
|
544
|
+
putInt("width", 0)
|
|
545
|
+
putInt("height", 0)
|
|
546
|
+
putInt("rotationDegrees", 0)
|
|
547
|
+
putDouble("frameRate", 0.0)
|
|
548
|
+
}
|
|
549
|
+
} finally {
|
|
550
|
+
retriever.release()
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// === Diagnostics ===
|
|
555
|
+
|
|
556
|
+
@androidx.annotation.OptIn(ExperimentalCamera2Interop::class)
|
|
557
|
+
fun getDeviceDiagnostics(): WritableNativeMap {
|
|
558
|
+
val cam = camera
|
|
559
|
+
val zoomState = cam?.cameraInfo?.zoomState?.value
|
|
560
|
+
val exposureState = cam?.cameraInfo?.exposureState
|
|
561
|
+
|
|
562
|
+
val minZoom = zoomState?.minZoomRatio?.toDouble() ?: 1.0
|
|
563
|
+
val maxZoom = minOf(zoomState?.maxZoomRatio ?: 1.0f, MAX_ZOOM_CAP).toDouble()
|
|
564
|
+
val currentZoomVal = zoomState?.zoomRatio?.toDouble() ?: currentZoom.toDouble()
|
|
565
|
+
|
|
566
|
+
val expStep = exposureState?.exposureCompensationStep?.toFloat() ?: 1.0f
|
|
567
|
+
val expRange = exposureState?.exposureCompensationRange
|
|
568
|
+
val minExpBias = (expRange?.lower ?: -2) * expStep
|
|
569
|
+
val maxExpBias = (expRange?.upper ?: 2) * expStep
|
|
570
|
+
val currentExpIndex = exposureState?.exposureCompensationIndex ?: 0
|
|
571
|
+
val currentExpBias = currentExpIndex * expStep
|
|
572
|
+
|
|
573
|
+
return WritableNativeMap().apply {
|
|
574
|
+
putString("deviceType", "builtInCamera")
|
|
575
|
+
putDouble("minZoom", minZoom)
|
|
576
|
+
putDouble("maxZoom", maxZoom)
|
|
577
|
+
putDouble("currentZoom", currentZoomVal)
|
|
578
|
+
putArray("switchOverFactors", WritableNativeArray())
|
|
579
|
+
putInt("switchingBehavior", 0)
|
|
580
|
+
putBoolean("isVirtualDevice", false)
|
|
581
|
+
putDouble("currentExposureBias", currentExpBias.toDouble())
|
|
582
|
+
putDouble("minExposureBias", minExpBias.toDouble())
|
|
583
|
+
putDouble("maxExposureBias", maxExpBias.toDouble())
|
|
584
|
+
putDouble("currentISO", 0.0)
|
|
585
|
+
putDouble("exposureDuration", 0.0)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
package com.succinctlabs.zcam1sdk.camera
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import android.widget.FrameLayout
|
|
6
|
+
import androidx.camera.view.PreviewView
|
|
7
|
+
import androidx.lifecycle.LifecycleOwner
|
|
8
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* React Native camera view. Hosts a CameraX PreviewView and manages
|
|
12
|
+
* the camera service lifecycle tied to the view's window attachment.
|
|
13
|
+
*
|
|
14
|
+
* Handles React Native Fabric's view recycling: Fabric may detach and
|
|
15
|
+
* re-attach the view during layout passes. We delay camera teardown
|
|
16
|
+
* to avoid restarting the camera on every re-layout.
|
|
17
|
+
*/
|
|
18
|
+
class Zcam1CameraView(private val reactContext: ThemedReactContext) : FrameLayout(reactContext) {
|
|
19
|
+
|
|
20
|
+
private val previewView = PreviewView(reactContext)
|
|
21
|
+
private val cameraService = Zcam1CameraService()
|
|
22
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
23
|
+
private var pendingTeardown: Runnable? = null
|
|
24
|
+
private var cameraStarted = false
|
|
25
|
+
|
|
26
|
+
var cameraType: String = "back"
|
|
27
|
+
set(value) {
|
|
28
|
+
if (field != value) {
|
|
29
|
+
field = value
|
|
30
|
+
if (cameraStarted) restartCamera()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var flashMode: String = "off"
|
|
35
|
+
set(value) {
|
|
36
|
+
field = value
|
|
37
|
+
cameraService.setFlashMode(value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
var zoom: Float = 1.0f
|
|
41
|
+
set(value) {
|
|
42
|
+
field = value
|
|
43
|
+
cameraService.setZoom(value)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
init {
|
|
47
|
+
addView(previewView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Override requestLayout to work with Fabric's measurement system.
|
|
52
|
+
* Without this, the PreviewView may not render correctly after Fabric layout passes.
|
|
53
|
+
*/
|
|
54
|
+
override fun requestLayout() {
|
|
55
|
+
super.requestLayout()
|
|
56
|
+
post {
|
|
57
|
+
measure(
|
|
58
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
59
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
60
|
+
)
|
|
61
|
+
layout(left, top, right, bottom)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override fun onAttachedToWindow() {
|
|
66
|
+
super.onAttachedToWindow()
|
|
67
|
+
// Cancel any pending teardown — this is just Fabric re-attaching us
|
|
68
|
+
pendingTeardown?.let {
|
|
69
|
+
mainHandler.removeCallbacks(it)
|
|
70
|
+
pendingTeardown = null
|
|
71
|
+
}
|
|
72
|
+
if (!cameraStarted) {
|
|
73
|
+
Zcam1OrientationManager.startUpdates(reactContext)
|
|
74
|
+
startCamera()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override fun onDetachedFromWindow() {
|
|
79
|
+
// Delay teardown to distinguish real removal from Fabric re-layout
|
|
80
|
+
pendingTeardown = Runnable {
|
|
81
|
+
pendingTeardown = null
|
|
82
|
+
cameraService.stopCamera()
|
|
83
|
+
Zcam1OrientationManager.stopUpdates()
|
|
84
|
+
cameraStarted = false
|
|
85
|
+
}
|
|
86
|
+
mainHandler.postDelayed(pendingTeardown!!, 200)
|
|
87
|
+
super.onDetachedFromWindow()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private fun startCamera() {
|
|
91
|
+
val lifecycleOwner = reactContext.currentActivity as? LifecycleOwner ?: return
|
|
92
|
+
|
|
93
|
+
cameraService.startCamera(
|
|
94
|
+
context = reactContext,
|
|
95
|
+
lifecycleOwner = lifecycleOwner,
|
|
96
|
+
surfaceProvider = previewView.surfaceProvider,
|
|
97
|
+
lensFacing = cameraType,
|
|
98
|
+
flashMode = flashMode
|
|
99
|
+
)
|
|
100
|
+
cameraStarted = true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fun restartCamera() {
|
|
104
|
+
cameraService.stopCamera()
|
|
105
|
+
startCamera()
|
|
106
|
+
}
|
|
107
|
+
}
|