@capgo/camera-preview 7.21.8 → 7.21.10
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.
|
@@ -239,11 +239,11 @@ public class CameraPreview
|
|
|
239
239
|
// Prevent starting while an existing view is still active or stopping
|
|
240
240
|
if (cameraXView != null) {
|
|
241
241
|
try {
|
|
242
|
-
if (cameraXView.isRunning()) {
|
|
242
|
+
if (cameraXView.isRunning() && !cameraXView.isStopping()) {
|
|
243
243
|
call.reject("Camera is already running");
|
|
244
244
|
return;
|
|
245
245
|
}
|
|
246
|
-
if (cameraXView.isBusy()) {
|
|
246
|
+
if (cameraXView.isStopping() || cameraXView.isBusy()) {
|
|
247
247
|
if (enqueuePendingStart(call)) {
|
|
248
248
|
Log.d(
|
|
249
249
|
TAG,
|
|
@@ -445,15 +445,11 @@ public class CameraPreview
|
|
|
445
445
|
}
|
|
446
446
|
|
|
447
447
|
if (cameraXView != null) {
|
|
448
|
-
boolean willDefer = false;
|
|
449
|
-
try {
|
|
450
|
-
willDefer = cameraXView.isCapturing();
|
|
451
|
-
} catch (Exception ignored) {}
|
|
452
448
|
if (cameraXView.isRunning()) {
|
|
453
449
|
cameraXView.stopSession();
|
|
454
450
|
}
|
|
455
451
|
// Only drop the reference if no deferred stop is pending
|
|
456
|
-
if (!
|
|
452
|
+
if (!cameraXView.isStopDeferred()) {
|
|
457
453
|
cameraXView = null;
|
|
458
454
|
}
|
|
459
455
|
}
|
|
@@ -203,6 +203,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
public boolean isStopDeferred() {
|
|
207
|
+
synchronized (operationLock) {
|
|
208
|
+
return stopPending && activeOperations > 0;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public boolean isStopping() {
|
|
213
|
+
synchronized (operationLock) {
|
|
214
|
+
return stopPending;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
206
218
|
public CameraXView(Context context, WebView webView) {
|
|
207
219
|
this.context = context;
|
|
208
220
|
this.webView = webView;
|
|
@@ -1174,11 +1186,19 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1174
1186
|
final boolean embedTimestamp,
|
|
1175
1187
|
final boolean embedLocation
|
|
1176
1188
|
) {
|
|
1189
|
+
if (imageCapture == null) {
|
|
1190
|
+
if (listener != null) {
|
|
1191
|
+
listener.onPictureTakenError("Camera not ready");
|
|
1192
|
+
}
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1177
1196
|
// Prevent capture if a stop is pending
|
|
1178
1197
|
if (IsOperationRunning("capturePhoto")) {
|
|
1179
1198
|
Log.d(TAG, "capturePhoto: Ignored because stop is pending");
|
|
1180
1199
|
return;
|
|
1181
1200
|
}
|
|
1201
|
+
|
|
1182
1202
|
Log.d(
|
|
1183
1203
|
TAG,
|
|
1184
1204
|
"capturePhoto: Starting photo capture with: " +
|
|
@@ -1195,224 +1215,239 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1195
1215
|
embedLocation
|
|
1196
1216
|
);
|
|
1197
1217
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1218
|
+
boolean dispatched = false;
|
|
1219
|
+
try {
|
|
1220
|
+
synchronized (captureLock) {
|
|
1221
|
+
isCapturingPhoto = true;
|
|
1201
1222
|
}
|
|
1202
|
-
return;
|
|
1203
|
-
}
|
|
1204
1223
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
ImageCapture.OutputFileOptions outputFileOptions =
|
|
1215
|
-
new ImageCapture.OutputFileOptions.Builder(imageStream)
|
|
1216
|
-
.setMetadata(metadata)
|
|
1217
|
-
.build();
|
|
1224
|
+
final ByteArrayOutputStream imageStream = new ByteArrayOutputStream();
|
|
1225
|
+
ImageCapture.Metadata metadata = new ImageCapture.Metadata();
|
|
1226
|
+
if (location != null) {
|
|
1227
|
+
metadata.setLocation(location);
|
|
1228
|
+
}
|
|
1229
|
+
ImageCapture.OutputFileOptions outputFileOptions =
|
|
1230
|
+
new ImageCapture.OutputFileOptions.Builder(imageStream)
|
|
1231
|
+
.setMetadata(metadata)
|
|
1232
|
+
.build();
|
|
1218
1233
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
}
|
|
1231
|
-
// End of capture lifecycle
|
|
1232
|
-
synchronized (captureLock) {
|
|
1233
|
-
isCapturingPhoto = false;
|
|
1234
|
-
if (stopRequested) {
|
|
1235
|
-
performImmediateStop();
|
|
1234
|
+
imageCapture.takePicture(
|
|
1235
|
+
outputFileOptions,
|
|
1236
|
+
cameraExecutor,
|
|
1237
|
+
new ImageCapture.OnImageSavedCallback() {
|
|
1238
|
+
@Override
|
|
1239
|
+
public void onError(@NonNull ImageCaptureException exception) {
|
|
1240
|
+
Log.e(TAG, "capturePhoto: Photo capture failed", exception);
|
|
1241
|
+
if (listener != null) {
|
|
1242
|
+
listener.onPictureTakenError(
|
|
1243
|
+
"Photo capture failed: " + exception.getMessage()
|
|
1244
|
+
);
|
|
1236
1245
|
}
|
|
1246
|
+
// End of capture lifecycle
|
|
1247
|
+
synchronized (captureLock) {
|
|
1248
|
+
isCapturingPhoto = false;
|
|
1249
|
+
if (stopRequested) {
|
|
1250
|
+
performImmediateStop();
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
endOperation("capturePhoto");
|
|
1237
1254
|
}
|
|
1238
|
-
endOperation("capturePhoto");
|
|
1239
|
-
}
|
|
1240
1255
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
);
|
|
1255
|
-
// Build EXIF JSON from captured bytes (location applied by metadata if provided)
|
|
1256
|
-
JSONObject exifData = getExifData(exifInterface);
|
|
1257
|
-
|
|
1258
|
-
if (width != null || height != null) {
|
|
1259
|
-
Bitmap bitmap = BitmapFactory.decodeByteArray(
|
|
1260
|
-
originalCaptureBytes,
|
|
1261
|
-
0,
|
|
1262
|
-
originalCaptureBytes.length
|
|
1263
|
-
);
|
|
1264
|
-
bitmap = applyExifOrientation(bitmap, exifInterface);
|
|
1265
|
-
Bitmap resizedBitmap = resizeBitmapToMaxDimensions(
|
|
1266
|
-
bitmap,
|
|
1267
|
-
width,
|
|
1268
|
-
height
|
|
1256
|
+
@Override
|
|
1257
|
+
public void onImageSaved(
|
|
1258
|
+
@NonNull ImageCapture.OutputFileResults output
|
|
1259
|
+
) {
|
|
1260
|
+
try {
|
|
1261
|
+
byte[] originalCaptureBytes = imageStream.toByteArray();
|
|
1262
|
+
byte[] bytes = originalCaptureBytes; // will be replaced if we transform
|
|
1263
|
+
int finalWidthOut = -1;
|
|
1264
|
+
int finalHeightOut = -1;
|
|
1265
|
+
boolean transformedPixels = false;
|
|
1266
|
+
|
|
1267
|
+
ExifInterface exifInterface = new ExifInterface(
|
|
1268
|
+
new ByteArrayInputStream(originalCaptureBytes)
|
|
1269
1269
|
);
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1270
|
+
// Build EXIF JSON from captured bytes (location applied by metadata if provided)
|
|
1271
|
+
JSONObject exifData = getExifData(exifInterface);
|
|
1272
|
+
|
|
1273
|
+
if (width != null || height != null) {
|
|
1274
|
+
Bitmap bitmap = BitmapFactory.decodeByteArray(
|
|
1275
|
+
originalCaptureBytes,
|
|
1276
|
+
0,
|
|
1277
|
+
originalCaptureBytes.length
|
|
1276
1278
|
);
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
stream
|
|
1283
|
-
);
|
|
1284
|
-
bytes = stream.toByteArray();
|
|
1285
|
-
transformedPixels = true;
|
|
1286
|
-
|
|
1287
|
-
// Update EXIF JSON to reflect new dimensions; no in-place EXIF write to bytes
|
|
1288
|
-
try {
|
|
1289
|
-
exifData.put("PixelXDimension", resizedBitmap.getWidth());
|
|
1290
|
-
exifData.put("PixelYDimension", resizedBitmap.getHeight());
|
|
1291
|
-
exifData.put("ImageWidth", resizedBitmap.getWidth());
|
|
1292
|
-
exifData.put("ImageLength", resizedBitmap.getHeight());
|
|
1293
|
-
exifData.put(
|
|
1294
|
-
"Orientation",
|
|
1295
|
-
Integer.toString(ExifInterface.ORIENTATION_NORMAL)
|
|
1279
|
+
bitmap = applyExifOrientation(bitmap, exifInterface);
|
|
1280
|
+
Bitmap resizedBitmap = resizeBitmapToMaxDimensions(
|
|
1281
|
+
bitmap,
|
|
1282
|
+
width,
|
|
1283
|
+
height
|
|
1296
1284
|
);
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
);
|
|
1311
|
-
Bitmap previewCropped = cropBitmapToMatchPreview(originalBitmap);
|
|
1312
|
-
if (embedTimestamp || embedLocation) {
|
|
1313
|
-
previewCropped = drawTimestampAndLocationOntoBitmap(
|
|
1314
|
-
previewCropped,
|
|
1315
|
-
exifInterface,
|
|
1316
|
-
embedTimestamp,
|
|
1317
|
-
embedLocation
|
|
1285
|
+
if (embedTimestamp || embedLocation) {
|
|
1286
|
+
resizedBitmap = drawTimestampAndLocationOntoBitmap(
|
|
1287
|
+
resizedBitmap,
|
|
1288
|
+
exifInterface,
|
|
1289
|
+
embedTimestamp,
|
|
1290
|
+
embedLocation
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
1294
|
+
resizedBitmap.compress(
|
|
1295
|
+
Bitmap.CompressFormat.JPEG,
|
|
1296
|
+
quality,
|
|
1297
|
+
stream
|
|
1318
1298
|
);
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1299
|
+
bytes = stream.toByteArray();
|
|
1300
|
+
transformedPixels = true;
|
|
1301
|
+
|
|
1302
|
+
// Update EXIF JSON to reflect new dimensions; no in-place EXIF write to bytes
|
|
1303
|
+
try {
|
|
1304
|
+
exifData.put("PixelXDimension", resizedBitmap.getWidth());
|
|
1305
|
+
exifData.put("PixelYDimension", resizedBitmap.getHeight());
|
|
1306
|
+
exifData.put("ImageWidth", resizedBitmap.getWidth());
|
|
1307
|
+
exifData.put("ImageLength", resizedBitmap.getHeight());
|
|
1308
|
+
exifData.put(
|
|
1309
|
+
"Orientation",
|
|
1310
|
+
Integer.toString(ExifInterface.ORIENTATION_NORMAL)
|
|
1311
|
+
);
|
|
1312
|
+
} catch (Exception ignore) {}
|
|
1313
|
+
finalWidthOut = resizedBitmap.getWidth();
|
|
1314
|
+
finalHeightOut = resizedBitmap.getHeight();
|
|
1315
|
+
} else {
|
|
1316
|
+
// No explicit size/ratio: crop to match current preview content
|
|
1317
|
+
Bitmap originalBitmap = BitmapFactory.decodeByteArray(
|
|
1318
|
+
originalCaptureBytes,
|
|
1319
|
+
0,
|
|
1320
|
+
originalCaptureBytes.length
|
|
1337
1321
|
);
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1322
|
+
originalBitmap = applyExifOrientation(
|
|
1323
|
+
originalBitmap,
|
|
1324
|
+
exifInterface
|
|
1325
|
+
);
|
|
1326
|
+
Bitmap previewCropped = cropBitmapToMatchPreview(
|
|
1327
|
+
originalBitmap
|
|
1328
|
+
);
|
|
1329
|
+
if (embedTimestamp || embedLocation) {
|
|
1330
|
+
previewCropped = drawTimestampAndLocationOntoBitmap(
|
|
1331
|
+
previewCropped,
|
|
1332
|
+
exifInterface,
|
|
1333
|
+
embedTimestamp,
|
|
1334
|
+
embedLocation
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
1338
|
+
previewCropped.compress(
|
|
1339
|
+
Bitmap.CompressFormat.JPEG,
|
|
1340
|
+
quality,
|
|
1341
|
+
stream
|
|
1342
|
+
);
|
|
1343
|
+
bytes = stream.toByteArray();
|
|
1344
|
+
transformedPixels = true;
|
|
1345
|
+
// Update EXIF JSON to reflect cropped dimensions; no in-place EXIF write to bytes
|
|
1346
|
+
try {
|
|
1347
|
+
exifData.put("PixelXDimension", previewCropped.getWidth());
|
|
1348
|
+
exifData.put("PixelYDimension", previewCropped.getHeight());
|
|
1349
|
+
exifData.put("ImageWidth", previewCropped.getWidth());
|
|
1350
|
+
exifData.put("ImageLength", previewCropped.getHeight());
|
|
1351
|
+
exifData.put(
|
|
1352
|
+
"Orientation",
|
|
1353
|
+
Integer.toString(ExifInterface.ORIENTATION_NORMAL)
|
|
1354
|
+
);
|
|
1355
|
+
} catch (Exception ignore) {}
|
|
1356
|
+
finalWidthOut = previewCropped.getWidth();
|
|
1357
|
+
finalHeightOut = previewCropped.getHeight();
|
|
1358
|
+
}
|
|
1342
1359
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1360
|
+
// After any transform, inject EXIF back into the in-memory JPEG bytes (no temp file)
|
|
1361
|
+
if (transformedPixels) {
|
|
1362
|
+
Integer fW = (finalWidthOut > 0) ? finalWidthOut : null;
|
|
1363
|
+
Integer fH = (finalHeightOut > 0) ? finalHeightOut : null;
|
|
1364
|
+
bytes = injectExifInMemory(bytes, originalCaptureBytes, fW, fH);
|
|
1365
|
+
}
|
|
1349
1366
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1367
|
+
// Save to gallery asynchronously if requested, copy EXIF to file
|
|
1368
|
+
if (saveToGallery) {
|
|
1369
|
+
final byte[] finalBytes = bytes;
|
|
1370
|
+
final ExifInterface exifForFile = exifInterface;
|
|
1371
|
+
final Integer fW = (finalWidthOut > 0) ? finalWidthOut : null;
|
|
1372
|
+
final Integer fH = (finalHeightOut > 0) ? finalHeightOut : null;
|
|
1373
|
+
new Thread(() ->
|
|
1374
|
+
saveImageToGallery(finalBytes, exifForFile, fW, fH)
|
|
1375
|
+
).start();
|
|
1376
|
+
}
|
|
1360
1377
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1378
|
+
String resultValue;
|
|
1379
|
+
boolean returnFileUri =
|
|
1380
|
+
sessionConfig != null && sessionConfig.isStoreToFile();
|
|
1381
|
+
if (returnFileUri) {
|
|
1382
|
+
// Persist processed image to a file and return its URI to avoid heavy base64 bridging
|
|
1383
|
+
try {
|
|
1384
|
+
String fileName =
|
|
1385
|
+
"cpcp_" +
|
|
1386
|
+
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(
|
|
1387
|
+
new java.util.Date()
|
|
1388
|
+
) +
|
|
1389
|
+
".jpg";
|
|
1390
|
+
File outDir = context.getCacheDir();
|
|
1391
|
+
File outFile = new File(outDir, fileName);
|
|
1392
|
+
FileOutputStream outFos = new FileOutputStream(outFile);
|
|
1393
|
+
outFos.write(bytes);
|
|
1394
|
+
outFos.close();
|
|
1395
|
+
|
|
1396
|
+
// No EXIF rewrite here; bytes already contain EXIF when needed
|
|
1397
|
+
|
|
1398
|
+
// Return a file path; apps can convert via Capacitor.convertFileSrc on JS side
|
|
1399
|
+
resultValue = outFile.getAbsolutePath();
|
|
1400
|
+
} catch (IOException ioEx) {
|
|
1401
|
+
Log.e(TAG, "capturePhoto: Failed to write image file", ioEx);
|
|
1402
|
+
// Fallback to base64 if file write fails
|
|
1403
|
+
resultValue = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
1404
|
+
}
|
|
1405
|
+
} else {
|
|
1406
|
+
// Backward-compatible behavior
|
|
1386
1407
|
resultValue = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
1387
1408
|
}
|
|
1388
|
-
} else {
|
|
1389
|
-
// Backward-compatible behavior
|
|
1390
|
-
resultValue = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
1391
|
-
}
|
|
1392
1409
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1410
|
+
if (listener != null) {
|
|
1411
|
+
listener.onPictureTaken(resultValue, exifData);
|
|
1412
|
+
}
|
|
1413
|
+
} catch (Exception e) {
|
|
1414
|
+
Log.e(TAG, "capturePhoto: Error processing image", e);
|
|
1415
|
+
if (listener != null) {
|
|
1416
|
+
listener.onPictureTakenError(
|
|
1417
|
+
"Error processing image: " + e.getMessage()
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
} finally {
|
|
1421
|
+
// End of capture lifecycle
|
|
1422
|
+
synchronized (captureLock) {
|
|
1423
|
+
isCapturingPhoto = false;
|
|
1424
|
+
if (stopRequested) {
|
|
1425
|
+
performImmediateStop();
|
|
1426
|
+
}
|
|
1409
1427
|
}
|
|
1428
|
+
endOperation("capturePhoto");
|
|
1410
1429
|
}
|
|
1411
|
-
endOperation("capturePhoto");
|
|
1412
1430
|
}
|
|
1413
1431
|
}
|
|
1432
|
+
);
|
|
1433
|
+
|
|
1434
|
+
dispatched = true;
|
|
1435
|
+
} catch (Exception e) {
|
|
1436
|
+
Log.e(TAG, "capturePhoto: Failed to start photo capture", e);
|
|
1437
|
+
if (listener != null) {
|
|
1438
|
+
listener.onPictureTakenError("Photo capture failed: " + e.getMessage());
|
|
1414
1439
|
}
|
|
1415
|
-
|
|
1440
|
+
} finally {
|
|
1441
|
+
if (!dispatched) {
|
|
1442
|
+
synchronized (captureLock) {
|
|
1443
|
+
isCapturingPhoto = false;
|
|
1444
|
+
if (stopRequested) {
|
|
1445
|
+
performImmediateStop();
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
endOperation("capturePhoto");
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1416
1451
|
}
|
|
1417
1452
|
|
|
1418
1453
|
private Bitmap drawTimestampAndLocationOntoBitmap(
|
|
@@ -1970,6 +2005,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1970
2005
|
// we recompress JPEG in-memory and update EXIF info only in the returned JSON, not in the bytes.
|
|
1971
2006
|
|
|
1972
2007
|
public void captureSample(int quality) {
|
|
2008
|
+
if (sampleImageCapture == null) {
|
|
2009
|
+
if (listener != null) {
|
|
2010
|
+
listener.onSampleTakenError("Camera not ready");
|
|
2011
|
+
}
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
1973
2015
|
if (IsOperationRunning("captureSample")) {
|
|
1974
2016
|
Log.d(TAG, "captureSample: Ignored because stop is pending");
|
|
1975
2017
|
return;
|
|
@@ -1979,52 +2021,59 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1979
2021
|
"captureSample: Starting sample capture with quality: " + quality
|
|
1980
2022
|
);
|
|
1981
2023
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
cameraExecutor,
|
|
1991
|
-
new ImageCapture.OnImageCapturedCallback() {
|
|
1992
|
-
@Override
|
|
1993
|
-
public void onError(@NonNull ImageCaptureException exception) {
|
|
1994
|
-
Log.e(TAG, "captureSample: Sample capture failed", exception);
|
|
1995
|
-
if (listener != null) {
|
|
1996
|
-
listener.onSampleTakenError(
|
|
1997
|
-
"Sample capture failed: " + exception.getMessage()
|
|
1998
|
-
);
|
|
1999
|
-
}
|
|
2000
|
-
endOperation("captureSample");
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
@Override
|
|
2004
|
-
public void onCaptureSuccess(@NonNull ImageProxy image) {
|
|
2005
|
-
//noinspection TryFinallyCanBeTryWithResources
|
|
2006
|
-
try {
|
|
2007
|
-
// Convert ImageProxy to byte array
|
|
2008
|
-
byte[] bytes = imageProxyToByteArray(image);
|
|
2009
|
-
String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
2010
|
-
|
|
2011
|
-
if (listener != null) {
|
|
2012
|
-
listener.onSampleTaken(base64);
|
|
2013
|
-
}
|
|
2014
|
-
} catch (Exception e) {
|
|
2015
|
-
Log.e(TAG, "captureSample: Error processing sample", e);
|
|
2024
|
+
boolean dispatched = false;
|
|
2025
|
+
try {
|
|
2026
|
+
sampleImageCapture.takePicture(
|
|
2027
|
+
cameraExecutor,
|
|
2028
|
+
new ImageCapture.OnImageCapturedCallback() {
|
|
2029
|
+
@Override
|
|
2030
|
+
public void onError(@NonNull ImageCaptureException exception) {
|
|
2031
|
+
Log.e(TAG, "captureSample: Sample capture failed", exception);
|
|
2016
2032
|
if (listener != null) {
|
|
2017
2033
|
listener.onSampleTakenError(
|
|
2018
|
-
"
|
|
2034
|
+
"Sample capture failed: " + exception.getMessage()
|
|
2019
2035
|
);
|
|
2020
2036
|
}
|
|
2021
|
-
} finally {
|
|
2022
|
-
image.close();
|
|
2023
2037
|
endOperation("captureSample");
|
|
2024
2038
|
}
|
|
2039
|
+
|
|
2040
|
+
@Override
|
|
2041
|
+
public void onCaptureSuccess(@NonNull ImageProxy image) {
|
|
2042
|
+
//noinspection TryFinallyCanBeTryWithResources
|
|
2043
|
+
try {
|
|
2044
|
+
// Convert ImageProxy to byte array
|
|
2045
|
+
byte[] bytes = imageProxyToByteArray(image);
|
|
2046
|
+
String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
2047
|
+
|
|
2048
|
+
if (listener != null) {
|
|
2049
|
+
listener.onSampleTaken(base64);
|
|
2050
|
+
}
|
|
2051
|
+
} catch (Exception e) {
|
|
2052
|
+
Log.e(TAG, "captureSample: Error processing sample", e);
|
|
2053
|
+
if (listener != null) {
|
|
2054
|
+
listener.onSampleTakenError(
|
|
2055
|
+
"Error processing sample: " + e.getMessage()
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
} finally {
|
|
2059
|
+
image.close();
|
|
2060
|
+
endOperation("captureSample");
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2025
2063
|
}
|
|
2064
|
+
);
|
|
2065
|
+
|
|
2066
|
+
dispatched = true;
|
|
2067
|
+
} catch (Exception e) {
|
|
2068
|
+
Log.e(TAG, "captureSample: Failed to start sample capture", e);
|
|
2069
|
+
if (listener != null) {
|
|
2070
|
+
listener.onSampleTakenError("Sample capture failed: " + e.getMessage());
|
|
2026
2071
|
}
|
|
2027
|
-
|
|
2072
|
+
} finally {
|
|
2073
|
+
if (!dispatched) {
|
|
2074
|
+
endOperation("captureSample");
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2028
2077
|
}
|
|
2029
2078
|
|
|
2030
2079
|
private byte[] imageProxyToByteArray(ImageProxy image) {
|
|
@@ -2377,18 +2426,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2377
2426
|
);
|
|
2378
2427
|
return;
|
|
2379
2428
|
}
|
|
2380
|
-
if (IsOperationRunning("setFocus")) {
|
|
2381
|
-
Log.d(TAG, "setFocus: Ignored because stop is pending");
|
|
2382
|
-
return;
|
|
2383
|
-
}
|
|
2384
|
-
if (camera == null) {
|
|
2385
|
-
throw new Exception("Camera not initialized");
|
|
2386
|
-
}
|
|
2387
|
-
|
|
2388
|
-
if (previewView == null) {
|
|
2389
|
-
throw new Exception("Preview view not initialized");
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
2429
|
// Validate that coordinates are within bounds (0-1 range)
|
|
2393
2430
|
if (x < 0f || x > 1f || y < 0f || y > 1f) {
|
|
2394
2431
|
Log.w(TAG, "setFocus: Coordinates out of bounds - x: " + x + ", y: " + y);
|
|
@@ -2426,21 +2463,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2426
2463
|
);
|
|
2427
2464
|
}
|
|
2428
2465
|
|
|
2429
|
-
// Only show focus indicator after validation passes
|
|
2430
|
-
float indicatorX = x * viewWidth;
|
|
2431
|
-
float indicatorY = y * viewHeight;
|
|
2432
|
-
final long indicatorToken;
|
|
2433
|
-
long indicatorToken1;
|
|
2434
|
-
try {
|
|
2435
|
-
indicatorToken1 = showFocusIndicator(indicatorX, indicatorY);
|
|
2436
|
-
} catch (Exception ignore) {
|
|
2437
|
-
// If we can't show the indicator (e.g., view is gone), still proceed with metering
|
|
2438
|
-
// Use current token so hide is a no-op later
|
|
2439
|
-
indicatorToken1 = focusIndicatorAnimationId;
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
// Create MeteringPoint using the preview view
|
|
2443
|
-
indicatorToken = indicatorToken1;
|
|
2444
2466
|
MeteringPointFactory factory = previewView.getMeteringPointFactory();
|
|
2445
2467
|
MeteringPoint point = factory.createPoint(x * viewWidth, y * viewHeight);
|
|
2446
2468
|
|
|
@@ -2452,16 +2474,34 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2452
2474
|
.disableAutoCancel()
|
|
2453
2475
|
.build();
|
|
2454
2476
|
|
|
2477
|
+
if (IsOperationRunning("setFocus")) {
|
|
2478
|
+
Log.d(TAG, "setFocus: Ignored because stop is pending");
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// Only show focus indicator after validation passes and operation is accepted
|
|
2483
|
+
float indicatorX = x * viewWidth;
|
|
2484
|
+
float indicatorY = y * viewHeight;
|
|
2485
|
+
long indicatorToken = focusIndicatorAnimationId;
|
|
2455
2486
|
try {
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2487
|
+
indicatorToken = showFocusIndicator(indicatorX, indicatorY);
|
|
2488
|
+
} catch (Exception ignore) {
|
|
2489
|
+
// If we can't show the indicator (e.g., view is gone), still proceed with metering
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
ListenableFuture<FocusMeteringResult> future = null;
|
|
2493
|
+
boolean dispatched = false;
|
|
2494
|
+
try {
|
|
2495
|
+
future = camera.getCameraControl().startFocusAndMetering(action);
|
|
2459
2496
|
currentFocusFuture = future;
|
|
2497
|
+
dispatched = true;
|
|
2460
2498
|
|
|
2499
|
+
final ListenableFuture<FocusMeteringResult> capturedFuture = future;
|
|
2500
|
+
final long tokenForListener = indicatorToken;
|
|
2461
2501
|
future.addListener(
|
|
2462
2502
|
() -> {
|
|
2463
2503
|
try {
|
|
2464
|
-
FocusMeteringResult result =
|
|
2504
|
+
FocusMeteringResult result = capturedFuture.get();
|
|
2465
2505
|
} catch (Exception e) {
|
|
2466
2506
|
// Handle cancellation gracefully - this is expected when rapid taps occur
|
|
2467
2507
|
if (
|
|
@@ -2483,10 +2523,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2483
2523
|
Log.e(TAG, "Error during focus: " + e.getMessage());
|
|
2484
2524
|
}
|
|
2485
2525
|
} finally {
|
|
2486
|
-
if (
|
|
2526
|
+
if (
|
|
2527
|
+
currentFocusFuture == capturedFuture &&
|
|
2528
|
+
currentFocusFuture.isDone()
|
|
2529
|
+
) {
|
|
2487
2530
|
currentFocusFuture = null;
|
|
2488
2531
|
}
|
|
2489
|
-
hideFocusIndicator(
|
|
2532
|
+
hideFocusIndicator(tokenForListener);
|
|
2490
2533
|
endOperation("setFocus");
|
|
2491
2534
|
}
|
|
2492
2535
|
},
|
|
@@ -2495,9 +2538,15 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2495
2538
|
} catch (Exception e) {
|
|
2496
2539
|
currentFocusFuture = null;
|
|
2497
2540
|
Log.e(TAG, "Failed to set focus: " + e.getMessage());
|
|
2498
|
-
hideFocusIndicator(indicatorToken);
|
|
2499
|
-
endOperation("setFocus");
|
|
2500
2541
|
throw e;
|
|
2542
|
+
} finally {
|
|
2543
|
+
if (!dispatched) {
|
|
2544
|
+
if (currentFocusFuture == future) {
|
|
2545
|
+
currentFocusFuture = null;
|
|
2546
|
+
}
|
|
2547
|
+
hideFocusIndicator(indicatorToken);
|
|
2548
|
+
endOperation("setFocus");
|
|
2549
|
+
}
|
|
2501
2550
|
}
|
|
2502
2551
|
}
|
|
2503
2552
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/camera-preview",
|
|
3
|
-
"version": "7.21.
|
|
3
|
+
"version": "7.21.10",
|
|
4
4
|
"description": "Camera preview",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"url": "https://github.com/Cap-go/capacitor-camera-preview"
|
|
9
9
|
},
|
|
10
10
|
"bugs": {
|
|
11
|
-
"url": "https://github.com/Cap-go/camera-preview/issues"
|
|
11
|
+
"url": "https://github.com/Cap-go/capacitor-camera-preview/issues"
|
|
12
12
|
},
|
|
13
13
|
"author": "Martin Donadieu <martin@capgo.app>",
|
|
14
14
|
"keywords": [
|