@capgo/camera-preview 7.14.8 → 7.16.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/README.md +13 -1
- package/android/build.gradle +3 -2
- package/android/src/main/java/{com/ahm → app/capgo}/capacitor/camera/preview/CameraPreview.java +74 -47
- package/android/src/main/java/{com/ahm → app/capgo}/capacitor/camera/preview/CameraXView.java +586 -628
- package/android/src/main/java/{com/ahm → app/capgo}/capacitor/camera/preview/GridOverlayView.java +3 -2
- package/android/src/main/java/{com/ahm → app/capgo}/capacitor/camera/preview/model/CameraDevice.java +1 -1
- package/android/src/main/java/{com/ahm → app/capgo}/capacitor/camera/preview/model/CameraSessionConfiguration.java +1 -12
- package/android/src/main/java/{com/ahm → app/capgo}/capacitor/camera/preview/model/LensInfo.java +1 -1
- package/android/src/main/java/{com/ahm → app/capgo}/capacitor/camera/preview/model/ZoomFactors.java +1 -1
- package/android/src/main/res/layout/camera_activity.xml +1 -1
- package/dist/docs.json +1 -17
- package/dist/esm/definitions.d.ts +3 -6
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift +0 -22
- package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +1 -5
- package/package.json +1 -1
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +0 -79
package/android/src/main/java/{com/ahm → app/capgo}/capacitor/camera/preview/CameraXView.java
RENAMED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
package
|
|
2
|
-
|
|
3
|
-
import static androidx.core.content.ContextCompat.getSystemService;
|
|
1
|
+
package app.capgo.capacitor.camera.preview;
|
|
4
2
|
|
|
3
|
+
import android.Manifest;
|
|
5
4
|
import android.content.Context;
|
|
5
|
+
import android.content.pm.PackageManager;
|
|
6
6
|
import android.content.res.Configuration;
|
|
7
7
|
import android.graphics.Bitmap;
|
|
8
8
|
import android.graphics.BitmapFactory;
|
|
9
9
|
import android.graphics.Color;
|
|
10
|
+
import android.graphics.Matrix;
|
|
10
11
|
import android.graphics.Rect;
|
|
11
12
|
import android.graphics.drawable.GradientDrawable;
|
|
12
13
|
import android.hardware.camera2.CameraAccessException;
|
|
@@ -23,7 +24,6 @@ import android.util.Log;
|
|
|
23
24
|
import android.util.Range;
|
|
24
25
|
import android.util.Rational;
|
|
25
26
|
import android.util.Size;
|
|
26
|
-
import android.view.MotionEvent;
|
|
27
27
|
import android.view.View;
|
|
28
28
|
import android.view.ViewGroup;
|
|
29
29
|
import android.view.WindowManager;
|
|
@@ -32,7 +32,6 @@ import android.webkit.WebView;
|
|
|
32
32
|
import android.widget.FrameLayout;
|
|
33
33
|
import androidx.annotation.NonNull;
|
|
34
34
|
import androidx.annotation.OptIn;
|
|
35
|
-
import androidx.annotation.RequiresApi;
|
|
36
35
|
import androidx.camera.camera2.interop.Camera2CameraControl;
|
|
37
36
|
import androidx.camera.camera2.interop.Camera2CameraInfo;
|
|
38
37
|
import androidx.camera.camera2.interop.CaptureRequestOptions;
|
|
@@ -52,7 +51,6 @@ import androidx.camera.core.MeteringPointFactory;
|
|
|
52
51
|
import androidx.camera.core.Preview;
|
|
53
52
|
import androidx.camera.core.ResolutionInfo;
|
|
54
53
|
import androidx.camera.core.TorchState;
|
|
55
|
-
import androidx.camera.core.UseCase;
|
|
56
54
|
import androidx.camera.core.ZoomState;
|
|
57
55
|
import androidx.camera.core.resolutionselector.AspectRatioStrategy;
|
|
58
56
|
import androidx.camera.core.resolutionselector.ResolutionSelector;
|
|
@@ -67,16 +65,18 @@ import androidx.camera.video.Recording;
|
|
|
67
65
|
import androidx.camera.video.VideoCapture;
|
|
68
66
|
import androidx.camera.video.VideoRecordEvent;
|
|
69
67
|
import androidx.camera.view.PreviewView;
|
|
68
|
+
import androidx.core.app.ActivityCompat;
|
|
70
69
|
import androidx.core.content.ContextCompat;
|
|
71
70
|
import androidx.exifinterface.media.ExifInterface;
|
|
72
71
|
import androidx.lifecycle.Lifecycle;
|
|
73
72
|
import androidx.lifecycle.LifecycleObserver;
|
|
74
73
|
import androidx.lifecycle.LifecycleOwner;
|
|
75
74
|
import androidx.lifecycle.LifecycleRegistry;
|
|
76
|
-
import
|
|
77
|
-
import
|
|
78
|
-
import
|
|
75
|
+
import app.capgo.capacitor.camera.preview.model.CameraSessionConfiguration;
|
|
76
|
+
import app.capgo.capacitor.camera.preview.model.LensInfo;
|
|
77
|
+
import app.capgo.capacitor.camera.preview.model.ZoomFactors;
|
|
79
78
|
import com.google.common.util.concurrent.ListenableFuture;
|
|
79
|
+
import java.io.ByteArrayInputStream;
|
|
80
80
|
import java.io.ByteArrayOutputStream;
|
|
81
81
|
import java.io.File;
|
|
82
82
|
import java.io.FileOutputStream;
|
|
@@ -85,7 +85,6 @@ import java.nio.ByteBuffer;
|
|
|
85
85
|
import java.text.SimpleDateFormat;
|
|
86
86
|
import java.util.ArrayList;
|
|
87
87
|
import java.util.Arrays;
|
|
88
|
-
import java.util.Arrays;
|
|
89
88
|
import java.util.Collections;
|
|
90
89
|
import java.util.List;
|
|
91
90
|
import java.util.Locale;
|
|
@@ -99,6 +98,7 @@ import org.json.JSONObject;
|
|
|
99
98
|
public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
100
99
|
|
|
101
100
|
private static final String TAG = "CameraPreview CameraXView";
|
|
101
|
+
private static final String FOCUS_INDICATOR_TAG = "cpcp_focus_indicator";
|
|
102
102
|
|
|
103
103
|
public interface CameraXViewListener {
|
|
104
104
|
void onPictureTaken(String base64, JSONObject exif);
|
|
@@ -124,12 +124,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
124
124
|
private File currentVideoFile;
|
|
125
125
|
private VideoRecordingCallback currentVideoCallback;
|
|
126
126
|
private PreviewView previewView;
|
|
127
|
-
private Preview previewUseCase;
|
|
128
127
|
private GridOverlayView gridOverlayView;
|
|
129
128
|
private FrameLayout previewContainer;
|
|
130
129
|
private View focusIndicatorView;
|
|
131
130
|
private long focusIndicatorAnimationId = 0; // Incrementing token to invalidate previous animations
|
|
132
|
-
private boolean disableFocusIndicator = false; // Default to false for backward compatibility
|
|
133
131
|
private CameraSelector currentCameraSelector;
|
|
134
132
|
private String currentDeviceId;
|
|
135
133
|
private int currentFlashMode = ImageCapture.FLASH_MODE_OFF;
|
|
@@ -144,21 +142,55 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
144
142
|
private Size currentPreviewResolution = null;
|
|
145
143
|
private ListenableFuture<FocusMeteringResult> currentFocusFuture = null; // Track current focus operation
|
|
146
144
|
private String currentExposureMode = "CONTINUOUS"; // Default behavior
|
|
147
|
-
private boolean isVideoCaptureInitializing = false;
|
|
148
145
|
// Capture/stop coordination
|
|
149
146
|
private final Object captureLock = new Object();
|
|
150
147
|
private volatile boolean isCapturingPhoto = false;
|
|
151
148
|
private volatile boolean stopRequested = false;
|
|
152
149
|
private volatile boolean previewDetachedOnDeferredStop = false;
|
|
153
150
|
|
|
154
|
-
|
|
155
|
-
|
|
151
|
+
// Operation coordination (acts like a semaphore to prevent stop during active ops)
|
|
152
|
+
private final Object operationLock = new Object();
|
|
153
|
+
private int activeOperations = 0;
|
|
154
|
+
private boolean stopPending = false;
|
|
155
|
+
|
|
156
|
+
private boolean IsOperationRunning(String name) {
|
|
157
|
+
synchronized (operationLock) {
|
|
158
|
+
if (stopPending) {
|
|
159
|
+
Log.d(TAG, "beginOperation: blocked '" + name + "' due to stopPending");
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
activeOperations++;
|
|
163
|
+
Log.v(
|
|
164
|
+
TAG,
|
|
165
|
+
"beginOperation: '" + name + "' (active=" + activeOperations + ")"
|
|
166
|
+
);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
156
169
|
}
|
|
157
170
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
171
|
+
private void endOperation(String name) {
|
|
172
|
+
boolean shouldStop = false;
|
|
173
|
+
synchronized (operationLock) {
|
|
174
|
+
if (activeOperations > 0) activeOperations--;
|
|
175
|
+
Log.v(
|
|
176
|
+
TAG,
|
|
177
|
+
"endOperation: '" + name + "' (active=" + activeOperations + ")"
|
|
178
|
+
);
|
|
179
|
+
if (activeOperations == 0 && stopPending) {
|
|
180
|
+
shouldStop = true;
|
|
181
|
+
}
|
|
161
182
|
}
|
|
183
|
+
if (shouldStop) {
|
|
184
|
+
Log.d(
|
|
185
|
+
TAG,
|
|
186
|
+
"endOperation: all operations complete; performing deferred stop"
|
|
187
|
+
);
|
|
188
|
+
performImmediateStop();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public boolean isCapturing() {
|
|
193
|
+
return isCapturingPhoto;
|
|
162
194
|
}
|
|
163
195
|
|
|
164
196
|
public boolean isBusy() {
|
|
@@ -269,9 +301,85 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
269
301
|
}
|
|
270
302
|
}
|
|
271
303
|
|
|
304
|
+
private void saveImageToGallery(
|
|
305
|
+
byte[] data,
|
|
306
|
+
ExifInterface sourceExif,
|
|
307
|
+
Integer finalWidth,
|
|
308
|
+
Integer finalHeight
|
|
309
|
+
) {
|
|
310
|
+
try {
|
|
311
|
+
// First, write the bytes to a file
|
|
312
|
+
String extension = ".jpg";
|
|
313
|
+
String mimeType = "image/jpeg";
|
|
314
|
+
if (data.length >= 8) {
|
|
315
|
+
if (
|
|
316
|
+
data[0] == (byte) 0x89 &&
|
|
317
|
+
data[1] == 0x50 &&
|
|
318
|
+
data[2] == 0x4E &&
|
|
319
|
+
data[3] == 0x47
|
|
320
|
+
) {
|
|
321
|
+
extension = ".png";
|
|
322
|
+
mimeType = "image/png";
|
|
323
|
+
} else if (
|
|
324
|
+
data[0] == (byte) 0xFF &&
|
|
325
|
+
data[1] == (byte) 0xD8 &&
|
|
326
|
+
data[2] == (byte) 0xFF
|
|
327
|
+
) {
|
|
328
|
+
extension = ".jpg";
|
|
329
|
+
mimeType = "image/jpeg";
|
|
330
|
+
} else if (
|
|
331
|
+
data[0] == 0x52 &&
|
|
332
|
+
data[1] == 0x49 &&
|
|
333
|
+
data[2] == 0x46 &&
|
|
334
|
+
data[3] == 0x46 &&
|
|
335
|
+
data.length >= 12 &&
|
|
336
|
+
data[8] == 0x57 &&
|
|
337
|
+
data[9] == 0x45 &&
|
|
338
|
+
data[10] == 0x42 &&
|
|
339
|
+
data[11] == 0x50
|
|
340
|
+
) {
|
|
341
|
+
extension = ".webp";
|
|
342
|
+
mimeType = "image/webp";
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
File photo = new File(
|
|
347
|
+
Environment.getExternalStoragePublicDirectory(
|
|
348
|
+
Environment.DIRECTORY_PICTURES
|
|
349
|
+
),
|
|
350
|
+
"IMG_" +
|
|
351
|
+
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(
|
|
352
|
+
new java.util.Date()
|
|
353
|
+
) +
|
|
354
|
+
extension
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
FileOutputStream fos = new FileOutputStream(photo);
|
|
358
|
+
fos.write(data);
|
|
359
|
+
fos.flush();
|
|
360
|
+
fos.close();
|
|
361
|
+
|
|
362
|
+
// No EXIF rewrite here; bytes already contain EXIF when needed
|
|
363
|
+
|
|
364
|
+
// Notify the gallery of the new image
|
|
365
|
+
MediaScannerConnection.scanFile(
|
|
366
|
+
this.context,
|
|
367
|
+
new String[] { photo.getAbsolutePath() },
|
|
368
|
+
new String[] { mimeType },
|
|
369
|
+
null
|
|
370
|
+
);
|
|
371
|
+
} catch (IOException e) {
|
|
372
|
+
Log.e(TAG, "Error saving image to gallery (with exif)", e);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
272
376
|
public void startSession(CameraSessionConfiguration config) {
|
|
273
377
|
this.sessionConfig = config;
|
|
274
378
|
cameraExecutor = Executors.newSingleThreadExecutor();
|
|
379
|
+
synchronized (operationLock) {
|
|
380
|
+
activeOperations = 0;
|
|
381
|
+
stopPending = false;
|
|
382
|
+
}
|
|
275
383
|
mainExecutor.execute(() -> {
|
|
276
384
|
lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
|
|
277
385
|
setupCamera();
|
|
@@ -279,26 +387,38 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
279
387
|
}
|
|
280
388
|
|
|
281
389
|
public void stopSession() {
|
|
282
|
-
//
|
|
283
|
-
synchronized (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
390
|
+
// Mark stop pending; reject new operations and wait for active ones to finish
|
|
391
|
+
synchronized (operationLock) {
|
|
392
|
+
stopPending = true;
|
|
393
|
+
}
|
|
394
|
+
stopRequested = true;
|
|
395
|
+
|
|
396
|
+
boolean hasOps;
|
|
397
|
+
synchronized (operationLock) {
|
|
398
|
+
hasOps = activeOperations > 0;
|
|
399
|
+
}
|
|
400
|
+
if (hasOps) {
|
|
401
|
+
// Detach preview so UI can close
|
|
402
|
+
if (!previewDetachedOnDeferredStop) {
|
|
403
|
+
mainExecutor.execute(() -> {
|
|
404
|
+
try {
|
|
405
|
+
if (previewContainer != null) {
|
|
406
|
+
ViewGroup parent = (ViewGroup) previewContainer.getParent();
|
|
407
|
+
if (parent != null) {
|
|
408
|
+
parent.removeView(previewContainer);
|
|
295
409
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
})
|
|
299
|
-
}
|
|
300
|
-
return;
|
|
410
|
+
}
|
|
411
|
+
previewDetachedOnDeferredStop = true;
|
|
412
|
+
} catch (Exception ignored) {}
|
|
413
|
+
});
|
|
301
414
|
}
|
|
415
|
+
// Cancel focus to hasten completion
|
|
416
|
+
if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
|
|
417
|
+
try {
|
|
418
|
+
currentFocusFuture.cancel(true);
|
|
419
|
+
} catch (Exception ignored) {}
|
|
420
|
+
}
|
|
421
|
+
return;
|
|
302
422
|
}
|
|
303
423
|
|
|
304
424
|
performImmediateStop();
|
|
@@ -328,6 +448,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
328
448
|
} finally {
|
|
329
449
|
stopRequested = false;
|
|
330
450
|
previewDetachedOnDeferredStop = false;
|
|
451
|
+
synchronized (operationLock) {
|
|
452
|
+
activeOperations = 0;
|
|
453
|
+
stopPending = false;
|
|
454
|
+
}
|
|
331
455
|
if (listener != null) {
|
|
332
456
|
try {
|
|
333
457
|
listener.onCameraStopped();
|
|
@@ -381,6 +505,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
381
505
|
|
|
382
506
|
// Create and setup the preview view
|
|
383
507
|
previewView = new PreviewView(context);
|
|
508
|
+
// Use TextureView-backed implementation for broader device compatibility when overlaying with WebView
|
|
509
|
+
// This avoids SurfaceView z-order issues seen on some MIUI/EMUI devices.
|
|
510
|
+
previewView.setImplementationMode(
|
|
511
|
+
PreviewView.ImplementationMode.COMPATIBLE
|
|
512
|
+
);
|
|
384
513
|
// Match iOS behavior: FIT when no aspect ratio, FILL when aspect ratio is set
|
|
385
514
|
String initialAspectRatio = sessionConfig != null
|
|
386
515
|
? sessionConfig.getAspectRatio()
|
|
@@ -394,58 +523,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
394
523
|
previewView.setClickable(true);
|
|
395
524
|
previewView.setFocusable(true);
|
|
396
525
|
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
@Override
|
|
400
|
-
public boolean onTouch(View v, MotionEvent event) {
|
|
401
|
-
Log.d(
|
|
402
|
-
TAG,
|
|
403
|
-
"onTouch: " +
|
|
404
|
-
v.getClass().getSimpleName() +
|
|
405
|
-
" received touch event: " +
|
|
406
|
-
event.getAction() +
|
|
407
|
-
" at (" +
|
|
408
|
-
event.getX() +
|
|
409
|
-
", " +
|
|
410
|
-
event.getY() +
|
|
411
|
-
")"
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
|
415
|
-
float x = event.getX() / v.getWidth();
|
|
416
|
-
float y = event.getY() / v.getHeight();
|
|
417
|
-
|
|
418
|
-
Log.d(
|
|
419
|
-
TAG,
|
|
420
|
-
"onTouch: Touch detected at raw coords (" +
|
|
421
|
-
event.getX() +
|
|
422
|
-
", " +
|
|
423
|
-
event.getY() +
|
|
424
|
-
"), view size: " +
|
|
425
|
-
v.getWidth() +
|
|
426
|
-
"x" +
|
|
427
|
-
v.getHeight() +
|
|
428
|
-
", normalized: (" +
|
|
429
|
-
x +
|
|
430
|
-
", " +
|
|
431
|
-
y +
|
|
432
|
-
")"
|
|
433
|
-
);
|
|
434
|
-
|
|
435
|
-
try {
|
|
436
|
-
// Trigger focus with indicator
|
|
437
|
-
setFocus(x, y);
|
|
438
|
-
} catch (Exception e) {
|
|
439
|
-
Log.e(TAG, "Error during tap-to-focus: " + e.getMessage(), e);
|
|
440
|
-
}
|
|
441
|
-
return true;
|
|
442
|
-
}
|
|
443
|
-
return false;
|
|
444
|
-
}
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
previewContainer.setOnTouchListener(touchListener);
|
|
448
|
-
previewView.setOnTouchListener(touchListener);
|
|
526
|
+
// Intentionally no native gesture handling (tap-to-focus, pinch-to-zoom)
|
|
527
|
+
// Focus and zoom are controlled exclusively via JS API calls for parity with iOS.
|
|
449
528
|
|
|
450
529
|
previewContainer.addView(
|
|
451
530
|
previewView,
|
|
@@ -578,6 +657,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
578
657
|
int webViewHeight = webView != null ? webView.getHeight() : 0;
|
|
579
658
|
|
|
580
659
|
// Get parent dimensions
|
|
660
|
+
assert webView != null;
|
|
581
661
|
ViewGroup parent = (ViewGroup) webView.getParent();
|
|
582
662
|
int parentWidth = parent != null ? parent.getWidth() : 0;
|
|
583
663
|
int parentHeight = parent != null ? parent.getHeight() : 0;
|
|
@@ -663,20 +743,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
663
743
|
);
|
|
664
744
|
|
|
665
745
|
// For centered mode with aspect ratio, calculate maximum size that fits
|
|
666
|
-
int availableWidth = screenWidthPx;
|
|
667
|
-
int availableHeight = screenHeightPx;
|
|
668
746
|
|
|
669
747
|
Log.d(
|
|
670
748
|
TAG,
|
|
671
749
|
"Available space for preview: " +
|
|
672
|
-
|
|
750
|
+
screenWidthPx +
|
|
673
751
|
"x" +
|
|
674
|
-
|
|
752
|
+
screenHeightPx
|
|
675
753
|
);
|
|
676
754
|
|
|
677
755
|
// Calculate maximum size that fits the aspect ratio in available space
|
|
678
|
-
double maxWidthByHeight =
|
|
679
|
-
double maxHeightByWidth =
|
|
756
|
+
double maxWidthByHeight = screenHeightPx * ratio;
|
|
757
|
+
double maxHeightByWidth = screenWidthPx / ratio;
|
|
680
758
|
|
|
681
759
|
Log.d(
|
|
682
760
|
TAG,
|
|
@@ -686,21 +764,21 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
686
764
|
maxHeightByWidth
|
|
687
765
|
);
|
|
688
766
|
|
|
689
|
-
if (maxWidthByHeight <=
|
|
767
|
+
if (maxWidthByHeight <= screenWidthPx) {
|
|
690
768
|
// Height is the limiting factor
|
|
691
769
|
width = (int) maxWidthByHeight;
|
|
692
|
-
height =
|
|
770
|
+
height = screenHeightPx;
|
|
693
771
|
Log.d(TAG, "Height-limited sizing: " + width + "x" + height);
|
|
694
772
|
} else {
|
|
695
773
|
// Width is the limiting factor
|
|
696
|
-
width =
|
|
774
|
+
width = screenWidthPx;
|
|
697
775
|
height = (int) maxHeightByWidth;
|
|
698
776
|
Log.d(TAG, "Width-limited sizing: " + width + "x" + height);
|
|
699
777
|
}
|
|
700
778
|
|
|
701
779
|
// Center the preview
|
|
702
|
-
x = (
|
|
703
|
-
y = (
|
|
780
|
+
x = (screenWidthPx - width) / 2;
|
|
781
|
+
y = (screenHeightPx - height) / 2;
|
|
704
782
|
|
|
705
783
|
Log.d(TAG, "Auto-centered position: x=" + x + ", y=" + y);
|
|
706
784
|
} catch (NumberFormatException e) {
|
|
@@ -805,7 +883,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
805
883
|
.setTargetRotation(rotation)
|
|
806
884
|
.build();
|
|
807
885
|
// Keep reference to preview use case for later re-binding (e.g., when enabling video)
|
|
808
|
-
previewUseCase = preview;
|
|
809
886
|
imageCapture = new ImageCapture.Builder()
|
|
810
887
|
.setResolutionSelector(resolutionSelector)
|
|
811
888
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
|
@@ -1062,25 +1139,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1062
1139
|
return builder.build();
|
|
1063
1140
|
}
|
|
1064
1141
|
|
|
1065
|
-
private static String getCameraId(
|
|
1066
|
-
androidx.camera.core.CameraInfo cameraInfo
|
|
1067
|
-
) {
|
|
1068
|
-
try {
|
|
1069
|
-
// Generate a stable ID based on camera characteristics
|
|
1070
|
-
boolean isBack = isBackCamera(cameraInfo);
|
|
1071
|
-
float minZoom = Objects.requireNonNull(
|
|
1072
|
-
cameraInfo.getZoomState().getValue()
|
|
1073
|
-
).getMinZoomRatio();
|
|
1074
|
-
float maxZoom = cameraInfo.getZoomState().getValue().getMaxZoomRatio();
|
|
1075
|
-
|
|
1076
|
-
// Create a unique ID based on camera properties
|
|
1077
|
-
String position = isBack ? "back" : "front";
|
|
1078
|
-
return position + "_" + minZoom + "_" + maxZoom;
|
|
1079
|
-
} catch (Exception e) {
|
|
1080
|
-
return "unknown_camera";
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
1142
|
private static boolean isBackCamera(
|
|
1085
1143
|
androidx.camera.core.CameraInfo cameraInfo
|
|
1086
1144
|
) {
|
|
@@ -1108,6 +1166,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1108
1166
|
Integer height,
|
|
1109
1167
|
Location location
|
|
1110
1168
|
) {
|
|
1169
|
+
// Prevent capture if a stop is pending
|
|
1170
|
+
if (IsOperationRunning("capturePhoto")) {
|
|
1171
|
+
Log.d(TAG, "capturePhoto: Ignored because stop is pending");
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1111
1174
|
Log.d(
|
|
1112
1175
|
TAG,
|
|
1113
1176
|
"capturePhoto: Starting photo capture with quality: " +
|
|
@@ -1129,9 +1192,15 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1129
1192
|
isCapturingPhoto = true;
|
|
1130
1193
|
}
|
|
1131
1194
|
|
|
1132
|
-
|
|
1195
|
+
ByteArrayOutputStream imageStream = new ByteArrayOutputStream();
|
|
1196
|
+
ImageCapture.Metadata metadata = new ImageCapture.Metadata();
|
|
1197
|
+
if (location != null) {
|
|
1198
|
+
metadata.setLocation(location);
|
|
1199
|
+
}
|
|
1133
1200
|
ImageCapture.OutputFileOptions outputFileOptions =
|
|
1134
|
-
new ImageCapture.OutputFileOptions.Builder(
|
|
1201
|
+
new ImageCapture.OutputFileOptions.Builder(imageStream)
|
|
1202
|
+
.setMetadata(metadata)
|
|
1203
|
+
.build();
|
|
1135
1204
|
|
|
1136
1205
|
imageCapture.takePicture(
|
|
1137
1206
|
outputFileOptions,
|
|
@@ -1152,6 +1221,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1152
1221
|
performImmediateStop();
|
|
1153
1222
|
}
|
|
1154
1223
|
}
|
|
1224
|
+
endOperation("capturePhoto");
|
|
1155
1225
|
}
|
|
1156
1226
|
|
|
1157
1227
|
@Override
|
|
@@ -1159,28 +1229,25 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1159
1229
|
@NonNull ImageCapture.OutputFileResults output
|
|
1160
1230
|
) {
|
|
1161
1231
|
try {
|
|
1162
|
-
|
|
1163
|
-
byte[] bytes =
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1232
|
+
byte[] originalCaptureBytes = imageStream.toByteArray();
|
|
1233
|
+
byte[] bytes = originalCaptureBytes; // will be replaced if we transform
|
|
1234
|
+
int finalWidthOut = -1;
|
|
1235
|
+
int finalHeightOut = -1;
|
|
1236
|
+
boolean transformedPixels = false;
|
|
1167
1237
|
|
|
1168
1238
|
ExifInterface exifInterface = new ExifInterface(
|
|
1169
|
-
|
|
1239
|
+
new ByteArrayInputStream(originalCaptureBytes)
|
|
1170
1240
|
);
|
|
1171
|
-
|
|
1172
|
-
if (location != null) {
|
|
1173
|
-
exifInterface.setGpsInfo(location);
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1241
|
+
// Build EXIF JSON from captured bytes (location applied by metadata if provided)
|
|
1176
1242
|
JSONObject exifData = getExifData(exifInterface);
|
|
1177
1243
|
|
|
1178
1244
|
if (width != null || height != null) {
|
|
1179
1245
|
Bitmap bitmap = BitmapFactory.decodeByteArray(
|
|
1180
|
-
|
|
1246
|
+
originalCaptureBytes,
|
|
1181
1247
|
0,
|
|
1182
|
-
|
|
1248
|
+
originalCaptureBytes.length
|
|
1183
1249
|
);
|
|
1250
|
+
bitmap = applyExifOrientation(bitmap, exifInterface);
|
|
1184
1251
|
Bitmap resizedBitmap = resizeBitmapToMaxDimensions(
|
|
1185
1252
|
bitmap,
|
|
1186
1253
|
width,
|
|
@@ -1193,15 +1260,31 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1193
1260
|
stream
|
|
1194
1261
|
);
|
|
1195
1262
|
bytes = stream.toByteArray();
|
|
1263
|
+
transformedPixels = true;
|
|
1196
1264
|
|
|
1197
|
-
//
|
|
1198
|
-
|
|
1265
|
+
// Update EXIF JSON to reflect new dimensions; no in-place EXIF write to bytes
|
|
1266
|
+
try {
|
|
1267
|
+
exifData.put("PixelXDimension", resizedBitmap.getWidth());
|
|
1268
|
+
exifData.put("PixelYDimension", resizedBitmap.getHeight());
|
|
1269
|
+
exifData.put("ImageWidth", resizedBitmap.getWidth());
|
|
1270
|
+
exifData.put("ImageLength", resizedBitmap.getHeight());
|
|
1271
|
+
exifData.put(
|
|
1272
|
+
"Orientation",
|
|
1273
|
+
Integer.toString(ExifInterface.ORIENTATION_NORMAL)
|
|
1274
|
+
);
|
|
1275
|
+
} catch (Exception ignore) {}
|
|
1276
|
+
finalWidthOut = resizedBitmap.getWidth();
|
|
1277
|
+
finalHeightOut = resizedBitmap.getHeight();
|
|
1199
1278
|
} else {
|
|
1200
1279
|
// No explicit size/ratio: crop to match current preview content
|
|
1201
1280
|
Bitmap originalBitmap = BitmapFactory.decodeByteArray(
|
|
1202
|
-
|
|
1281
|
+
originalCaptureBytes,
|
|
1203
1282
|
0,
|
|
1204
|
-
|
|
1283
|
+
originalCaptureBytes.length
|
|
1284
|
+
);
|
|
1285
|
+
originalBitmap = applyExifOrientation(
|
|
1286
|
+
originalBitmap,
|
|
1287
|
+
exifInterface
|
|
1205
1288
|
);
|
|
1206
1289
|
Bitmap previewCropped = cropBitmapToMatchPreview(originalBitmap);
|
|
1207
1290
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
@@ -1211,16 +1294,44 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1211
1294
|
stream
|
|
1212
1295
|
);
|
|
1213
1296
|
bytes = stream.toByteArray();
|
|
1214
|
-
|
|
1215
|
-
|
|
1297
|
+
transformedPixels = true;
|
|
1298
|
+
// Update EXIF JSON to reflect cropped dimensions; no in-place EXIF write to bytes
|
|
1299
|
+
try {
|
|
1300
|
+
exifData.put("PixelXDimension", previewCropped.getWidth());
|
|
1301
|
+
exifData.put("PixelYDimension", previewCropped.getHeight());
|
|
1302
|
+
exifData.put("ImageWidth", previewCropped.getWidth());
|
|
1303
|
+
exifData.put("ImageLength", previewCropped.getHeight());
|
|
1304
|
+
exifData.put(
|
|
1305
|
+
"Orientation",
|
|
1306
|
+
Integer.toString(ExifInterface.ORIENTATION_NORMAL)
|
|
1307
|
+
);
|
|
1308
|
+
} catch (Exception ignore) {}
|
|
1309
|
+
finalWidthOut = previewCropped.getWidth();
|
|
1310
|
+
finalHeightOut = previewCropped.getHeight();
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// After any transform, inject EXIF back into the in-memory JPEG bytes (no temp file)
|
|
1314
|
+
if (transformedPixels) {
|
|
1315
|
+
Integer fW = (finalWidthOut > 0) ? finalWidthOut : null;
|
|
1316
|
+
Integer fH = (finalHeightOut > 0) ? finalHeightOut : null;
|
|
1317
|
+
bytes = injectExifInMemory(
|
|
1318
|
+
bytes,
|
|
1319
|
+
originalCaptureBytes,
|
|
1320
|
+
fW,
|
|
1321
|
+
fH,
|
|
1322
|
+
/*normalizeOrientation*/true
|
|
1323
|
+
);
|
|
1216
1324
|
}
|
|
1217
1325
|
|
|
1218
|
-
// Save to gallery asynchronously if requested
|
|
1326
|
+
// Save to gallery asynchronously if requested, copy EXIF to file
|
|
1219
1327
|
if (saveToGallery) {
|
|
1220
1328
|
final byte[] finalBytes = bytes;
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1329
|
+
final ExifInterface exifForFile = exifInterface;
|
|
1330
|
+
final Integer fW = (finalWidthOut > 0) ? finalWidthOut : null;
|
|
1331
|
+
final Integer fH = (finalHeightOut > 0) ? finalHeightOut : null;
|
|
1332
|
+
new Thread(() ->
|
|
1333
|
+
saveImageToGallery(finalBytes, exifForFile, fW, fH)
|
|
1334
|
+
).start();
|
|
1224
1335
|
}
|
|
1225
1336
|
|
|
1226
1337
|
String resultValue;
|
|
@@ -1241,6 +1352,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1241
1352
|
outFos.write(bytes);
|
|
1242
1353
|
outFos.close();
|
|
1243
1354
|
|
|
1355
|
+
// No EXIF rewrite here; bytes already contain EXIF when needed
|
|
1356
|
+
|
|
1244
1357
|
// Return a file path; apps can convert via Capacitor.convertFileSrc on JS side
|
|
1245
1358
|
resultValue = outFile.getAbsolutePath();
|
|
1246
1359
|
} catch (IOException ioEx) {
|
|
@@ -1253,8 +1366,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1253
1366
|
resultValue = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
1254
1367
|
}
|
|
1255
1368
|
|
|
1256
|
-
tempFile.delete();
|
|
1257
|
-
|
|
1258
1369
|
if (listener != null) {
|
|
1259
1370
|
listener.onPictureTaken(resultValue, exifData);
|
|
1260
1371
|
}
|
|
@@ -1273,14 +1384,56 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1273
1384
|
performImmediateStop();
|
|
1274
1385
|
}
|
|
1275
1386
|
}
|
|
1387
|
+
endOperation("capturePhoto");
|
|
1276
1388
|
}
|
|
1277
1389
|
}
|
|
1278
1390
|
}
|
|
1279
1391
|
);
|
|
1280
1392
|
}
|
|
1281
1393
|
|
|
1282
|
-
private
|
|
1283
|
-
|
|
1394
|
+
private int exifToDegrees(int exifOrientation) {
|
|
1395
|
+
switch (exifOrientation) {
|
|
1396
|
+
case ExifInterface.ORIENTATION_ROTATE_90:
|
|
1397
|
+
case ExifInterface.ORIENTATION_TRANSPOSE:
|
|
1398
|
+
return 90;
|
|
1399
|
+
case ExifInterface.ORIENTATION_ROTATE_180:
|
|
1400
|
+
return 180;
|
|
1401
|
+
case ExifInterface.ORIENTATION_ROTATE_270:
|
|
1402
|
+
case ExifInterface.ORIENTATION_TRANSVERSE:
|
|
1403
|
+
return 270;
|
|
1404
|
+
default:
|
|
1405
|
+
return 0;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
private Bitmap applyExifOrientation(Bitmap bitmap, ExifInterface exif) {
|
|
1410
|
+
try {
|
|
1411
|
+
int orientation = exif.getAttributeInt(
|
|
1412
|
+
ExifInterface.TAG_ORIENTATION,
|
|
1413
|
+
ExifInterface.ORIENTATION_UNDEFINED
|
|
1414
|
+
);
|
|
1415
|
+
int rotation = exifToDegrees(orientation);
|
|
1416
|
+
if (rotation == 0) return bitmap;
|
|
1417
|
+
Matrix m = new Matrix();
|
|
1418
|
+
m.postRotate(rotation);
|
|
1419
|
+
Bitmap rotated = Bitmap.createBitmap(
|
|
1420
|
+
bitmap,
|
|
1421
|
+
0,
|
|
1422
|
+
0,
|
|
1423
|
+
bitmap.getWidth(),
|
|
1424
|
+
bitmap.getHeight(),
|
|
1425
|
+
m,
|
|
1426
|
+
true
|
|
1427
|
+
);
|
|
1428
|
+
if (rotated != bitmap) {
|
|
1429
|
+
try {
|
|
1430
|
+
bitmap.recycle();
|
|
1431
|
+
} catch (Exception ignore) {}
|
|
1432
|
+
}
|
|
1433
|
+
return rotated;
|
|
1434
|
+
} catch (Exception e) {
|
|
1435
|
+
return bitmap;
|
|
1436
|
+
}
|
|
1284
1437
|
}
|
|
1285
1438
|
|
|
1286
1439
|
private Bitmap resizeBitmapToMaxDimensions(
|
|
@@ -1292,7 +1445,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1292
1445
|
int originalHeight = bitmap.getHeight();
|
|
1293
1446
|
float originalAspectRatio = (float) originalWidth / originalHeight;
|
|
1294
1447
|
|
|
1295
|
-
int targetWidth
|
|
1448
|
+
int targetWidth;
|
|
1296
1449
|
int targetHeight = originalHeight;
|
|
1297
1450
|
|
|
1298
1451
|
if (maxWidth != null && maxHeight != null) {
|
|
@@ -1336,6 +1489,69 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1336
1489
|
return exifData;
|
|
1337
1490
|
}
|
|
1338
1491
|
|
|
1492
|
+
// Inject EXIF into a JPEG byte[] fully in-memory using Apache Commons Imaging (no temp files)
|
|
1493
|
+
// Copies EXIF from sourceJpeg (original capture) and updates orientation/dimensions if provided.
|
|
1494
|
+
private byte[] injectExifInMemory(
|
|
1495
|
+
byte[] targetJpeg,
|
|
1496
|
+
byte[] sourceJpegWithExif,
|
|
1497
|
+
Integer finalWidth,
|
|
1498
|
+
Integer finalHeight,
|
|
1499
|
+
boolean normalizeOrientation
|
|
1500
|
+
) {
|
|
1501
|
+
try {
|
|
1502
|
+
// Quick signature check for JPEG (FF D8 FF)
|
|
1503
|
+
if (
|
|
1504
|
+
targetJpeg == null ||
|
|
1505
|
+
targetJpeg.length < 3 ||
|
|
1506
|
+
(targetJpeg[0] & 0xFF) != 0xFF ||
|
|
1507
|
+
(targetJpeg[1] & 0xFF) != 0xD8 ||
|
|
1508
|
+
(targetJpeg[2] & 0xFF) != 0xFF
|
|
1509
|
+
) {
|
|
1510
|
+
return targetJpeg; // Not a JPEG; nothing to do
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// Use Commons Imaging to read EXIF from the original capture bytes
|
|
1514
|
+
org.apache.commons.imaging.formats.jpeg.JpegImageMetadata jpegMetadata =
|
|
1515
|
+
(org.apache.commons.imaging.formats.jpeg.JpegImageMetadata) org.apache.commons.imaging.Imaging.getMetadata(
|
|
1516
|
+
sourceJpegWithExif
|
|
1517
|
+
);
|
|
1518
|
+
org.apache.commons.imaging.formats.tiff.TiffImageMetadata exif =
|
|
1519
|
+
jpegMetadata != null ? jpegMetadata.getExif() : null;
|
|
1520
|
+
|
|
1521
|
+
org.apache.commons.imaging.formats.tiff.write.TiffOutputSet outputSet =
|
|
1522
|
+
exif != null
|
|
1523
|
+
? exif.getOutputSet()
|
|
1524
|
+
: new org.apache.commons.imaging.formats.tiff.write.TiffOutputSet();
|
|
1525
|
+
|
|
1526
|
+
// Update orientation if requested (normalize to 1)
|
|
1527
|
+
if (normalizeOrientation) {
|
|
1528
|
+
org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory rootDir =
|
|
1529
|
+
outputSet.getOrCreateRootDirectory();
|
|
1530
|
+
rootDir.removeField(
|
|
1531
|
+
org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants.TIFF_TAG_ORIENTATION
|
|
1532
|
+
);
|
|
1533
|
+
rootDir.add(
|
|
1534
|
+
org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants.TIFF_TAG_ORIENTATION,
|
|
1535
|
+
(short) 1
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Optionally update dimensions here. Skipped to maximize compatibility with Commons Imaging 1.0-alpha3.
|
|
1540
|
+
|
|
1541
|
+
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
|
|
1542
|
+
new org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter()
|
|
1543
|
+
.updateExifMetadataLossless(
|
|
1544
|
+
new java.io.ByteArrayInputStream(targetJpeg),
|
|
1545
|
+
out,
|
|
1546
|
+
outputSet
|
|
1547
|
+
);
|
|
1548
|
+
return out.toByteArray();
|
|
1549
|
+
} catch (Throwable t) {
|
|
1550
|
+
Log.w(TAG, "injectExifInMemory: Failed to write EXIF in memory", t);
|
|
1551
|
+
return targetJpeg; // Fallback: return original bytes
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1339
1555
|
private static final String[][] EXIF_TAGS = new String[][] {
|
|
1340
1556
|
{ ExifInterface.TAG_APERTURE_VALUE, "ApertureValue" },
|
|
1341
1557
|
{ ExifInterface.TAG_ARTIST, "Artist" },
|
|
@@ -1501,54 +1717,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1501
1717
|
{ ExifInterface.TAG_Y_RESOLUTION, "YResolution" },
|
|
1502
1718
|
};
|
|
1503
1719
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
ExifInterface sourceExif
|
|
1507
|
-
) {
|
|
1508
|
-
try {
|
|
1509
|
-
// Create a temporary file to write the image with EXIF
|
|
1510
|
-
File tempExifFile = File.createTempFile(
|
|
1511
|
-
"temp_exif",
|
|
1512
|
-
".jpg",
|
|
1513
|
-
context.getCacheDir()
|
|
1514
|
-
);
|
|
1515
|
-
|
|
1516
|
-
// Write the image bytes to temp file
|
|
1517
|
-
java.io.FileOutputStream fos = new java.io.FileOutputStream(tempExifFile);
|
|
1518
|
-
fos.write(imageBytes);
|
|
1519
|
-
fos.close();
|
|
1520
|
-
|
|
1521
|
-
// Create new ExifInterface for the temp file and copy all EXIF data
|
|
1522
|
-
ExifInterface newExif = new ExifInterface(tempExifFile.getAbsolutePath());
|
|
1523
|
-
|
|
1524
|
-
// Copy all EXIF attributes from source to new
|
|
1525
|
-
for (String[] tag : EXIF_TAGS) {
|
|
1526
|
-
String value = sourceExif.getAttribute(tag[0]);
|
|
1527
|
-
if (value != null) {
|
|
1528
|
-
newExif.setAttribute(tag[0], value);
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
// Save the EXIF data
|
|
1533
|
-
newExif.saveAttributes();
|
|
1534
|
-
|
|
1535
|
-
// Read the file back with EXIF embedded
|
|
1536
|
-
byte[] result = new byte[(int) tempExifFile.length()];
|
|
1537
|
-
java.io.FileInputStream fis = new java.io.FileInputStream(tempExifFile);
|
|
1538
|
-
fis.read(result);
|
|
1539
|
-
fis.close();
|
|
1540
|
-
|
|
1541
|
-
// Clean up temp file
|
|
1542
|
-
tempExifFile.delete();
|
|
1543
|
-
|
|
1544
|
-
return result;
|
|
1545
|
-
} catch (Exception e) {
|
|
1546
|
-
Log.e(TAG, "writeExifToImageBytes: Error writing EXIF data", e);
|
|
1547
|
-
return imageBytes; // Return original bytes if error
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1720
|
+
// Note: We avoid temporary files for EXIF writes. When we transform pixels (resize/crop),
|
|
1721
|
+
// we recompress JPEG in-memory and update EXIF info only in the returned JSON, not in the bytes.
|
|
1550
1722
|
|
|
1551
1723
|
public void captureSample(int quality) {
|
|
1724
|
+
if (IsOperationRunning("captureSample")) {
|
|
1725
|
+
Log.d(TAG, "captureSample: Ignored because stop is pending");
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1552
1728
|
Log.d(
|
|
1553
1729
|
TAG,
|
|
1554
1730
|
"captureSample: Starting sample capture with quality: " + quality
|
|
@@ -1572,10 +1748,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1572
1748
|
"Sample capture failed: " + exception.getMessage()
|
|
1573
1749
|
);
|
|
1574
1750
|
}
|
|
1751
|
+
endOperation("captureSample");
|
|
1575
1752
|
}
|
|
1576
1753
|
|
|
1577
1754
|
@Override
|
|
1578
1755
|
public void onCaptureSuccess(@NonNull ImageProxy image) {
|
|
1756
|
+
//noinspection TryFinallyCanBeTryWithResources
|
|
1579
1757
|
try {
|
|
1580
1758
|
// Convert ImageProxy to byte array
|
|
1581
1759
|
byte[] bytes = imageProxyToByteArray(image);
|
|
@@ -1593,6 +1771,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1593
1771
|
}
|
|
1594
1772
|
} finally {
|
|
1595
1773
|
image.close();
|
|
1774
|
+
endOperation("captureSample");
|
|
1596
1775
|
}
|
|
1597
1776
|
}
|
|
1598
1777
|
}
|
|
@@ -1653,13 +1832,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1653
1832
|
// not workin for xiaomi https://xiaomi.eu/community/threads/mi-11-ultra-unable-to-access-camera-lenses-in-apps-camera2-api.61456/
|
|
1654
1833
|
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
1655
1834
|
public static List<
|
|
1656
|
-
|
|
1835
|
+
app.capgo.capacitor.camera.preview.model.CameraDevice
|
|
1657
1836
|
> getAvailableDevicesStatic(Context context) {
|
|
1658
1837
|
Log.d(
|
|
1659
1838
|
TAG,
|
|
1660
1839
|
"getAvailableDevicesStatic: Starting CameraX device enumeration with getPhysicalCameraInfos."
|
|
1661
1840
|
);
|
|
1662
|
-
List<
|
|
1841
|
+
List<app.capgo.capacitor.camera.preview.model.CameraDevice> devices =
|
|
1663
1842
|
new ArrayList<>();
|
|
1664
1843
|
try {
|
|
1665
1844
|
ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
|
|
@@ -1683,7 +1862,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1683
1862
|
List<LensInfo> logicalLenses = new ArrayList<>();
|
|
1684
1863
|
logicalLenses.add(new LensInfo(4.25f, "wideAngle", 1.0f, maxZoom));
|
|
1685
1864
|
devices.add(
|
|
1686
|
-
new
|
|
1865
|
+
new app.capgo.capacitor.camera.preview.model.CameraDevice(
|
|
1687
1866
|
logicalCameraId,
|
|
1688
1867
|
"Logical Camera (" + position + ")",
|
|
1689
1868
|
position,
|
|
@@ -1782,7 +1961,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1782
1961
|
);
|
|
1783
1962
|
|
|
1784
1963
|
devices.add(
|
|
1785
|
-
new
|
|
1964
|
+
new app.capgo.capacitor.camera.preview.model.CameraDevice(
|
|
1786
1965
|
physicalId,
|
|
1787
1966
|
label,
|
|
1788
1967
|
position,
|
|
@@ -1930,6 +2109,29 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1930
2109
|
}
|
|
1931
2110
|
|
|
1932
2111
|
public void setFocus(float x, float y) throws Exception {
|
|
2112
|
+
// Ignore focus if capture/stop is in progress or view is gone
|
|
2113
|
+
synchronized (captureLock) {
|
|
2114
|
+
if (isCapturingPhoto || stopRequested) {
|
|
2115
|
+
Log.d(TAG, "setFocus: Ignored because capture/stop in progress");
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
if (
|
|
2120
|
+
!isRunning ||
|
|
2121
|
+
camera == null ||
|
|
2122
|
+
previewView == null ||
|
|
2123
|
+
previewContainer == null
|
|
2124
|
+
) {
|
|
2125
|
+
Log.d(
|
|
2126
|
+
TAG,
|
|
2127
|
+
"setFocus: Ignored because camera/view not ready or not running"
|
|
2128
|
+
);
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
if (IsOperationRunning("setFocus")) {
|
|
2132
|
+
Log.d(TAG, "setFocus: Ignored because stop is pending");
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
1933
2135
|
if (camera == null) {
|
|
1934
2136
|
throw new Exception("Camera not initialized");
|
|
1935
2137
|
}
|
|
@@ -1955,7 +2157,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1955
2157
|
ExposureState state = camera.getCameraInfo().getExposureState();
|
|
1956
2158
|
Range<Integer> range = state.getExposureCompensationRange();
|
|
1957
2159
|
int zeroIdx = 0;
|
|
1958
|
-
if (
|
|
2160
|
+
if (!range.contains(0)) {
|
|
1959
2161
|
// Choose the closest index to 0 if 0 is not available
|
|
1960
2162
|
zeroIdx = Math.abs(range.getLower()) < Math.abs(range.getUpper())
|
|
1961
2163
|
? range.getLower()
|
|
@@ -1978,9 +2180,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1978
2180
|
// Only show focus indicator after validation passes
|
|
1979
2181
|
float indicatorX = x * viewWidth;
|
|
1980
2182
|
float indicatorY = y * viewHeight;
|
|
1981
|
-
|
|
2183
|
+
final long indicatorToken;
|
|
2184
|
+
long indicatorToken1;
|
|
2185
|
+
try {
|
|
2186
|
+
indicatorToken1 = showFocusIndicator(indicatorX, indicatorY);
|
|
2187
|
+
} catch (Exception ignore) {
|
|
2188
|
+
// If we can't show the indicator (e.g., view is gone), still proceed with metering
|
|
2189
|
+
// Use current token so hide is a no-op later
|
|
2190
|
+
indicatorToken1 = focusIndicatorAnimationId;
|
|
2191
|
+
}
|
|
1982
2192
|
|
|
1983
2193
|
// Create MeteringPoint using the preview view
|
|
2194
|
+
indicatorToken = indicatorToken1;
|
|
1984
2195
|
MeteringPointFactory factory = previewView.getMeteringPointFactory();
|
|
1985
2196
|
MeteringPoint point = factory.createPoint(x * viewWidth, y * viewHeight);
|
|
1986
2197
|
|
|
@@ -2026,6 +2237,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2026
2237
|
if (currentFocusFuture == future && currentFocusFuture.isDone()) {
|
|
2027
2238
|
currentFocusFuture = null;
|
|
2028
2239
|
}
|
|
2240
|
+
hideFocusIndicator(indicatorToken);
|
|
2241
|
+
endOperation("setFocus");
|
|
2029
2242
|
}
|
|
2030
2243
|
},
|
|
2031
2244
|
ContextCompat.getMainExecutor(context)
|
|
@@ -2033,6 +2246,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2033
2246
|
} catch (Exception e) {
|
|
2034
2247
|
currentFocusFuture = null;
|
|
2035
2248
|
Log.e(TAG, "Failed to set focus: " + e.getMessage());
|
|
2249
|
+
hideFocusIndicator(indicatorToken);
|
|
2250
|
+
endOperation("setFocus");
|
|
2036
2251
|
throw e;
|
|
2037
2252
|
}
|
|
2038
2253
|
}
|
|
@@ -2056,40 +2271,36 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2056
2271
|
}
|
|
2057
2272
|
String normalized = mode.toUpperCase(Locale.US);
|
|
2058
2273
|
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
.
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
break;
|
|
2075
|
-
}
|
|
2076
|
-
case "CONTINUOUS": {
|
|
2077
|
-
CaptureRequestOptions opts = new CaptureRequestOptions.Builder()
|
|
2078
|
-
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_LOCK, false)
|
|
2079
|
-
.setCaptureRequestOption(
|
|
2080
|
-
CaptureRequest.CONTROL_AE_MODE,
|
|
2081
|
-
CaptureRequest.CONTROL_AE_MODE_ON
|
|
2082
|
-
)
|
|
2083
|
-
.build();
|
|
2084
|
-
mainExecutor.execute(() -> c2.setCaptureRequestOptions(opts));
|
|
2085
|
-
currentExposureMode = "CONTINUOUS";
|
|
2086
|
-
break;
|
|
2087
|
-
}
|
|
2088
|
-
default:
|
|
2089
|
-
throw new Exception("Unsupported exposure mode: " + mode);
|
|
2274
|
+
Camera2CameraControl c2 = Camera2CameraControl.from(
|
|
2275
|
+
camera.getCameraControl()
|
|
2276
|
+
);
|
|
2277
|
+
switch (normalized) {
|
|
2278
|
+
case "LOCK": {
|
|
2279
|
+
CaptureRequestOptions opts = new CaptureRequestOptions.Builder()
|
|
2280
|
+
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_LOCK, true)
|
|
2281
|
+
.setCaptureRequestOption(
|
|
2282
|
+
CaptureRequest.CONTROL_AE_MODE,
|
|
2283
|
+
CaptureRequest.CONTROL_AE_MODE_ON
|
|
2284
|
+
)
|
|
2285
|
+
.build();
|
|
2286
|
+
mainExecutor.execute(() -> c2.setCaptureRequestOptions(opts));
|
|
2287
|
+
currentExposureMode = "LOCK";
|
|
2288
|
+
break;
|
|
2090
2289
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2290
|
+
case "CONTINUOUS": {
|
|
2291
|
+
CaptureRequestOptions opts = new CaptureRequestOptions.Builder()
|
|
2292
|
+
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_LOCK, false)
|
|
2293
|
+
.setCaptureRequestOption(
|
|
2294
|
+
CaptureRequest.CONTROL_AE_MODE,
|
|
2295
|
+
CaptureRequest.CONTROL_AE_MODE_ON
|
|
2296
|
+
)
|
|
2297
|
+
.build();
|
|
2298
|
+
mainExecutor.execute(() -> c2.setCaptureRequestOptions(opts));
|
|
2299
|
+
currentExposureMode = "CONTINUOUS";
|
|
2300
|
+
break;
|
|
2301
|
+
}
|
|
2302
|
+
default:
|
|
2303
|
+
throw new Exception("Unsupported exposure mode: " + mode);
|
|
2093
2304
|
}
|
|
2094
2305
|
}
|
|
2095
2306
|
|
|
@@ -2100,9 +2311,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2100
2311
|
ExposureState state = camera.getCameraInfo().getExposureState();
|
|
2101
2312
|
Range<Integer> idxRange = state.getExposureCompensationRange();
|
|
2102
2313
|
Rational step = state.getExposureCompensationStep();
|
|
2103
|
-
float evStep = step
|
|
2104
|
-
? (float) step.getNumerator() / (float) step.getDenominator()
|
|
2105
|
-
: 1.0f;
|
|
2314
|
+
float evStep = (float) step.getNumerator() / (float) step.getDenominator();
|
|
2106
2315
|
float min = idxRange.getLower() * evStep;
|
|
2107
2316
|
float max = idxRange.getUpper() * evStep;
|
|
2108
2317
|
return new float[] { min, max, evStep };
|
|
@@ -2115,9 +2324,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2115
2324
|
ExposureState state = camera.getCameraInfo().getExposureState();
|
|
2116
2325
|
int idx = state.getExposureCompensationIndex();
|
|
2117
2326
|
Rational step = state.getExposureCompensationStep();
|
|
2118
|
-
float evStep = step
|
|
2119
|
-
? (float) step.getNumerator() / (float) step.getDenominator()
|
|
2120
|
-
: 1.0f;
|
|
2327
|
+
float evStep = (float) step.getNumerator() / (float) step.getDenominator();
|
|
2121
2328
|
return idx * evStep;
|
|
2122
2329
|
}
|
|
2123
2330
|
|
|
@@ -2128,9 +2335,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2128
2335
|
ExposureState state = camera.getCameraInfo().getExposureState();
|
|
2129
2336
|
Range<Integer> idxRange = state.getExposureCompensationRange();
|
|
2130
2337
|
Rational step = state.getExposureCompensationStep();
|
|
2131
|
-
float evStep = step
|
|
2132
|
-
? (float) step.getNumerator() / (float) step.getDenominator()
|
|
2133
|
-
: 1.0f;
|
|
2338
|
+
float evStep = (float) step.getNumerator() / (float) step.getDenominator();
|
|
2134
2339
|
if (evStep <= 0f) evStep = 1.0f;
|
|
2135
2340
|
int idx = Math.round(ev / evStep);
|
|
2136
2341
|
// clamp
|
|
@@ -2139,13 +2344,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2139
2344
|
camera.getCameraControl().setExposureCompensationIndex(idx);
|
|
2140
2345
|
}
|
|
2141
2346
|
|
|
2142
|
-
private
|
|
2143
|
-
|
|
2144
|
-
return;
|
|
2145
|
-
}
|
|
2347
|
+
private long showFocusIndicator(float x, float y) {
|
|
2348
|
+
// If preview is gone (e.g., stopping/closing), bail out safely
|
|
2146
2349
|
if (previewContainer == null) {
|
|
2147
2350
|
Log.w(TAG, "showFocusIndicator: previewContainer is null");
|
|
2148
|
-
return;
|
|
2351
|
+
return focusIndicatorAnimationId;
|
|
2352
|
+
}
|
|
2353
|
+
if (sessionConfig.getDisableFocusIndicator()) {
|
|
2354
|
+
return focusIndicatorAnimationId;
|
|
2149
2355
|
}
|
|
2150
2356
|
|
|
2151
2357
|
// Check if container has been laid out
|
|
@@ -2155,17 +2361,24 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2155
2361
|
"showFocusIndicator: previewContainer not laid out yet, posting to run after layout"
|
|
2156
2362
|
);
|
|
2157
2363
|
previewContainer.post(() -> showFocusIndicator(x, y));
|
|
2158
|
-
return;
|
|
2364
|
+
return focusIndicatorAnimationId;
|
|
2159
2365
|
}
|
|
2160
2366
|
|
|
2161
|
-
// Remove any existing focus
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2367
|
+
// Remove any existing focus indicators (ensure only one is visible)
|
|
2368
|
+
try {
|
|
2369
|
+
for (int i = previewContainer.getChildCount() - 1; i >= 0; i--) {
|
|
2370
|
+
View child = previewContainer.getChildAt(i);
|
|
2371
|
+
CharSequence desc = child.getContentDescription();
|
|
2372
|
+
if (desc != null && FOCUS_INDICATOR_TAG.contentEquals(desc)) {
|
|
2373
|
+
previewContainer.removeViewAt(i);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
if (focusIndicatorView != null) {
|
|
2377
|
+
ViewGroup parent = (ViewGroup) focusIndicatorView.getParent();
|
|
2378
|
+
if (parent != null) parent.removeView(focusIndicatorView);
|
|
2379
|
+
focusIndicatorView = null;
|
|
2380
|
+
}
|
|
2381
|
+
} catch (Exception ignore) {}
|
|
2169
2382
|
|
|
2170
2383
|
// Create an elegant focus indicator
|
|
2171
2384
|
FrameLayout container = new FrameLayout(context);
|
|
@@ -2178,11 +2391,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2178
2391
|
|
|
2179
2392
|
params.leftMargin = Math.max(
|
|
2180
2393
|
0,
|
|
2181
|
-
Math.min((int) (x - size / 2), containerWidth - size)
|
|
2394
|
+
Math.min((int) (x - (float) size / 2), containerWidth - size)
|
|
2182
2395
|
);
|
|
2183
2396
|
params.topMargin = Math.max(
|
|
2184
2397
|
0,
|
|
2185
|
-
Math.min((int) (y - size / 2), containerHeight - size)
|
|
2398
|
+
Math.min((int) (y - (float) size / 2), containerHeight - size)
|
|
2186
2399
|
);
|
|
2187
2400
|
|
|
2188
2401
|
// iOS Camera style: square with mid-edge ticks
|
|
@@ -2197,7 +2410,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2197
2410
|
// Add 4 tiny mid-edge ticks inside the square
|
|
2198
2411
|
int tickLen = (int) (12 *
|
|
2199
2412
|
context.getResources().getDisplayMetrics().density);
|
|
2200
|
-
|
|
2413
|
+
// ticks should touch the sides
|
|
2201
2414
|
// Top tick (perpendicular): vertical inward from top edge
|
|
2202
2415
|
View topTick = new View(context);
|
|
2203
2416
|
FrameLayout.LayoutParams topParams = new FrameLayout.LayoutParams(
|
|
@@ -2205,7 +2418,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2205
2418
|
tickLen
|
|
2206
2419
|
);
|
|
2207
2420
|
topParams.leftMargin = (size - stroke) / 2;
|
|
2208
|
-
topParams.topMargin =
|
|
2421
|
+
topParams.topMargin = stroke;
|
|
2209
2422
|
topTick.setLayoutParams(topParams);
|
|
2210
2423
|
topTick.setBackgroundColor(Color.YELLOW);
|
|
2211
2424
|
container.addView(topTick);
|
|
@@ -2216,7 +2429,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2216
2429
|
tickLen
|
|
2217
2430
|
);
|
|
2218
2431
|
bottomParams.leftMargin = (size - stroke) / 2;
|
|
2219
|
-
bottomParams.topMargin = size -
|
|
2432
|
+
bottomParams.topMargin = size - stroke - tickLen;
|
|
2220
2433
|
bottomTick.setLayoutParams(bottomParams);
|
|
2221
2434
|
bottomTick.setBackgroundColor(Color.YELLOW);
|
|
2222
2435
|
container.addView(bottomTick);
|
|
@@ -2226,7 +2439,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2226
2439
|
tickLen,
|
|
2227
2440
|
stroke
|
|
2228
2441
|
);
|
|
2229
|
-
leftParams.leftMargin =
|
|
2442
|
+
leftParams.leftMargin = stroke;
|
|
2230
2443
|
leftParams.topMargin = (size - stroke) / 2;
|
|
2231
2444
|
leftTick.setLayoutParams(leftParams);
|
|
2232
2445
|
leftTick.setBackgroundColor(Color.YELLOW);
|
|
@@ -2237,21 +2450,22 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2237
2450
|
tickLen,
|
|
2238
2451
|
stroke
|
|
2239
2452
|
);
|
|
2240
|
-
rightParams.leftMargin = size -
|
|
2453
|
+
rightParams.leftMargin = size - stroke - tickLen;
|
|
2241
2454
|
rightParams.topMargin = (size - stroke) / 2;
|
|
2242
2455
|
rightTick.setLayoutParams(rightParams);
|
|
2243
2456
|
rightTick.setBackgroundColor(Color.YELLOW);
|
|
2244
2457
|
container.addView(rightTick);
|
|
2245
2458
|
|
|
2459
|
+
container.setContentDescription(FOCUS_INDICATOR_TAG);
|
|
2246
2460
|
focusIndicatorView = container;
|
|
2247
2461
|
// Bump animation token; everything after this must validate against this token
|
|
2248
2462
|
final long thisAnimationId = ++focusIndicatorAnimationId;
|
|
2249
2463
|
final View thisIndicatorView = focusIndicatorView;
|
|
2250
2464
|
|
|
2251
|
-
//
|
|
2252
|
-
focusIndicatorView.setAlpha(
|
|
2253
|
-
focusIndicatorView.setScaleX(
|
|
2254
|
-
focusIndicatorView.setScaleY(
|
|
2465
|
+
// Show immediately (avoid complex animations that can race with teardown)
|
|
2466
|
+
focusIndicatorView.setAlpha(1f);
|
|
2467
|
+
focusIndicatorView.setScaleX(1f);
|
|
2468
|
+
focusIndicatorView.setScaleY(1f);
|
|
2255
2469
|
focusIndicatorView.setVisibility(View.VISIBLE);
|
|
2256
2470
|
|
|
2257
2471
|
// Ensure container doesn't intercept touch events
|
|
@@ -2259,12 +2473,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2259
2473
|
container.setFocusable(false);
|
|
2260
2474
|
|
|
2261
2475
|
// Ensure the focus indicator has a high elevation for visibility
|
|
2262
|
-
|
|
2263
|
-
android.os.Build.VERSION.SDK_INT >=
|
|
2264
|
-
android.os.Build.VERSION_CODES.LOLLIPOP
|
|
2265
|
-
) {
|
|
2266
|
-
focusIndicatorView.setElevation(10f);
|
|
2267
|
-
}
|
|
2476
|
+
focusIndicatorView.setElevation(10f);
|
|
2268
2477
|
|
|
2269
2478
|
// Add to container first
|
|
2270
2479
|
previewContainer.addView(focusIndicatorView, params);
|
|
@@ -2275,88 +2484,43 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2275
2484
|
// Force a layout pass to ensure the view is properly positioned
|
|
2276
2485
|
previewContainer.requestLayout();
|
|
2277
2486
|
|
|
2278
|
-
//
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
public void run() {
|
|
2292
|
-
// Ensure this runnable belongs to the latest indicator
|
|
2487
|
+
// Do not schedule delayed cleanup; indicator will be removed when focus completes
|
|
2488
|
+
return thisAnimationId;
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
private void hideFocusIndicator(long token) {
|
|
2492
|
+
// If we're stopping or not running anymore, don't attempt to touch the view tree
|
|
2493
|
+
if (stopRequested || !isRunning) {
|
|
2494
|
+
focusIndicatorView = null;
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
try {
|
|
2498
|
+
mainExecutor.execute(() -> {
|
|
2499
|
+
try {
|
|
2293
2500
|
if (
|
|
2294
|
-
focusIndicatorView
|
|
2295
|
-
|
|
2296
|
-
|
|
2501
|
+
focusIndicatorView == null ||
|
|
2502
|
+
previewContainer == null ||
|
|
2503
|
+
token != focusIndicatorAnimationId
|
|
2297
2504
|
) {
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
@Override
|
|
2305
|
-
public void run() {
|
|
2306
|
-
// Phase 3: after 200ms more, fade out to 0 and scale to 0.8 over 300ms
|
|
2307
|
-
focusIndicatorView.postDelayed(
|
|
2308
|
-
new Runnable() {
|
|
2309
|
-
@Override
|
|
2310
|
-
public void run() {
|
|
2311
|
-
if (
|
|
2312
|
-
focusIndicatorView != null &&
|
|
2313
|
-
thisIndicatorView == focusIndicatorView &&
|
|
2314
|
-
thisAnimationId == focusIndicatorAnimationId
|
|
2315
|
-
) {
|
|
2316
|
-
focusIndicatorView
|
|
2317
|
-
.animate()
|
|
2318
|
-
.alpha(0f)
|
|
2319
|
-
.scaleX(0.8f)
|
|
2320
|
-
.scaleY(0.8f)
|
|
2321
|
-
.setDuration(300)
|
|
2322
|
-
.setInterpolator(
|
|
2323
|
-
new android.view.animation.AccelerateInterpolator()
|
|
2324
|
-
)
|
|
2325
|
-
.withEndAction(
|
|
2326
|
-
new Runnable() {
|
|
2327
|
-
@Override
|
|
2328
|
-
public void run() {
|
|
2329
|
-
if (
|
|
2330
|
-
focusIndicatorView != null &&
|
|
2331
|
-
previewContainer != null &&
|
|
2332
|
-
thisIndicatorView == focusIndicatorView &&
|
|
2333
|
-
thisAnimationId ==
|
|
2334
|
-
focusIndicatorAnimationId
|
|
2335
|
-
) {
|
|
2336
|
-
try {
|
|
2337
|
-
focusIndicatorView.clearAnimation();
|
|
2338
|
-
} catch (Exception ignore) {}
|
|
2339
|
-
previewContainer.removeView(
|
|
2340
|
-
focusIndicatorView
|
|
2341
|
-
);
|
|
2342
|
-
focusIndicatorView = null;
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
);
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
|
-
},
|
|
2350
|
-
200
|
|
2351
|
-
);
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
);
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
// If the view hierarchy is already being torn down, skip safely
|
|
2508
|
+
if (!previewContainer.isAttachedToWindow()) {
|
|
2509
|
+
focusIndicatorView = null;
|
|
2510
|
+
return;
|
|
2355
2511
|
}
|
|
2512
|
+
ViewGroup parent = (ViewGroup) focusIndicatorView.getParent();
|
|
2513
|
+
if (parent != null) {
|
|
2514
|
+
parent.removeView(focusIndicatorView);
|
|
2515
|
+
}
|
|
2516
|
+
} catch (Exception ignore) {} finally {
|
|
2517
|
+
focusIndicatorView = null;
|
|
2356
2518
|
}
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2519
|
+
});
|
|
2520
|
+
} catch (Exception ignore) {
|
|
2521
|
+
// Executor or Looper not available; just null out the reference
|
|
2522
|
+
focusIndicatorView = null;
|
|
2523
|
+
}
|
|
2360
2524
|
}
|
|
2361
2525
|
|
|
2362
2526
|
public static List<Size> getSupportedPictureSizes(String facing) {
|
|
@@ -2562,7 +2726,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2562
2726
|
sessionConfig.isToBack(), // toBack
|
|
2563
2727
|
sessionConfig.isStoreToFile(), // storeToFile
|
|
2564
2728
|
sessionConfig.isEnableOpacity(), // enableOpacity
|
|
2565
|
-
sessionConfig.isEnableZoom(), // enableZoom
|
|
2566
2729
|
sessionConfig.isDisableExifHeaderStripping(), // disableExifHeaderStripping
|
|
2567
2730
|
sessionConfig.isDisableAudio(), // disableAudio
|
|
2568
2731
|
sessionConfig.getZoomFactor(), // zoomFactor
|
|
@@ -2722,7 +2885,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2722
2885
|
sessionConfig.getToBack(),
|
|
2723
2886
|
sessionConfig.getStoreToFile(),
|
|
2724
2887
|
sessionConfig.getEnableOpacity(),
|
|
2725
|
-
sessionConfig.getEnableZoom(),
|
|
2726
2888
|
sessionConfig.getDisableExifHeaderStripping(),
|
|
2727
2889
|
sessionConfig.getDisableAudio(),
|
|
2728
2890
|
sessionConfig.getZoomFactor(),
|
|
@@ -2737,7 +2899,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2737
2899
|
if (isRunning && previewContainer != null) {
|
|
2738
2900
|
mainExecutor.execute(() -> {
|
|
2739
2901
|
// First update the UI layout - always pass null for x,y to force auto-centering (matching iOS)
|
|
2740
|
-
updatePreviewLayoutForAspectRatio(aspectRatio
|
|
2902
|
+
updatePreviewLayoutForAspectRatio(aspectRatio);
|
|
2741
2903
|
|
|
2742
2904
|
// Then rebind the camera with new aspect ratio configuration
|
|
2743
2905
|
Log.d(
|
|
@@ -2826,7 +2988,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2826
2988
|
sessionConfig.getToBack(),
|
|
2827
2989
|
sessionConfig.getStoreToFile(),
|
|
2828
2990
|
sessionConfig.getEnableOpacity(),
|
|
2829
|
-
sessionConfig.getEnableZoom(),
|
|
2830
2991
|
sessionConfig.getDisableExifHeaderStripping(),
|
|
2831
2992
|
sessionConfig.getDisableAudio(),
|
|
2832
2993
|
sessionConfig.getZoomFactor(),
|
|
@@ -2841,7 +3002,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2841
3002
|
if (isRunning && previewContainer != null) {
|
|
2842
3003
|
mainExecutor.execute(() -> {
|
|
2843
3004
|
// First update the UI layout - always pass null for x,y to force auto-centering (matching iOS)
|
|
2844
|
-
updatePreviewLayoutForAspectRatio(aspectRatio
|
|
3005
|
+
updatePreviewLayoutForAspectRatio(aspectRatio);
|
|
2845
3006
|
|
|
2846
3007
|
// Then rebind the camera with new aspect ratio configuration
|
|
2847
3008
|
Log.d(
|
|
@@ -2902,7 +3063,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2902
3063
|
sessionConfig.getToBack(),
|
|
2903
3064
|
sessionConfig.getStoreToFile(),
|
|
2904
3065
|
sessionConfig.getEnableOpacity(),
|
|
2905
|
-
sessionConfig.getEnableZoom(),
|
|
2906
3066
|
sessionConfig.getDisableExifHeaderStripping(),
|
|
2907
3067
|
sessionConfig.getDisableAudio(),
|
|
2908
3068
|
sessionConfig.getZoomFactor(),
|
|
@@ -3010,6 +3170,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3010
3170
|
// Swap dimensions if in portrait mode to match how PreviewView displays it
|
|
3011
3171
|
if (isPortrait) {
|
|
3012
3172
|
int temp = cameraWidth;
|
|
3173
|
+
//noinspection SuspiciousNameCombination,ReassignedVariable
|
|
3013
3174
|
cameraWidth = cameraHeight;
|
|
3014
3175
|
cameraHeight = temp;
|
|
3015
3176
|
}
|
|
@@ -3286,7 +3447,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3286
3447
|
sessionConfig.getToBack(),
|
|
3287
3448
|
sessionConfig.getStoreToFile(),
|
|
3288
3449
|
sessionConfig.getEnableOpacity(),
|
|
3289
|
-
sessionConfig.getEnableZoom(),
|
|
3290
3450
|
sessionConfig.getDisableExifHeaderStripping(),
|
|
3291
3451
|
sessionConfig.getDisableAudio(),
|
|
3292
3452
|
sessionConfig.getZoomFactor(),
|
|
@@ -3318,7 +3478,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3318
3478
|
previewContainer.post(callback);
|
|
3319
3479
|
});
|
|
3320
3480
|
} else {
|
|
3321
|
-
previewContainer.post(
|
|
3481
|
+
previewContainer.post(this::updateGridOverlayBounds);
|
|
3322
3482
|
}
|
|
3323
3483
|
} else {
|
|
3324
3484
|
// No camera rebinding needed, wait for layout to complete then call callback
|
|
@@ -3359,29 +3519,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3359
3519
|
}
|
|
3360
3520
|
|
|
3361
3521
|
private void updatePreviewLayoutForAspectRatio(String aspectRatio) {
|
|
3362
|
-
updatePreviewLayoutForAspectRatio(aspectRatio, null, null);
|
|
3363
|
-
}
|
|
3364
|
-
|
|
3365
|
-
private void updatePreviewLayoutForAspectRatio(
|
|
3366
|
-
String aspectRatio,
|
|
3367
|
-
Float x,
|
|
3368
|
-
Float y
|
|
3369
|
-
) {
|
|
3370
3522
|
if (previewContainer == null || aspectRatio == null) return;
|
|
3371
3523
|
|
|
3372
3524
|
Log.d(
|
|
3373
3525
|
TAG,
|
|
3374
3526
|
"======================== UPDATE PREVIEW LAYOUT FOR ASPECT RATIO ========================"
|
|
3375
3527
|
);
|
|
3376
|
-
Log.d(
|
|
3377
|
-
TAG,
|
|
3378
|
-
"Input parameters - aspectRatio: " +
|
|
3379
|
-
aspectRatio +
|
|
3380
|
-
", x: " +
|
|
3381
|
-
x +
|
|
3382
|
-
", y: " +
|
|
3383
|
-
y
|
|
3384
|
-
);
|
|
3528
|
+
Log.d(TAG, "Input parameters - aspectRatio: " + aspectRatio);
|
|
3385
3529
|
|
|
3386
3530
|
// Get comprehensive display information
|
|
3387
3531
|
WindowManager windowManager = (WindowManager) this.context.getSystemService(
|
|
@@ -3491,115 +3635,57 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3491
3635
|
);
|
|
3492
3636
|
|
|
3493
3637
|
// Get available space from webview dimensions
|
|
3494
|
-
int availableWidth = webViewWidth;
|
|
3495
|
-
int availableHeight = webViewHeight;
|
|
3496
3638
|
|
|
3497
3639
|
Log.d(
|
|
3498
3640
|
TAG,
|
|
3499
|
-
"Available space from WebView: " +
|
|
3500
|
-
availableWidth +
|
|
3501
|
-
"x" +
|
|
3502
|
-
availableHeight
|
|
3641
|
+
"Available space from WebView: " + webViewWidth + "x" + webViewHeight
|
|
3503
3642
|
);
|
|
3504
3643
|
|
|
3505
3644
|
// Calculate position and size
|
|
3506
3645
|
int finalX, finalY, finalWidth, finalHeight;
|
|
3646
|
+
// Auto-center mode - match iOS behavior exactly
|
|
3647
|
+
Log.d(TAG, "Auto-center mode");
|
|
3507
3648
|
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
int webViewLeftInset = getWebViewLeftInset();
|
|
3512
|
-
|
|
3513
|
-
Log.d(
|
|
3514
|
-
TAG,
|
|
3515
|
-
"Manual positioning mode - WebView insets: left=" +
|
|
3516
|
-
webViewLeftInset +
|
|
3517
|
-
", top=" +
|
|
3518
|
-
webViewTopInset
|
|
3519
|
-
);
|
|
3520
|
-
|
|
3521
|
-
finalX = Math.max(
|
|
3522
|
-
0,
|
|
3523
|
-
Math.min(x.intValue() + webViewLeftInset, availableWidth)
|
|
3524
|
-
);
|
|
3525
|
-
finalY = Math.max(
|
|
3526
|
-
0,
|
|
3527
|
-
Math.min(y.intValue() + webViewTopInset, availableHeight)
|
|
3528
|
-
);
|
|
3529
|
-
|
|
3530
|
-
// Calculate maximum available space from the given position
|
|
3531
|
-
int maxWidth = availableWidth - finalX;
|
|
3532
|
-
int maxHeight = availableHeight - finalY;
|
|
3533
|
-
|
|
3534
|
-
Log.d(
|
|
3535
|
-
TAG,
|
|
3536
|
-
"Max available space from position: " + maxWidth + "x" + maxHeight
|
|
3537
|
-
);
|
|
3649
|
+
// Calculate maximum size that fits the aspect ratio in available space
|
|
3650
|
+
double maxWidthByHeight = webViewHeight * ratio;
|
|
3651
|
+
double maxHeightByWidth = webViewWidth / ratio;
|
|
3538
3652
|
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
finalWidth = (int) (maxHeight * ratio);
|
|
3547
|
-
Log.d(TAG, "Height-constrained sizing");
|
|
3548
|
-
} else {
|
|
3549
|
-
Log.d(TAG, "Width-constrained sizing");
|
|
3550
|
-
}
|
|
3653
|
+
Log.d(
|
|
3654
|
+
TAG,
|
|
3655
|
+
"Aspect ratio calculations - maxWidthByHeight: " +
|
|
3656
|
+
maxWidthByHeight +
|
|
3657
|
+
", maxHeightByWidth: " +
|
|
3658
|
+
maxHeightByWidth
|
|
3659
|
+
);
|
|
3551
3660
|
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3661
|
+
if (maxWidthByHeight <= webViewWidth) {
|
|
3662
|
+
// Height is the limiting factor
|
|
3663
|
+
finalWidth = (int) maxWidthByHeight;
|
|
3664
|
+
finalHeight = webViewHeight;
|
|
3665
|
+
Log.d(TAG, "Height-limited sizing: " + finalWidth + "x" + finalHeight);
|
|
3555
3666
|
} else {
|
|
3556
|
-
//
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
double maxHeightByWidth = availableWidth / ratio;
|
|
3562
|
-
|
|
3563
|
-
Log.d(
|
|
3564
|
-
TAG,
|
|
3565
|
-
"Aspect ratio calculations - maxWidthByHeight: " +
|
|
3566
|
-
maxWidthByHeight +
|
|
3567
|
-
", maxHeightByWidth: " +
|
|
3568
|
-
maxHeightByWidth
|
|
3569
|
-
);
|
|
3570
|
-
|
|
3571
|
-
if (maxWidthByHeight <= availableWidth) {
|
|
3572
|
-
// Height is the limiting factor
|
|
3573
|
-
finalWidth = (int) maxWidthByHeight;
|
|
3574
|
-
finalHeight = availableHeight;
|
|
3575
|
-
Log.d(
|
|
3576
|
-
TAG,
|
|
3577
|
-
"Height-limited sizing: " + finalWidth + "x" + finalHeight
|
|
3578
|
-
);
|
|
3579
|
-
} else {
|
|
3580
|
-
// Width is the limiting factor
|
|
3581
|
-
finalWidth = availableWidth;
|
|
3582
|
-
finalHeight = (int) maxHeightByWidth;
|
|
3583
|
-
Log.d(TAG, "Width-limited sizing: " + finalWidth + "x" + finalHeight);
|
|
3584
|
-
}
|
|
3667
|
+
// Width is the limiting factor
|
|
3668
|
+
finalWidth = webViewWidth;
|
|
3669
|
+
finalHeight = (int) maxHeightByWidth;
|
|
3670
|
+
Log.d(TAG, "Width-limited sizing: " + finalWidth + "x" + finalHeight);
|
|
3671
|
+
}
|
|
3585
3672
|
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3673
|
+
// Center the preview
|
|
3674
|
+
finalX = (webViewWidth - finalWidth) / 2;
|
|
3675
|
+
finalY = (webViewHeight - finalHeight) / 2;
|
|
3589
3676
|
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
}
|
|
3677
|
+
Log.d(
|
|
3678
|
+
TAG,
|
|
3679
|
+
"Auto-center mode: calculated size " +
|
|
3680
|
+
finalWidth +
|
|
3681
|
+
"x" +
|
|
3682
|
+
finalHeight +
|
|
3683
|
+
" at position (" +
|
|
3684
|
+
finalX +
|
|
3685
|
+
", " +
|
|
3686
|
+
finalY +
|
|
3687
|
+
")"
|
|
3688
|
+
);
|
|
3603
3689
|
|
|
3604
3690
|
Log.d(
|
|
3605
3691
|
TAG,
|
|
@@ -3627,24 +3713,22 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3627
3713
|
);
|
|
3628
3714
|
|
|
3629
3715
|
// Compare with expected ratio based on orientation
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
);
|
|
3647
|
-
}
|
|
3716
|
+
String[] parts = aspectRatio.split(":");
|
|
3717
|
+
if (parts.length == 2) {
|
|
3718
|
+
double expectedDisplayRatio = isPortrait
|
|
3719
|
+
? (ratioHeight / ratioWidth)
|
|
3720
|
+
: (ratioWidth / ratioHeight);
|
|
3721
|
+
double difference = Math.abs(displayedRatio - expectedDisplayRatio);
|
|
3722
|
+
Log.d(
|
|
3723
|
+
TAG,
|
|
3724
|
+
"Display ratio check - Expected: " +
|
|
3725
|
+
expectedDisplayRatio +
|
|
3726
|
+
", Actual: " +
|
|
3727
|
+
displayedRatio +
|
|
3728
|
+
", Difference: " +
|
|
3729
|
+
difference +
|
|
3730
|
+
" (tolerance should be < 0.01)"
|
|
3731
|
+
);
|
|
3648
3732
|
}
|
|
3649
3733
|
|
|
3650
3734
|
// Update layout params
|
|
@@ -3796,93 +3880,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3796
3880
|
|
|
3797
3881
|
Log.d(
|
|
3798
3882
|
TAG,
|
|
3799
|
-
"updateGridOverlayBounds: Updated grid bounds to " +
|
|
3800
|
-
cameraBounds.toString()
|
|
3801
|
-
);
|
|
3802
|
-
}
|
|
3803
|
-
}
|
|
3804
|
-
|
|
3805
|
-
private void triggerAutoFocus() {
|
|
3806
|
-
if (camera == null) {
|
|
3807
|
-
return;
|
|
3808
|
-
}
|
|
3809
|
-
|
|
3810
|
-
Log.d(TAG, "triggerAutoFocus: Triggering autofocus at center");
|
|
3811
|
-
|
|
3812
|
-
// Cancel any ongoing focus operation
|
|
3813
|
-
if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
|
|
3814
|
-
Log.d(TAG, "triggerAutoFocus: Cancelling previous focus operation");
|
|
3815
|
-
currentFocusFuture.cancel(true);
|
|
3816
|
-
}
|
|
3817
|
-
|
|
3818
|
-
// Focus on the center of the view
|
|
3819
|
-
int viewWidth = previewView.getWidth();
|
|
3820
|
-
int viewHeight = previewView.getHeight();
|
|
3821
|
-
|
|
3822
|
-
if (viewWidth == 0 || viewHeight == 0) {
|
|
3823
|
-
return;
|
|
3824
|
-
}
|
|
3825
|
-
|
|
3826
|
-
// Create MeteringPoint at the center of the preview
|
|
3827
|
-
MeteringPointFactory factory = previewView.getMeteringPointFactory();
|
|
3828
|
-
MeteringPoint point = factory.createPoint(viewWidth / 2f, viewHeight / 2f);
|
|
3829
|
-
|
|
3830
|
-
// Create focus and metering action (persistent, no auto-cancel) to match iOS behavior
|
|
3831
|
-
FocusMeteringAction action = new FocusMeteringAction.Builder(
|
|
3832
|
-
point,
|
|
3833
|
-
FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE
|
|
3834
|
-
)
|
|
3835
|
-
.disableAutoCancel()
|
|
3836
|
-
.build();
|
|
3837
|
-
|
|
3838
|
-
try {
|
|
3839
|
-
currentFocusFuture = camera
|
|
3840
|
-
.getCameraControl()
|
|
3841
|
-
.startFocusAndMetering(action);
|
|
3842
|
-
currentFocusFuture.addListener(
|
|
3843
|
-
() -> {
|
|
3844
|
-
try {
|
|
3845
|
-
FocusMeteringResult result = currentFocusFuture.get();
|
|
3846
|
-
Log.d(
|
|
3847
|
-
TAG,
|
|
3848
|
-
"triggerAutoFocus: Focus completed successfully: " +
|
|
3849
|
-
result.isFocusSuccessful()
|
|
3850
|
-
);
|
|
3851
|
-
} catch (Exception e) {
|
|
3852
|
-
// Handle cancellation gracefully - this is expected when rapid operations occur
|
|
3853
|
-
if (
|
|
3854
|
-
e.getMessage() != null &&
|
|
3855
|
-
(e
|
|
3856
|
-
.getMessage()
|
|
3857
|
-
.contains("Cancelled by another startFocusAndMetering") ||
|
|
3858
|
-
e.getMessage().contains("OperationCanceledException") ||
|
|
3859
|
-
e
|
|
3860
|
-
.getClass()
|
|
3861
|
-
.getSimpleName()
|
|
3862
|
-
.contains("OperationCanceledException"))
|
|
3863
|
-
) {
|
|
3864
|
-
Log.d(
|
|
3865
|
-
TAG,
|
|
3866
|
-
"triggerAutoFocus: Auto-focus was cancelled by a newer focus request"
|
|
3867
|
-
);
|
|
3868
|
-
} else {
|
|
3869
|
-
Log.e(TAG, "triggerAutoFocus: Error during focus", e);
|
|
3870
|
-
}
|
|
3871
|
-
} finally {
|
|
3872
|
-
// Clear the reference if this is still the current operation
|
|
3873
|
-
if (currentFocusFuture != null && currentFocusFuture.isDone()) {
|
|
3874
|
-
currentFocusFuture = null;
|
|
3875
|
-
}
|
|
3876
|
-
}
|
|
3877
|
-
},
|
|
3878
|
-
ContextCompat.getMainExecutor(context)
|
|
3883
|
+
"updateGridOverlayBounds: Updated grid bounds to " + cameraBounds
|
|
3879
3884
|
);
|
|
3880
|
-
} catch (Exception e) {
|
|
3881
|
-
currentFocusFuture = null;
|
|
3882
|
-
Log.e(TAG, "triggerAutoFocus: Failed to trigger autofocus", e);
|
|
3883
3885
|
}
|
|
3884
3886
|
}
|
|
3885
3887
|
|
|
3888
|
+
/** @noinspection ResultOfMethodCallIgnored*/
|
|
3886
3889
|
public void startRecordVideo() throws Exception {
|
|
3887
3890
|
if (videoCapture == null) {
|
|
3888
3891
|
throw new Exception("VideoCapture is not initialized");
|
|
@@ -3921,6 +3924,15 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3921
3924
|
|
|
3922
3925
|
// Start recording
|
|
3923
3926
|
if (sessionConfig != null && !sessionConfig.isDisableAudio()) {
|
|
3927
|
+
if (
|
|
3928
|
+
ActivityCompat.checkSelfPermission(
|
|
3929
|
+
context,
|
|
3930
|
+
Manifest.permission.RECORD_AUDIO
|
|
3931
|
+
) !=
|
|
3932
|
+
PackageManager.PERMISSION_GRANTED
|
|
3933
|
+
) {
|
|
3934
|
+
return;
|
|
3935
|
+
}
|
|
3924
3936
|
currentRecording = videoCapture
|
|
3925
3937
|
.getOutput()
|
|
3926
3938
|
.prepareRecording(context, outputOptions)
|
|
@@ -3945,60 +3957,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3945
3957
|
);
|
|
3946
3958
|
}
|
|
3947
3959
|
|
|
3948
|
-
private void initializeVideoCapture() throws Exception {
|
|
3949
|
-
if (isVideoCaptureInitializing) {
|
|
3950
|
-
throw new Exception("VideoCapture initialization is already in progress");
|
|
3951
|
-
}
|
|
3952
|
-
if (cameraProvider == null || camera == null) {
|
|
3953
|
-
throw new Exception("Camera is not initialized");
|
|
3954
|
-
}
|
|
3955
|
-
|
|
3956
|
-
isVideoCaptureInitializing = true;
|
|
3957
|
-
|
|
3958
|
-
try {
|
|
3959
|
-
// Get current rotation for video capture
|
|
3960
|
-
int rotation = previewView != null && previewView.getDisplay() != null
|
|
3961
|
-
? previewView.getDisplay().getRotation()
|
|
3962
|
-
: android.view.Surface.ROTATION_0;
|
|
3963
|
-
|
|
3964
|
-
// Setup VideoCapture with rotation and quality fallback
|
|
3965
|
-
QualitySelector qualitySelector = QualitySelector.fromOrderedList(
|
|
3966
|
-
Arrays.asList(Quality.FHD, Quality.HD, Quality.SD),
|
|
3967
|
-
FallbackStrategy.higherQualityOrLowerThan(Quality.FHD)
|
|
3968
|
-
);
|
|
3969
|
-
Recorder recorder = new Recorder.Builder()
|
|
3970
|
-
.setQualitySelector(qualitySelector)
|
|
3971
|
-
.build();
|
|
3972
|
-
videoCapture = VideoCapture.withOutput(recorder);
|
|
3973
|
-
|
|
3974
|
-
// Reuse the Preview use case we created during initial binding
|
|
3975
|
-
Preview preview = previewUseCase;
|
|
3976
|
-
|
|
3977
|
-
// Rebind with video capture included
|
|
3978
|
-
cameraProvider.unbindAll();
|
|
3979
|
-
if (preview != null) {
|
|
3980
|
-
// CRITICAL: Re-set the surface provider after unbinding
|
|
3981
|
-
// Without this, the preview won't be connected to the surface and video won't be captured
|
|
3982
|
-
preview.setSurfaceProvider(previewView.getSurfaceProvider());
|
|
3983
|
-
|
|
3984
|
-
camera = cameraProvider.bindToLifecycle(
|
|
3985
|
-
this,
|
|
3986
|
-
currentCameraSelector,
|
|
3987
|
-
preview,
|
|
3988
|
-
imageCapture,
|
|
3989
|
-
videoCapture
|
|
3990
|
-
);
|
|
3991
|
-
} else {
|
|
3992
|
-
// Shouldn't happen, but handle gracefully
|
|
3993
|
-
throw new Exception("Preview use case not found");
|
|
3994
|
-
}
|
|
3995
|
-
|
|
3996
|
-
Log.d(TAG, "VideoCapture initialized successfully");
|
|
3997
|
-
} finally {
|
|
3998
|
-
isVideoCaptureInitializing = false;
|
|
3999
|
-
}
|
|
4000
|
-
}
|
|
4001
|
-
|
|
4002
3960
|
public void stopRecordVideo(VideoRecordingCallback callback) {
|
|
4003
3961
|
if (currentRecording == null) {
|
|
4004
3962
|
callback.onError("No video recording in progress");
|