capacitor-camera-view 2.4.0 → 3.0.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/CapacitorCameraView.podspec +1 -1
- package/Package.swift +1 -1
- package/README.md +341 -49
- package/android/build.gradle +0 -1
- package/android/src/main/AndroidManifest.xml +0 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraError.kt +42 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +945 -207
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +87 -43
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/BarcodeDetectionResult.kt +28 -2
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraDevice.kt +6 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +15 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/TorchModeState.kt +15 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/WebBoundingRect.kt +1 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +73 -19
- package/dist/docs.json +475 -30
- package/dist/esm/definitions.d.ts +478 -26
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/utils.d.ts +59 -15
- package/dist/esm/utils.js +79 -38
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/web.d.ts +191 -13
- package/dist/esm/web.js +624 -141
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +711 -179
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +711 -179
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CameraViewPlugin/CameraError.swift +147 -2
- package/ios/Sources/CameraViewPlugin/CameraEvents.swift +41 -19
- package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +29 -1
- package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +78 -23
- package/ios/Sources/CameraViewPlugin/CameraViewManager+DeferredStart.swift +37 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+Focus.swift +131 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+Lifecycle.swift +156 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +73 -41
- package/ios/Sources/CameraViewPlugin/CameraViewManager+ResolutionSelection.swift +57 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+Rotation.swift +195 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +24 -12
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoRecording.swift +22 -73
- package/ios/Sources/CameraViewPlugin/CameraViewManager+Zoom.swift +113 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +450 -403
- package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +98 -65
- package/ios/Sources/CameraViewPlugin/Utils.swift +61 -7
- package/package.json +25 -9
|
@@ -52,7 +52,7 @@ class CameraViewPlugin : Plugin() {
|
|
|
52
52
|
if (getPermissionState("camera") == PermissionState.GRANTED) {
|
|
53
53
|
startCamera(call)
|
|
54
54
|
} else {
|
|
55
|
-
call.reject("Permission is required to take a picture")
|
|
55
|
+
call.reject("Permission is required to take a picture", CameraError.PERMISSION_DENIED)
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -60,21 +60,35 @@ class CameraViewPlugin : Plugin() {
|
|
|
60
60
|
val config = sessionConfigFromPluginCall(call)
|
|
61
61
|
|
|
62
62
|
pluginScope.launch {
|
|
63
|
+
// Cancel any previous collector and, if this session wants barcode
|
|
64
|
+
// detection, subscribe a fresh one *before* starting the session.
|
|
65
|
+
// `barcodeEvents` has replay = 0, so a detection emitted between
|
|
66
|
+
// analyzer attachment (inside startSessionAsync -> bindToLifecycle)
|
|
67
|
+
// and collector subscription would otherwise be silently dropped -
|
|
68
|
+
// subscribing here, before startSessionAsync even runs, closes
|
|
69
|
+
// that window entirely. pluginScope uses Dispatchers.Main.immediate,
|
|
70
|
+
// so this nested launch's `collect` call registers its slot on the
|
|
71
|
+
// SharedFlow synchronously before control returns here.
|
|
72
|
+
barcodeJob?.cancel()
|
|
73
|
+
barcodeJob = if (config.enableBarcodeDetection) {
|
|
74
|
+
pluginScope.launch {
|
|
75
|
+
implementation.barcodeEvents.collect { result ->
|
|
76
|
+
notifyBarcodeDetected(result)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
null
|
|
81
|
+
}
|
|
82
|
+
|
|
63
83
|
implementation.startSessionAsync(config).fold(
|
|
64
84
|
onSuccess = {
|
|
65
|
-
// Subscribe to barcode events if detection is enabled
|
|
66
|
-
if (config.enableBarcodeDetection) {
|
|
67
|
-
barcodeJob?.cancel()
|
|
68
|
-
barcodeJob = pluginScope.launch {
|
|
69
|
-
implementation.barcodeEvents.collect { result ->
|
|
70
|
-
notifyBarcodeDetected(result)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
85
|
call.resolve()
|
|
75
86
|
},
|
|
76
87
|
onError = { error ->
|
|
77
|
-
|
|
88
|
+
// The session never started - don't leak a live collector.
|
|
89
|
+
barcodeJob?.cancel()
|
|
90
|
+
barcodeJob = null
|
|
91
|
+
call.reject("Failed to start camera preview: ${error.localizedMessage}", error.cameraErrorCode, error)
|
|
78
92
|
}
|
|
79
93
|
)
|
|
80
94
|
}
|
|
@@ -90,7 +104,7 @@ class CameraViewPlugin : Plugin() {
|
|
|
90
104
|
implementation.stopSessionAsync().fold(
|
|
91
105
|
onSuccess = { call.resolve() },
|
|
92
106
|
onError = { error ->
|
|
93
|
-
call.reject("Failed to stop camera preview: ${error.localizedMessage}", error)
|
|
107
|
+
call.reject("Failed to stop camera preview: ${error.localizedMessage}", error.cameraErrorCode, error)
|
|
94
108
|
}
|
|
95
109
|
)
|
|
96
110
|
}
|
|
@@ -111,7 +125,7 @@ class CameraViewPlugin : Plugin() {
|
|
|
111
125
|
val saveToFile = call.getBoolean("saveToFile") ?: false
|
|
112
126
|
|
|
113
127
|
if (quality !in 0..100) {
|
|
114
|
-
call.reject("Quality must be between 0 and 100")
|
|
128
|
+
call.reject("Quality must be between 0 and 100", CameraError.INVALID_ARGUMENT)
|
|
115
129
|
return
|
|
116
130
|
}
|
|
117
131
|
|
|
@@ -122,7 +136,7 @@ class CameraViewPlugin : Plugin() {
|
|
|
122
136
|
Log.d(TAG, "capture took ${System.currentTimeMillis() - timeStart}ms")
|
|
123
137
|
},
|
|
124
138
|
onError = { error ->
|
|
125
|
-
call.reject("Failed to capture image: ${error.message}", error)
|
|
139
|
+
call.reject("Failed to capture image: ${error.message}", error.cameraErrorCode, error)
|
|
126
140
|
Log.d(TAG, "capture failed after ${System.currentTimeMillis() - timeStart}ms")
|
|
127
141
|
}
|
|
128
142
|
)
|
|
@@ -136,7 +150,7 @@ class CameraViewPlugin : Plugin() {
|
|
|
136
150
|
val saveToFile = call.getBoolean("saveToFile") ?: false
|
|
137
151
|
|
|
138
152
|
if (quality !in 0..100) {
|
|
139
|
-
call.reject("Quality must be between 0 and 100")
|
|
153
|
+
call.reject("Quality must be between 0 and 100", CameraError.INVALID_ARGUMENT)
|
|
140
154
|
return
|
|
141
155
|
}
|
|
142
156
|
|
|
@@ -147,7 +161,7 @@ class CameraViewPlugin : Plugin() {
|
|
|
147
161
|
Log.d(TAG, "captureSample took ${System.currentTimeMillis() - timeStart}ms")
|
|
148
162
|
},
|
|
149
163
|
onError = { error ->
|
|
150
|
-
call.reject("Failed to capture frame: ${error.message}", error)
|
|
164
|
+
call.reject("Failed to capture frame: ${error.message}", error.cameraErrorCode, error)
|
|
151
165
|
Log.d(
|
|
152
166
|
TAG,
|
|
153
167
|
"captureSample failed after ${System.currentTimeMillis() - timeStart}ms"
|
|
@@ -201,7 +215,10 @@ class CameraViewPlugin : Plugin() {
|
|
|
201
215
|
val videoQuality =
|
|
202
216
|
parseVideoRecordingQuality(call.getString("videoQuality"))
|
|
203
217
|
?: run {
|
|
204
|
-
call.reject(
|
|
218
|
+
call.reject(
|
|
219
|
+
"Invalid videoQuality. Use one of: lowest, sd, hd, fhd, uhd, highest",
|
|
220
|
+
CameraError.INVALID_ARGUMENT
|
|
221
|
+
)
|
|
205
222
|
return
|
|
206
223
|
}
|
|
207
224
|
|
|
@@ -220,12 +237,15 @@ class CameraViewPlugin : Plugin() {
|
|
|
220
237
|
val videoQuality =
|
|
221
238
|
parseVideoRecordingQuality(call.getString("videoQuality"))
|
|
222
239
|
?: run {
|
|
223
|
-
call.reject(
|
|
240
|
+
call.reject(
|
|
241
|
+
"Invalid videoQuality. Use one of: lowest, sd, hd, fhd, uhd, highest",
|
|
242
|
+
CameraError.INVALID_ARGUMENT
|
|
243
|
+
)
|
|
224
244
|
return
|
|
225
245
|
}
|
|
226
246
|
doStartRecording(call, enableAudio, videoQuality)
|
|
227
247
|
} else {
|
|
228
|
-
call.reject("Microphone permission is required for audio recording")
|
|
248
|
+
call.reject("Microphone permission is required for audio recording", CameraError.PERMISSION_DENIED)
|
|
229
249
|
}
|
|
230
250
|
}
|
|
231
251
|
|
|
@@ -242,7 +262,7 @@ class CameraViewPlugin : Plugin() {
|
|
|
242
262
|
implementation.startRecordingAsync(enableAudio, videoQuality).fold(
|
|
243
263
|
onSuccess = { call.resolve() },
|
|
244
264
|
onError = { error ->
|
|
245
|
-
call.reject("Failed to start recording: ${error.message}", error)
|
|
265
|
+
call.reject("Failed to start recording: ${error.message}", error.cameraErrorCode, error)
|
|
246
266
|
}
|
|
247
267
|
)
|
|
248
268
|
}
|
|
@@ -254,7 +274,7 @@ class CameraViewPlugin : Plugin() {
|
|
|
254
274
|
implementation.stopRecordingAsync().fold(
|
|
255
275
|
onSuccess = { result -> call.resolve(result) },
|
|
256
276
|
onError = { error ->
|
|
257
|
-
call.reject("Failed to stop recording: ${error.message}", error)
|
|
277
|
+
call.reject("Failed to stop recording: ${error.message}", error.cameraErrorCode, error)
|
|
258
278
|
}
|
|
259
279
|
)
|
|
260
280
|
}
|
|
@@ -269,6 +289,7 @@ class CameraViewPlugin : Plugin() {
|
|
|
269
289
|
put("id", device.id)
|
|
270
290
|
put("name", device.name)
|
|
271
291
|
put("position", device.position)
|
|
292
|
+
device.deviceType?.let { put("deviceType", it) }
|
|
272
293
|
})
|
|
273
294
|
}
|
|
274
295
|
}
|
|
@@ -280,7 +301,7 @@ class CameraViewPlugin : Plugin() {
|
|
|
280
301
|
fun flipCamera(call: PluginCall) {
|
|
281
302
|
implementation.flipCamera { error ->
|
|
282
303
|
if (error != null) {
|
|
283
|
-
call.reject("Failed to flip camera: ${error.localizedMessage}", error)
|
|
304
|
+
call.reject("Failed to flip camera: ${error.localizedMessage}", error.cameraErrorCode, error)
|
|
284
305
|
} else {
|
|
285
306
|
call.resolve()
|
|
286
307
|
}
|
|
@@ -302,13 +323,32 @@ class CameraViewPlugin : Plugin() {
|
|
|
302
323
|
fun setZoom(call: PluginCall) {
|
|
303
324
|
val level = call.getFloat("level")
|
|
304
325
|
if (level == null) {
|
|
305
|
-
call.reject("Zoom level must be provided")
|
|
326
|
+
call.reject("Zoom level must be provided", CameraError.INVALID_ARGUMENT)
|
|
306
327
|
return
|
|
307
328
|
}
|
|
308
329
|
|
|
309
330
|
implementation.setZoomFactor(level) { error ->
|
|
310
331
|
if (error != null) {
|
|
311
|
-
call.reject(error.localizedMessage)
|
|
332
|
+
call.reject(error.localizedMessage ?: "Failed to set zoom level", error.cameraErrorCode, error)
|
|
333
|
+
} else {
|
|
334
|
+
call.resolve()
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@PluginMethod
|
|
340
|
+
fun setFocusPoint(call: PluginCall) {
|
|
341
|
+
val x = call.getFloat("x")
|
|
342
|
+
val y = call.getFloat("y")
|
|
343
|
+
|
|
344
|
+
if (x == null || y == null) {
|
|
345
|
+
call.reject("Focus point x and y must be provided", CameraError.INVALID_ARGUMENT)
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
implementation.setFocusPoint(x, y) { error ->
|
|
350
|
+
if (error != null) {
|
|
351
|
+
call.reject("Failed to set focus point: ${error.localizedMessage}", error.cameraErrorCode, error)
|
|
312
352
|
} else {
|
|
313
353
|
call.resolve()
|
|
314
354
|
}
|
|
@@ -337,21 +377,22 @@ class CameraViewPlugin : Plugin() {
|
|
|
337
377
|
fun setFlashMode(call: PluginCall) {
|
|
338
378
|
val mode = call.getString("mode")
|
|
339
379
|
if (mode == null) {
|
|
340
|
-
call.reject("Flash mode must be provided")
|
|
380
|
+
call.reject("Flash mode must be provided", CameraError.INVALID_ARGUMENT)
|
|
341
381
|
return
|
|
342
382
|
}
|
|
343
383
|
|
|
344
384
|
val validModes = listOf("off", "on", "auto")
|
|
345
385
|
if (!validModes.contains(mode)) {
|
|
346
|
-
call.reject("Invalid flash mode. Must be one of: ${validModes.joinToString(", ")}")
|
|
386
|
+
call.reject("Invalid flash mode. Must be one of: ${validModes.joinToString(", ")}", CameraError.INVALID_ARGUMENT)
|
|
347
387
|
return
|
|
348
388
|
}
|
|
349
389
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
390
|
+
implementation.setFlashMode(mode) { error ->
|
|
391
|
+
if (error != null) {
|
|
392
|
+
call.reject("Failed to set flash mode: ${error.localizedMessage}", error.cameraErrorCode, error)
|
|
393
|
+
} else {
|
|
394
|
+
call.resolve()
|
|
395
|
+
}
|
|
355
396
|
}
|
|
356
397
|
}
|
|
357
398
|
|
|
@@ -366,14 +407,10 @@ class CameraViewPlugin : Plugin() {
|
|
|
366
407
|
|
|
367
408
|
@PluginMethod
|
|
368
409
|
fun getTorchMode(call: PluginCall) {
|
|
369
|
-
implementation.getTorchMode {
|
|
410
|
+
implementation.getTorchMode { state ->
|
|
370
411
|
call.resolve(JSObject().apply {
|
|
371
|
-
put("enabled", enabled)
|
|
372
|
-
put(
|
|
373
|
-
"level",
|
|
374
|
-
// Android always uses full intensity when enabled
|
|
375
|
-
if (enabled) 1.0f else 0.0f
|
|
376
|
-
)
|
|
412
|
+
put("enabled", state.enabled)
|
|
413
|
+
put("level", state.level)
|
|
377
414
|
})
|
|
378
415
|
}
|
|
379
416
|
}
|
|
@@ -382,13 +419,19 @@ class CameraViewPlugin : Plugin() {
|
|
|
382
419
|
fun setTorchMode(call: PluginCall) {
|
|
383
420
|
val enabled = call.getBoolean("enabled")
|
|
384
421
|
if (enabled == null) {
|
|
385
|
-
call.reject("Enabled parameter is required")
|
|
422
|
+
call.reject("Enabled parameter is required", CameraError.INVALID_ARGUMENT)
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
val level = call.getFloat("level")
|
|
427
|
+
if (level != null && (level < 0.0f || level > 1.0f)) {
|
|
428
|
+
call.reject("Level must be between 0.0 and 1.0", CameraError.INVALID_ARGUMENT)
|
|
386
429
|
return
|
|
387
430
|
}
|
|
388
431
|
|
|
389
|
-
implementation.setTorchMode(enabled) { error ->
|
|
432
|
+
implementation.setTorchMode(enabled, level) { error ->
|
|
390
433
|
if (error != null) {
|
|
391
|
-
call.reject("Failed to set torch mode: ${error.localizedMessage}", error)
|
|
434
|
+
call.reject("Failed to set torch mode: ${error.localizedMessage}", error.cameraErrorCode, error)
|
|
392
435
|
} else {
|
|
393
436
|
call.resolve()
|
|
394
437
|
}
|
|
@@ -396,9 +439,10 @@ class CameraViewPlugin : Plugin() {
|
|
|
396
439
|
}
|
|
397
440
|
|
|
398
441
|
/**
|
|
399
|
-
*
|
|
442
|
+
* Notifies JS listeners of a barcode detection collected from
|
|
443
|
+
* [CameraView.barcodeEvents].
|
|
400
444
|
*/
|
|
401
|
-
fun notifyBarcodeDetected(result: BarcodeDetectionResult) {
|
|
445
|
+
private fun notifyBarcodeDetected(result: BarcodeDetectionResult) {
|
|
402
446
|
val rawBytesArray = JSArray().apply {
|
|
403
447
|
result.rawBytes.forEach { put(it.toInt() and 0xFF) }
|
|
404
448
|
}
|
package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/BarcodeDetectionResult.kt
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
package com.michaelwolz.capacitorcameraview.model
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Barcode detection result containing the value, format type, and
|
|
4
|
+
* Barcode detection result containing the value, format type, and the bounding
|
|
5
|
+
* rectangle in display/CSS pixels within the webview coordinate space.
|
|
6
|
+
*
|
|
7
|
+
* Overrides `equals`/`hashCode` explicitly because [rawBytes] is an array: the
|
|
8
|
+
* data-class-generated versions would otherwise compare/hash it by reference identity
|
|
9
|
+
* rather than content, silently breaking equality (and any hash-based collection use)
|
|
10
|
+
* for two results with the same content but different array instances.
|
|
5
11
|
*/
|
|
6
12
|
data class BarcodeDetectionResult(
|
|
7
13
|
val value: String,
|
|
@@ -9,4 +15,24 @@ data class BarcodeDetectionResult(
|
|
|
9
15
|
val displayValue: String,
|
|
10
16
|
val type: String,
|
|
11
17
|
val boundingRect: WebBoundingRect
|
|
12
|
-
)
|
|
18
|
+
) {
|
|
19
|
+
override fun equals(other: Any?): Boolean {
|
|
20
|
+
if (this === other) return true
|
|
21
|
+
if (other !is BarcodeDetectionResult) return false
|
|
22
|
+
|
|
23
|
+
return value == other.value &&
|
|
24
|
+
rawBytes.contentEquals(other.rawBytes) &&
|
|
25
|
+
displayValue == other.displayValue &&
|
|
26
|
+
type == other.type &&
|
|
27
|
+
boundingRect == other.boundingRect
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override fun hashCode(): Int {
|
|
31
|
+
var result = value.hashCode()
|
|
32
|
+
result = 31 * result + rawBytes.contentHashCode()
|
|
33
|
+
result = 31 * result + displayValue.hashCode()
|
|
34
|
+
result = 31 * result + type.hashCode()
|
|
35
|
+
result = 31 * result + boundingRect.hashCode()
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -6,9 +6,14 @@ package com.michaelwolz.capacitorcameraview.model
|
|
|
6
6
|
* @property id Unique identifier for the camera
|
|
7
7
|
* @property name Human-readable name of the camera
|
|
8
8
|
* @property position Position of the camera ("front" or "back")
|
|
9
|
+
* @property deviceType Lens type ("wideAngle", "ultraWide", or "telephoto") derived from
|
|
10
|
+
* camera characteristics where derivable, matching the
|
|
11
|
+
* iOS-populated field of the same name. `null` when it can't be
|
|
12
|
+
* determined (e.g. focal length/sensor size not reported).
|
|
9
13
|
*/
|
|
10
14
|
data class CameraDevice(
|
|
11
15
|
val id: String,
|
|
12
16
|
val name: String,
|
|
13
|
-
val position: String
|
|
17
|
+
val position: String,
|
|
18
|
+
val deviceType: String? = null
|
|
14
19
|
)
|
|
@@ -9,12 +9,26 @@ package com.michaelwolz.capacitorcameraview.model
|
|
|
9
9
|
* If null, all supported formats are detected.
|
|
10
10
|
* @property position Camera position to use ("front" or "back").
|
|
11
11
|
* @property zoomFactor Initial zoom factor.
|
|
12
|
+
* @property aspectRatio Desired sensor aspect ratio ("4:3" or "16:9") applied to both
|
|
13
|
+
* the preview and photo capture. If null, the long-standing
|
|
14
|
+
* defaults are kept (16:9-with-fallback for capture, automatic
|
|
15
|
+
* preview resolution).
|
|
16
|
+
* @property captureMaxDimension Optional upper bound, in pixels, for the longer edge
|
|
17
|
+
* of captured photos. If null, the default capture
|
|
18
|
+
* resolution is kept.
|
|
19
|
+
* @property previewScaleMode How the preview is scaled into its container. "fit"
|
|
20
|
+
* letterboxes the whole frame (FIT_CENTER); any other
|
|
21
|
+
* value (including null) keeps the default cover
|
|
22
|
+
* behavior (FILL_CENTER).
|
|
12
23
|
*/
|
|
13
24
|
data class CameraSessionConfiguration(
|
|
14
25
|
val deviceId: String? = null,
|
|
15
26
|
val enableBarcodeDetection: Boolean = false,
|
|
16
27
|
val barcodeTypes: List<Int>? = null,
|
|
17
28
|
val position: String = "back",
|
|
18
|
-
val zoomFactor: Float = 1.0f
|
|
29
|
+
val zoomFactor: Float = 1.0f,
|
|
30
|
+
val aspectRatio: String? = null,
|
|
31
|
+
val captureMaxDimension: Int? = null,
|
|
32
|
+
val previewScaleMode: String? = null
|
|
19
33
|
)
|
|
20
34
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package com.michaelwolz.capacitorcameraview.model
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents the current torch (flashlight) mode and intensity.
|
|
5
|
+
*
|
|
6
|
+
* @property enabled Whether the torch is currently enabled.
|
|
7
|
+
* @property level The current torch intensity level, normalized to 0.0-1.0. Only meaningful
|
|
8
|
+
* as a continuous value on API 33+ devices with multi-level torch hardware - below API 33, or
|
|
9
|
+
* on single-level hardware, the torch is binary and this is always 1.0 when enabled and 0.0
|
|
10
|
+
* when off.
|
|
11
|
+
*/
|
|
12
|
+
data class TorchModeState(
|
|
13
|
+
val enabled: Boolean,
|
|
14
|
+
val level: Float
|
|
15
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
package com.michaelwolz.capacitorcameraview.model
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** Rectangle for barcode bounds in display/CSS pixels within the webview coordinate space. */
|
|
4
4
|
data class WebBoundingRect(
|
|
5
5
|
/** Top left x coordinate of the rectangle. */
|
|
6
6
|
val x: Float,
|
|
@@ -5,9 +5,11 @@ import android.graphics.BitmapFactory
|
|
|
5
5
|
import android.graphics.Matrix
|
|
6
6
|
import android.graphics.Rect
|
|
7
7
|
import android.util.Base64
|
|
8
|
+
import android.util.Base64OutputStream
|
|
8
9
|
import android.view.Surface
|
|
9
10
|
import android.view.View
|
|
10
11
|
import android.view.ViewGroup.MarginLayoutParams
|
|
12
|
+
import androidx.camera.core.CameraSelector
|
|
11
13
|
import androidx.camera.core.ImageProxy
|
|
12
14
|
import androidx.camera.view.PreviewView
|
|
13
15
|
import com.getcapacitor.PluginCall
|
|
@@ -15,7 +17,9 @@ import com.google.mlkit.vision.barcode.common.Barcode
|
|
|
15
17
|
import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
|
|
16
18
|
import com.michaelwolz.capacitorcameraview.model.VideoRecordingQuality
|
|
17
19
|
import com.michaelwolz.capacitorcameraview.model.WebBoundingRect
|
|
20
|
+
import kotlinx.coroutines.CancellableContinuation
|
|
18
21
|
import java.io.ByteArrayOutputStream
|
|
22
|
+
import kotlin.coroutines.resume
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* Memory-efficient Base64 encoding utilities.
|
|
@@ -31,7 +35,12 @@ object StreamingBase64Encoder {
|
|
|
31
35
|
|
|
32
36
|
/**
|
|
33
37
|
* Encodes a bitmap to Base64 with memory optimization.
|
|
34
|
-
* Reuses ByteArrayOutputStream
|
|
38
|
+
* Reuses a pooled [ByteArrayOutputStream] and streams the compressed bytes straight
|
|
39
|
+
* through a [Base64OutputStream] into it, instead of collecting the raw (un-encoded)
|
|
40
|
+
* bytes first and then Base64-encoding them as a separate pass. This avoids ever
|
|
41
|
+
* holding a full-size raw buffer and a full-size Base64 buffer at the same time,
|
|
42
|
+
* roughly halving peak memory versus a two-step compress-then-`Base64.encodeToString`
|
|
43
|
+
* approach.
|
|
35
44
|
*
|
|
36
45
|
* @param bitmap The bitmap to encode
|
|
37
46
|
* @param quality JPEG compression quality (0-100)
|
|
@@ -46,10 +55,16 @@ object StreamingBase64Encoder {
|
|
|
46
55
|
val outputStream = outputStreamPool.get()!!
|
|
47
56
|
outputStream.reset() // Clear previous data
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
// Base64OutputStream.close() finalizes any trailing padding and then closes the
|
|
59
|
+
// wrapped stream; closing a ByteArrayOutputStream is a documented no-op, so the
|
|
60
|
+
// pooled buffer and its contents remain valid (and reusable) afterwards.
|
|
61
|
+
Base64OutputStream(outputStream, Base64.NO_WRAP).use { base64Stream ->
|
|
62
|
+
bitmap.compress(format, quality, base64Stream)
|
|
63
|
+
}
|
|
51
64
|
|
|
52
|
-
|
|
65
|
+
// The Base64 alphabet is pure ASCII, so this is a lossless decode of the bytes
|
|
66
|
+
// already sitting in the pooled buffer.
|
|
67
|
+
return String(outputStream.toByteArray(), Charsets.US_ASCII)
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
/**
|
|
@@ -72,10 +87,10 @@ fun getBarcodeFormatString(format: Int): String {
|
|
|
72
87
|
Barcode.FORMAT_DATA_MATRIX -> "dataMatrix"
|
|
73
88
|
Barcode.FORMAT_EAN_8 -> "ean8"
|
|
74
89
|
Barcode.FORMAT_EAN_13 -> "ean13"
|
|
75
|
-
Barcode.FORMAT_ITF -> "
|
|
90
|
+
Barcode.FORMAT_ITF -> "itf14"
|
|
76
91
|
Barcode.FORMAT_PDF417 -> "pdf417"
|
|
77
92
|
Barcode.FORMAT_UPC_A -> "upcA"
|
|
78
|
-
Barcode.FORMAT_UPC_E -> "
|
|
93
|
+
Barcode.FORMAT_UPC_E -> "upce"
|
|
79
94
|
else -> "unknown"
|
|
80
95
|
}
|
|
81
96
|
}
|
|
@@ -180,25 +195,43 @@ fun sessionConfigFromPluginCall(call: PluginCall): CameraSessionConfiguration {
|
|
|
180
195
|
enableBarcodeDetection = call.getBoolean("enableBarcodeDetection") ?: false,
|
|
181
196
|
barcodeTypes = barcodeTypes,
|
|
182
197
|
position = call.getString("position") ?: "back",
|
|
183
|
-
zoomFactor = call.getFloat("zoomFactor") ?: 1.0f
|
|
198
|
+
zoomFactor = call.getFloat("zoomFactor") ?: 1.0f,
|
|
199
|
+
aspectRatio = call.getString("aspectRatio"),
|
|
200
|
+
captureMaxDimension = call.getInt("captureMaxDimension"),
|
|
201
|
+
previewScaleMode = call.getString("previewScaleMode")
|
|
184
202
|
)
|
|
185
203
|
}
|
|
186
204
|
|
|
187
205
|
/**
|
|
188
|
-
*
|
|
206
|
+
* Whether the given CameraX lens-facing value denotes a front-facing camera.
|
|
207
|
+
*
|
|
208
|
+
* Deriving facing from the bound camera's lens facing is the only reliable signal:
|
|
209
|
+
* comparing the current [CameraSelector] against [CameraSelector.DEFAULT_FRONT_CAMERA] is
|
|
210
|
+
* wrong for any custom selector built from a `deviceId`.
|
|
211
|
+
*
|
|
212
|
+
* @param lensFacing A `CameraSelector.LENS_FACING_*` value, or `null` if not yet known.
|
|
213
|
+
* @return `true` only when [lensFacing] is [CameraSelector.LENS_FACING_FRONT].
|
|
214
|
+
*/
|
|
215
|
+
fun isLensFacingFront(lensFacing: Int?): Boolean {
|
|
216
|
+
return lensFacing == CameraSelector.LENS_FACING_FRONT
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Calculates the clockwise rotation (in degrees) to apply to a still capture so it is
|
|
221
|
+
* upright for the given display orientation.
|
|
189
222
|
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
223
|
+
* The base value is the frame's own [imageRotationDegrees] rather than a fixed
|
|
224
|
+
* sensor-derived value, so devices whose HAL returns pre-rotated buffers report `0` here
|
|
225
|
+
* and are not rotated a second time.
|
|
193
226
|
*
|
|
194
|
-
* @param
|
|
195
|
-
* @param
|
|
196
|
-
* @param isFrontFacing Whether the camera is front-facing
|
|
197
|
-
* @return The calculated image orientation in degrees.
|
|
227
|
+
* @param imageRotationDegrees The captured frame's `ImageInfo.rotationDegrees` (0/90/180/270).
|
|
228
|
+
* @param displayRotation The current display rotation (`Surface.ROTATION_*`: 0, 1, 2, or 3).
|
|
229
|
+
* @param isFrontFacing Whether the active camera is front-facing.
|
|
230
|
+
* @return The calculated image orientation in degrees (0, 90, 180, or 270).
|
|
198
231
|
*/
|
|
199
|
-
fun
|
|
232
|
+
fun calculateImageRotation(
|
|
233
|
+
imageRotationDegrees: Int,
|
|
200
234
|
displayRotation: Int,
|
|
201
|
-
sensorRotationDegrees: Int,
|
|
202
235
|
isFrontFacing: Boolean
|
|
203
236
|
): Int {
|
|
204
237
|
val surfaceRotationDegrees = when (displayRotation) {
|
|
@@ -210,9 +243,9 @@ fun calculateImageRotationBasedOnDisplayRotation(
|
|
|
210
243
|
}
|
|
211
244
|
|
|
212
245
|
return if (isFrontFacing) {
|
|
213
|
-
(
|
|
246
|
+
(imageRotationDegrees + surfaceRotationDegrees) % 360
|
|
214
247
|
} else {
|
|
215
|
-
(
|
|
248
|
+
(imageRotationDegrees - surfaceRotationDegrees + 360) % 360
|
|
216
249
|
}
|
|
217
250
|
}
|
|
218
251
|
|
|
@@ -253,6 +286,27 @@ fun imageProxyToBase64(image: ImageProxy, quality: Int, rotationDegrees: Int): S
|
|
|
253
286
|
}
|
|
254
287
|
}
|
|
255
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Resumes this continuation with [value], but only if it hasn't already been resumed or
|
|
291
|
+
* cancelled. The camera/capture callbacks that resume continuations in this plugin run on
|
|
292
|
+
* the CameraX/ML Kit executor or a main-thread [android.os.Handler.post] callback - never on
|
|
293
|
+
* the coroutine's own dispatcher - so a resume can race with the caller cancelling the
|
|
294
|
+
* coroutine (e.g. because the plugin is being torn down mid-capture).
|
|
295
|
+
*
|
|
296
|
+
* The atomic `tryResume`/`completeResume` pair is `@InternalCoroutinesApi`, so this checks
|
|
297
|
+
* [CancellableContinuation.isActive] first and swallows the `IllegalStateException` a plain
|
|
298
|
+
* `resume()` throws if cancellation still wins the race.
|
|
299
|
+
*/
|
|
300
|
+
fun <T> CancellableContinuation<T>.resumeIfActive(value: T) {
|
|
301
|
+
if (!isActive) return
|
|
302
|
+
try {
|
|
303
|
+
resume(value)
|
|
304
|
+
} catch (e: IllegalStateException) {
|
|
305
|
+
// Lost the race with cancellation between the isActive check and resume() -
|
|
306
|
+
// nobody is awaiting the result anymore, so there's nothing to recover.
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
256
310
|
/**
|
|
257
311
|
* Parses a string representation of video recording quality into a [VideoRecordingQuality] enum.
|
|
258
312
|
*/
|