@capgo/camera-preview 7.14.8 → 7.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/android/build.gradle +1 -1
- 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 +576 -624
- 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,125 @@ 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
|
+
// If JPEG and we have source EXIF, copy EXIF tags into the saved file
|
|
363
|
+
if ("image/jpeg".equals(mimeType) && sourceExif != null) {
|
|
364
|
+
try {
|
|
365
|
+
ExifInterface newExif = new ExifInterface(photo.getAbsolutePath());
|
|
366
|
+
for (String[] tag : EXIF_TAGS) {
|
|
367
|
+
String value = sourceExif.getAttribute(tag[0]);
|
|
368
|
+
if (value != null) newExif.setAttribute(tag[0], value);
|
|
369
|
+
}
|
|
370
|
+
// Normalize orientation for recompressed pixels
|
|
371
|
+
newExif.setAttribute(
|
|
372
|
+
ExifInterface.TAG_ORIENTATION,
|
|
373
|
+
Integer.toString(ExifInterface.ORIENTATION_NORMAL)
|
|
374
|
+
);
|
|
375
|
+
if (
|
|
376
|
+
finalWidth != null &&
|
|
377
|
+
finalHeight != null &&
|
|
378
|
+
finalWidth > 0 &&
|
|
379
|
+
finalHeight > 0
|
|
380
|
+
) {
|
|
381
|
+
newExif.setAttribute(
|
|
382
|
+
ExifInterface.TAG_PIXEL_X_DIMENSION,
|
|
383
|
+
String.valueOf(finalWidth)
|
|
384
|
+
);
|
|
385
|
+
newExif.setAttribute(
|
|
386
|
+
ExifInterface.TAG_PIXEL_Y_DIMENSION,
|
|
387
|
+
String.valueOf(finalHeight)
|
|
388
|
+
);
|
|
389
|
+
newExif.setAttribute(
|
|
390
|
+
ExifInterface.TAG_IMAGE_WIDTH,
|
|
391
|
+
String.valueOf(finalWidth)
|
|
392
|
+
);
|
|
393
|
+
newExif.setAttribute(
|
|
394
|
+
ExifInterface.TAG_IMAGE_LENGTH,
|
|
395
|
+
String.valueOf(finalHeight)
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
newExif.saveAttributes();
|
|
399
|
+
} catch (Exception ex) {
|
|
400
|
+
Log.w(TAG, "Failed to write EXIF to saved gallery image", ex);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Notify the gallery of the new image
|
|
405
|
+
MediaScannerConnection.scanFile(
|
|
406
|
+
this.context,
|
|
407
|
+
new String[] { photo.getAbsolutePath() },
|
|
408
|
+
new String[] { mimeType },
|
|
409
|
+
null
|
|
410
|
+
);
|
|
411
|
+
} catch (IOException e) {
|
|
412
|
+
Log.e(TAG, "Error saving image to gallery (with exif)", e);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
272
416
|
public void startSession(CameraSessionConfiguration config) {
|
|
273
417
|
this.sessionConfig = config;
|
|
274
418
|
cameraExecutor = Executors.newSingleThreadExecutor();
|
|
419
|
+
synchronized (operationLock) {
|
|
420
|
+
activeOperations = 0;
|
|
421
|
+
stopPending = false;
|
|
422
|
+
}
|
|
275
423
|
mainExecutor.execute(() -> {
|
|
276
424
|
lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
|
|
277
425
|
setupCamera();
|
|
@@ -279,26 +427,38 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
279
427
|
}
|
|
280
428
|
|
|
281
429
|
public void stopSession() {
|
|
282
|
-
//
|
|
283
|
-
synchronized (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
430
|
+
// Mark stop pending; reject new operations and wait for active ones to finish
|
|
431
|
+
synchronized (operationLock) {
|
|
432
|
+
stopPending = true;
|
|
433
|
+
}
|
|
434
|
+
stopRequested = true;
|
|
435
|
+
|
|
436
|
+
boolean hasOps;
|
|
437
|
+
synchronized (operationLock) {
|
|
438
|
+
hasOps = activeOperations > 0;
|
|
439
|
+
}
|
|
440
|
+
if (hasOps) {
|
|
441
|
+
// Detach preview so UI can close
|
|
442
|
+
if (!previewDetachedOnDeferredStop) {
|
|
443
|
+
mainExecutor.execute(() -> {
|
|
444
|
+
try {
|
|
445
|
+
if (previewContainer != null) {
|
|
446
|
+
ViewGroup parent = (ViewGroup) previewContainer.getParent();
|
|
447
|
+
if (parent != null) {
|
|
448
|
+
parent.removeView(previewContainer);
|
|
295
449
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
})
|
|
299
|
-
}
|
|
300
|
-
return;
|
|
450
|
+
}
|
|
451
|
+
previewDetachedOnDeferredStop = true;
|
|
452
|
+
} catch (Exception ignored) {}
|
|
453
|
+
});
|
|
301
454
|
}
|
|
455
|
+
// Cancel focus to hasten completion
|
|
456
|
+
if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
|
|
457
|
+
try {
|
|
458
|
+
currentFocusFuture.cancel(true);
|
|
459
|
+
} catch (Exception ignored) {}
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
302
462
|
}
|
|
303
463
|
|
|
304
464
|
performImmediateStop();
|
|
@@ -328,6 +488,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
328
488
|
} finally {
|
|
329
489
|
stopRequested = false;
|
|
330
490
|
previewDetachedOnDeferredStop = false;
|
|
491
|
+
synchronized (operationLock) {
|
|
492
|
+
activeOperations = 0;
|
|
493
|
+
stopPending = false;
|
|
494
|
+
}
|
|
331
495
|
if (listener != null) {
|
|
332
496
|
try {
|
|
333
497
|
listener.onCameraStopped();
|
|
@@ -381,6 +545,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
381
545
|
|
|
382
546
|
// Create and setup the preview view
|
|
383
547
|
previewView = new PreviewView(context);
|
|
548
|
+
// Use TextureView-backed implementation for broader device compatibility when overlaying with WebView
|
|
549
|
+
// This avoids SurfaceView z-order issues seen on some MIUI/EMUI devices.
|
|
550
|
+
previewView.setImplementationMode(
|
|
551
|
+
PreviewView.ImplementationMode.COMPATIBLE
|
|
552
|
+
);
|
|
384
553
|
// Match iOS behavior: FIT when no aspect ratio, FILL when aspect ratio is set
|
|
385
554
|
String initialAspectRatio = sessionConfig != null
|
|
386
555
|
? sessionConfig.getAspectRatio()
|
|
@@ -394,58 +563,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
394
563
|
previewView.setClickable(true);
|
|
395
564
|
previewView.setFocusable(true);
|
|
396
565
|
|
|
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);
|
|
566
|
+
// Intentionally no native gesture handling (tap-to-focus, pinch-to-zoom)
|
|
567
|
+
// Focus and zoom are controlled exclusively via JS API calls for parity with iOS.
|
|
449
568
|
|
|
450
569
|
previewContainer.addView(
|
|
451
570
|
previewView,
|
|
@@ -578,6 +697,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
578
697
|
int webViewHeight = webView != null ? webView.getHeight() : 0;
|
|
579
698
|
|
|
580
699
|
// Get parent dimensions
|
|
700
|
+
assert webView != null;
|
|
581
701
|
ViewGroup parent = (ViewGroup) webView.getParent();
|
|
582
702
|
int parentWidth = parent != null ? parent.getWidth() : 0;
|
|
583
703
|
int parentHeight = parent != null ? parent.getHeight() : 0;
|
|
@@ -663,20 +783,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
663
783
|
);
|
|
664
784
|
|
|
665
785
|
// For centered mode with aspect ratio, calculate maximum size that fits
|
|
666
|
-
int availableWidth = screenWidthPx;
|
|
667
|
-
int availableHeight = screenHeightPx;
|
|
668
786
|
|
|
669
787
|
Log.d(
|
|
670
788
|
TAG,
|
|
671
789
|
"Available space for preview: " +
|
|
672
|
-
|
|
790
|
+
screenWidthPx +
|
|
673
791
|
"x" +
|
|
674
|
-
|
|
792
|
+
screenHeightPx
|
|
675
793
|
);
|
|
676
794
|
|
|
677
795
|
// Calculate maximum size that fits the aspect ratio in available space
|
|
678
|
-
double maxWidthByHeight =
|
|
679
|
-
double maxHeightByWidth =
|
|
796
|
+
double maxWidthByHeight = screenHeightPx * ratio;
|
|
797
|
+
double maxHeightByWidth = screenWidthPx / ratio;
|
|
680
798
|
|
|
681
799
|
Log.d(
|
|
682
800
|
TAG,
|
|
@@ -686,21 +804,21 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
686
804
|
maxHeightByWidth
|
|
687
805
|
);
|
|
688
806
|
|
|
689
|
-
if (maxWidthByHeight <=
|
|
807
|
+
if (maxWidthByHeight <= screenWidthPx) {
|
|
690
808
|
// Height is the limiting factor
|
|
691
809
|
width = (int) maxWidthByHeight;
|
|
692
|
-
height =
|
|
810
|
+
height = screenHeightPx;
|
|
693
811
|
Log.d(TAG, "Height-limited sizing: " + width + "x" + height);
|
|
694
812
|
} else {
|
|
695
813
|
// Width is the limiting factor
|
|
696
|
-
width =
|
|
814
|
+
width = screenWidthPx;
|
|
697
815
|
height = (int) maxHeightByWidth;
|
|
698
816
|
Log.d(TAG, "Width-limited sizing: " + width + "x" + height);
|
|
699
817
|
}
|
|
700
818
|
|
|
701
819
|
// Center the preview
|
|
702
|
-
x = (
|
|
703
|
-
y = (
|
|
820
|
+
x = (screenWidthPx - width) / 2;
|
|
821
|
+
y = (screenHeightPx - height) / 2;
|
|
704
822
|
|
|
705
823
|
Log.d(TAG, "Auto-centered position: x=" + x + ", y=" + y);
|
|
706
824
|
} catch (NumberFormatException e) {
|
|
@@ -805,7 +923,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
805
923
|
.setTargetRotation(rotation)
|
|
806
924
|
.build();
|
|
807
925
|
// Keep reference to preview use case for later re-binding (e.g., when enabling video)
|
|
808
|
-
previewUseCase = preview;
|
|
809
926
|
imageCapture = new ImageCapture.Builder()
|
|
810
927
|
.setResolutionSelector(resolutionSelector)
|
|
811
928
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
|
@@ -1062,25 +1179,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1062
1179
|
return builder.build();
|
|
1063
1180
|
}
|
|
1064
1181
|
|
|
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
1182
|
private static boolean isBackCamera(
|
|
1085
1183
|
androidx.camera.core.CameraInfo cameraInfo
|
|
1086
1184
|
) {
|
|
@@ -1108,6 +1206,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1108
1206
|
Integer height,
|
|
1109
1207
|
Location location
|
|
1110
1208
|
) {
|
|
1209
|
+
// Prevent capture if a stop is pending
|
|
1210
|
+
if (IsOperationRunning("capturePhoto")) {
|
|
1211
|
+
Log.d(TAG, "capturePhoto: Ignored because stop is pending");
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1111
1214
|
Log.d(
|
|
1112
1215
|
TAG,
|
|
1113
1216
|
"capturePhoto: Starting photo capture with quality: " +
|
|
@@ -1129,9 +1232,15 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1129
1232
|
isCapturingPhoto = true;
|
|
1130
1233
|
}
|
|
1131
1234
|
|
|
1132
|
-
|
|
1235
|
+
ByteArrayOutputStream imageStream = new ByteArrayOutputStream();
|
|
1236
|
+
ImageCapture.Metadata metadata = new ImageCapture.Metadata();
|
|
1237
|
+
if (location != null) {
|
|
1238
|
+
metadata.setLocation(location);
|
|
1239
|
+
}
|
|
1133
1240
|
ImageCapture.OutputFileOptions outputFileOptions =
|
|
1134
|
-
new ImageCapture.OutputFileOptions.Builder(
|
|
1241
|
+
new ImageCapture.OutputFileOptions.Builder(imageStream)
|
|
1242
|
+
.setMetadata(metadata)
|
|
1243
|
+
.build();
|
|
1135
1244
|
|
|
1136
1245
|
imageCapture.takePicture(
|
|
1137
1246
|
outputFileOptions,
|
|
@@ -1152,6 +1261,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1152
1261
|
performImmediateStop();
|
|
1153
1262
|
}
|
|
1154
1263
|
}
|
|
1264
|
+
endOperation("capturePhoto");
|
|
1155
1265
|
}
|
|
1156
1266
|
|
|
1157
1267
|
@Override
|
|
@@ -1159,20 +1269,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1159
1269
|
@NonNull ImageCapture.OutputFileResults output
|
|
1160
1270
|
) {
|
|
1161
1271
|
try {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
fis.read(bytes);
|
|
1166
|
-
fis.close();
|
|
1272
|
+
byte[] bytes = imageStream.toByteArray();
|
|
1273
|
+
int finalWidthOut = -1;
|
|
1274
|
+
int finalHeightOut = -1;
|
|
1167
1275
|
|
|
1168
1276
|
ExifInterface exifInterface = new ExifInterface(
|
|
1169
|
-
|
|
1277
|
+
new ByteArrayInputStream(bytes)
|
|
1170
1278
|
);
|
|
1171
|
-
|
|
1172
|
-
if (location != null) {
|
|
1173
|
-
exifInterface.setGpsInfo(location);
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1279
|
+
// Build EXIF JSON from captured bytes (location applied by metadata if provided)
|
|
1176
1280
|
JSONObject exifData = getExifData(exifInterface);
|
|
1177
1281
|
|
|
1178
1282
|
if (width != null || height != null) {
|
|
@@ -1181,6 +1285,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1181
1285
|
0,
|
|
1182
1286
|
bytes.length
|
|
1183
1287
|
);
|
|
1288
|
+
bitmap = applyExifOrientation(bitmap, exifInterface);
|
|
1184
1289
|
Bitmap resizedBitmap = resizeBitmapToMaxDimensions(
|
|
1185
1290
|
bitmap,
|
|
1186
1291
|
width,
|
|
@@ -1194,8 +1299,19 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1194
1299
|
);
|
|
1195
1300
|
bytes = stream.toByteArray();
|
|
1196
1301
|
|
|
1197
|
-
//
|
|
1198
|
-
|
|
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();
|
|
1199
1315
|
} else {
|
|
1200
1316
|
// No explicit size/ratio: crop to match current preview content
|
|
1201
1317
|
Bitmap originalBitmap = BitmapFactory.decodeByteArray(
|
|
@@ -1203,6 +1319,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1203
1319
|
0,
|
|
1204
1320
|
bytes.length
|
|
1205
1321
|
);
|
|
1322
|
+
originalBitmap = applyExifOrientation(
|
|
1323
|
+
originalBitmap,
|
|
1324
|
+
exifInterface
|
|
1325
|
+
);
|
|
1206
1326
|
Bitmap previewCropped = cropBitmapToMatchPreview(originalBitmap);
|
|
1207
1327
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
1208
1328
|
previewCropped.compress(
|
|
@@ -1211,16 +1331,30 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1211
1331
|
stream
|
|
1212
1332
|
);
|
|
1213
1333
|
bytes = stream.toByteArray();
|
|
1214
|
-
//
|
|
1215
|
-
|
|
1334
|
+
// Update EXIF JSON to reflect cropped dimensions; no in-place EXIF write to bytes
|
|
1335
|
+
try {
|
|
1336
|
+
exifData.put("PixelXDimension", previewCropped.getWidth());
|
|
1337
|
+
exifData.put("PixelYDimension", previewCropped.getHeight());
|
|
1338
|
+
exifData.put("ImageWidth", previewCropped.getWidth());
|
|
1339
|
+
exifData.put("ImageLength", previewCropped.getHeight());
|
|
1340
|
+
exifData.put(
|
|
1341
|
+
"Orientation",
|
|
1342
|
+
Integer.toString(ExifInterface.ORIENTATION_NORMAL)
|
|
1343
|
+
);
|
|
1344
|
+
} catch (Exception ignore) {}
|
|
1345
|
+
finalWidthOut = previewCropped.getWidth();
|
|
1346
|
+
finalHeightOut = previewCropped.getHeight();
|
|
1216
1347
|
}
|
|
1217
1348
|
|
|
1218
|
-
// Save to gallery asynchronously if requested
|
|
1349
|
+
// Save to gallery asynchronously if requested, copy EXIF to file
|
|
1219
1350
|
if (saveToGallery) {
|
|
1220
1351
|
final byte[] finalBytes = bytes;
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1352
|
+
final ExifInterface exifForFile = exifInterface;
|
|
1353
|
+
final Integer fW = (finalWidthOut > 0) ? finalWidthOut : null;
|
|
1354
|
+
final Integer fH = (finalHeightOut > 0) ? finalHeightOut : null;
|
|
1355
|
+
new Thread(() ->
|
|
1356
|
+
saveImageToGallery(finalBytes, exifForFile, fW, fH)
|
|
1357
|
+
).start();
|
|
1224
1358
|
}
|
|
1225
1359
|
|
|
1226
1360
|
String resultValue;
|
|
@@ -1241,6 +1375,42 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1241
1375
|
outFos.write(bytes);
|
|
1242
1376
|
outFos.close();
|
|
1243
1377
|
|
|
1378
|
+
// Write EXIF into the saved file
|
|
1379
|
+
try {
|
|
1380
|
+
ExifInterface newExif = new ExifInterface(
|
|
1381
|
+
outFile.getAbsolutePath()
|
|
1382
|
+
);
|
|
1383
|
+
for (String[] tag : EXIF_TAGS) {
|
|
1384
|
+
String value = exifInterface.getAttribute(tag[0]);
|
|
1385
|
+
if (value != null) newExif.setAttribute(tag[0], value);
|
|
1386
|
+
}
|
|
1387
|
+
newExif.setAttribute(
|
|
1388
|
+
ExifInterface.TAG_ORIENTATION,
|
|
1389
|
+
Integer.toString(ExifInterface.ORIENTATION_NORMAL)
|
|
1390
|
+
);
|
|
1391
|
+
if (finalWidthOut > 0 && finalHeightOut > 0) {
|
|
1392
|
+
newExif.setAttribute(
|
|
1393
|
+
ExifInterface.TAG_PIXEL_X_DIMENSION,
|
|
1394
|
+
String.valueOf(finalWidthOut)
|
|
1395
|
+
);
|
|
1396
|
+
newExif.setAttribute(
|
|
1397
|
+
ExifInterface.TAG_PIXEL_Y_DIMENSION,
|
|
1398
|
+
String.valueOf(finalHeightOut)
|
|
1399
|
+
);
|
|
1400
|
+
newExif.setAttribute(
|
|
1401
|
+
ExifInterface.TAG_IMAGE_WIDTH,
|
|
1402
|
+
String.valueOf(finalWidthOut)
|
|
1403
|
+
);
|
|
1404
|
+
newExif.setAttribute(
|
|
1405
|
+
ExifInterface.TAG_IMAGE_LENGTH,
|
|
1406
|
+
String.valueOf(finalHeightOut)
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
newExif.saveAttributes();
|
|
1410
|
+
} catch (Exception ex) {
|
|
1411
|
+
Log.w(TAG, "Failed to embed EXIF into output file", ex);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1244
1414
|
// Return a file path; apps can convert via Capacitor.convertFileSrc on JS side
|
|
1245
1415
|
resultValue = outFile.getAbsolutePath();
|
|
1246
1416
|
} catch (IOException ioEx) {
|
|
@@ -1253,8 +1423,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1253
1423
|
resultValue = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
1254
1424
|
}
|
|
1255
1425
|
|
|
1256
|
-
tempFile.delete();
|
|
1257
|
-
|
|
1258
1426
|
if (listener != null) {
|
|
1259
1427
|
listener.onPictureTaken(resultValue, exifData);
|
|
1260
1428
|
}
|
|
@@ -1273,14 +1441,56 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1273
1441
|
performImmediateStop();
|
|
1274
1442
|
}
|
|
1275
1443
|
}
|
|
1444
|
+
endOperation("capturePhoto");
|
|
1276
1445
|
}
|
|
1277
1446
|
}
|
|
1278
1447
|
}
|
|
1279
1448
|
);
|
|
1280
1449
|
}
|
|
1281
1450
|
|
|
1282
|
-
private
|
|
1283
|
-
|
|
1451
|
+
private int exifToDegrees(int exifOrientation) {
|
|
1452
|
+
switch (exifOrientation) {
|
|
1453
|
+
case ExifInterface.ORIENTATION_ROTATE_90:
|
|
1454
|
+
case ExifInterface.ORIENTATION_TRANSPOSE:
|
|
1455
|
+
return 90;
|
|
1456
|
+
case ExifInterface.ORIENTATION_ROTATE_180:
|
|
1457
|
+
return 180;
|
|
1458
|
+
case ExifInterface.ORIENTATION_ROTATE_270:
|
|
1459
|
+
case ExifInterface.ORIENTATION_TRANSVERSE:
|
|
1460
|
+
return 270;
|
|
1461
|
+
default:
|
|
1462
|
+
return 0;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
private Bitmap applyExifOrientation(Bitmap bitmap, ExifInterface exif) {
|
|
1467
|
+
try {
|
|
1468
|
+
int orientation = exif.getAttributeInt(
|
|
1469
|
+
ExifInterface.TAG_ORIENTATION,
|
|
1470
|
+
ExifInterface.ORIENTATION_UNDEFINED
|
|
1471
|
+
);
|
|
1472
|
+
int rotation = exifToDegrees(orientation);
|
|
1473
|
+
if (rotation == 0) return bitmap;
|
|
1474
|
+
Matrix m = new Matrix();
|
|
1475
|
+
m.postRotate(rotation);
|
|
1476
|
+
Bitmap rotated = Bitmap.createBitmap(
|
|
1477
|
+
bitmap,
|
|
1478
|
+
0,
|
|
1479
|
+
0,
|
|
1480
|
+
bitmap.getWidth(),
|
|
1481
|
+
bitmap.getHeight(),
|
|
1482
|
+
m,
|
|
1483
|
+
true
|
|
1484
|
+
);
|
|
1485
|
+
if (rotated != bitmap) {
|
|
1486
|
+
try {
|
|
1487
|
+
bitmap.recycle();
|
|
1488
|
+
} catch (Exception ignore) {}
|
|
1489
|
+
}
|
|
1490
|
+
return rotated;
|
|
1491
|
+
} catch (Exception e) {
|
|
1492
|
+
return bitmap;
|
|
1493
|
+
}
|
|
1284
1494
|
}
|
|
1285
1495
|
|
|
1286
1496
|
private Bitmap resizeBitmapToMaxDimensions(
|
|
@@ -1292,7 +1502,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1292
1502
|
int originalHeight = bitmap.getHeight();
|
|
1293
1503
|
float originalAspectRatio = (float) originalWidth / originalHeight;
|
|
1294
1504
|
|
|
1295
|
-
int targetWidth
|
|
1505
|
+
int targetWidth;
|
|
1296
1506
|
int targetHeight = originalHeight;
|
|
1297
1507
|
|
|
1298
1508
|
if (maxWidth != null && maxHeight != null) {
|
|
@@ -1501,54 +1711,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1501
1711
|
{ ExifInterface.TAG_Y_RESOLUTION, "YResolution" },
|
|
1502
1712
|
};
|
|
1503
1713
|
|
|
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
|
-
}
|
|
1714
|
+
// Note: We avoid temporary files for EXIF writes. When we transform pixels (resize/crop),
|
|
1715
|
+
// we recompress JPEG in-memory and update EXIF info only in the returned JSON, not in the bytes.
|
|
1550
1716
|
|
|
1551
1717
|
public void captureSample(int quality) {
|
|
1718
|
+
if (IsOperationRunning("captureSample")) {
|
|
1719
|
+
Log.d(TAG, "captureSample: Ignored because stop is pending");
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1552
1722
|
Log.d(
|
|
1553
1723
|
TAG,
|
|
1554
1724
|
"captureSample: Starting sample capture with quality: " + quality
|
|
@@ -1572,10 +1742,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1572
1742
|
"Sample capture failed: " + exception.getMessage()
|
|
1573
1743
|
);
|
|
1574
1744
|
}
|
|
1745
|
+
endOperation("captureSample");
|
|
1575
1746
|
}
|
|
1576
1747
|
|
|
1577
1748
|
@Override
|
|
1578
1749
|
public void onCaptureSuccess(@NonNull ImageProxy image) {
|
|
1750
|
+
//noinspection TryFinallyCanBeTryWithResources
|
|
1579
1751
|
try {
|
|
1580
1752
|
// Convert ImageProxy to byte array
|
|
1581
1753
|
byte[] bytes = imageProxyToByteArray(image);
|
|
@@ -1593,6 +1765,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1593
1765
|
}
|
|
1594
1766
|
} finally {
|
|
1595
1767
|
image.close();
|
|
1768
|
+
endOperation("captureSample");
|
|
1596
1769
|
}
|
|
1597
1770
|
}
|
|
1598
1771
|
}
|
|
@@ -1653,13 +1826,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1653
1826
|
// not workin for xiaomi https://xiaomi.eu/community/threads/mi-11-ultra-unable-to-access-camera-lenses-in-apps-camera2-api.61456/
|
|
1654
1827
|
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
1655
1828
|
public static List<
|
|
1656
|
-
|
|
1829
|
+
app.capgo.capacitor.camera.preview.model.CameraDevice
|
|
1657
1830
|
> getAvailableDevicesStatic(Context context) {
|
|
1658
1831
|
Log.d(
|
|
1659
1832
|
TAG,
|
|
1660
1833
|
"getAvailableDevicesStatic: Starting CameraX device enumeration with getPhysicalCameraInfos."
|
|
1661
1834
|
);
|
|
1662
|
-
List<
|
|
1835
|
+
List<app.capgo.capacitor.camera.preview.model.CameraDevice> devices =
|
|
1663
1836
|
new ArrayList<>();
|
|
1664
1837
|
try {
|
|
1665
1838
|
ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
|
|
@@ -1683,7 +1856,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1683
1856
|
List<LensInfo> logicalLenses = new ArrayList<>();
|
|
1684
1857
|
logicalLenses.add(new LensInfo(4.25f, "wideAngle", 1.0f, maxZoom));
|
|
1685
1858
|
devices.add(
|
|
1686
|
-
new
|
|
1859
|
+
new app.capgo.capacitor.camera.preview.model.CameraDevice(
|
|
1687
1860
|
logicalCameraId,
|
|
1688
1861
|
"Logical Camera (" + position + ")",
|
|
1689
1862
|
position,
|
|
@@ -1782,7 +1955,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1782
1955
|
);
|
|
1783
1956
|
|
|
1784
1957
|
devices.add(
|
|
1785
|
-
new
|
|
1958
|
+
new app.capgo.capacitor.camera.preview.model.CameraDevice(
|
|
1786
1959
|
physicalId,
|
|
1787
1960
|
label,
|
|
1788
1961
|
position,
|
|
@@ -1930,6 +2103,29 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1930
2103
|
}
|
|
1931
2104
|
|
|
1932
2105
|
public void setFocus(float x, float y) throws Exception {
|
|
2106
|
+
// Ignore focus if capture/stop is in progress or view is gone
|
|
2107
|
+
synchronized (captureLock) {
|
|
2108
|
+
if (isCapturingPhoto || stopRequested) {
|
|
2109
|
+
Log.d(TAG, "setFocus: Ignored because capture/stop in progress");
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
if (
|
|
2114
|
+
!isRunning ||
|
|
2115
|
+
camera == null ||
|
|
2116
|
+
previewView == null ||
|
|
2117
|
+
previewContainer == null
|
|
2118
|
+
) {
|
|
2119
|
+
Log.d(
|
|
2120
|
+
TAG,
|
|
2121
|
+
"setFocus: Ignored because camera/view not ready or not running"
|
|
2122
|
+
);
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
if (IsOperationRunning("setFocus")) {
|
|
2126
|
+
Log.d(TAG, "setFocus: Ignored because stop is pending");
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
1933
2129
|
if (camera == null) {
|
|
1934
2130
|
throw new Exception("Camera not initialized");
|
|
1935
2131
|
}
|
|
@@ -1955,7 +2151,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1955
2151
|
ExposureState state = camera.getCameraInfo().getExposureState();
|
|
1956
2152
|
Range<Integer> range = state.getExposureCompensationRange();
|
|
1957
2153
|
int zeroIdx = 0;
|
|
1958
|
-
if (
|
|
2154
|
+
if (!range.contains(0)) {
|
|
1959
2155
|
// Choose the closest index to 0 if 0 is not available
|
|
1960
2156
|
zeroIdx = Math.abs(range.getLower()) < Math.abs(range.getUpper())
|
|
1961
2157
|
? range.getLower()
|
|
@@ -1978,9 +2174,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1978
2174
|
// Only show focus indicator after validation passes
|
|
1979
2175
|
float indicatorX = x * viewWidth;
|
|
1980
2176
|
float indicatorY = y * viewHeight;
|
|
1981
|
-
|
|
2177
|
+
final long indicatorToken;
|
|
2178
|
+
long indicatorToken1;
|
|
2179
|
+
try {
|
|
2180
|
+
indicatorToken1 = showFocusIndicator(indicatorX, indicatorY);
|
|
2181
|
+
} catch (Exception ignore) {
|
|
2182
|
+
// If we can't show the indicator (e.g., view is gone), still proceed with metering
|
|
2183
|
+
// Use current token so hide is a no-op later
|
|
2184
|
+
indicatorToken1 = focusIndicatorAnimationId;
|
|
2185
|
+
}
|
|
1982
2186
|
|
|
1983
2187
|
// Create MeteringPoint using the preview view
|
|
2188
|
+
indicatorToken = indicatorToken1;
|
|
1984
2189
|
MeteringPointFactory factory = previewView.getMeteringPointFactory();
|
|
1985
2190
|
MeteringPoint point = factory.createPoint(x * viewWidth, y * viewHeight);
|
|
1986
2191
|
|
|
@@ -2026,6 +2231,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2026
2231
|
if (currentFocusFuture == future && currentFocusFuture.isDone()) {
|
|
2027
2232
|
currentFocusFuture = null;
|
|
2028
2233
|
}
|
|
2234
|
+
hideFocusIndicator(indicatorToken);
|
|
2235
|
+
endOperation("setFocus");
|
|
2029
2236
|
}
|
|
2030
2237
|
},
|
|
2031
2238
|
ContextCompat.getMainExecutor(context)
|
|
@@ -2033,6 +2240,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2033
2240
|
} catch (Exception e) {
|
|
2034
2241
|
currentFocusFuture = null;
|
|
2035
2242
|
Log.e(TAG, "Failed to set focus: " + e.getMessage());
|
|
2243
|
+
hideFocusIndicator(indicatorToken);
|
|
2244
|
+
endOperation("setFocus");
|
|
2036
2245
|
throw e;
|
|
2037
2246
|
}
|
|
2038
2247
|
}
|
|
@@ -2056,40 +2265,36 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2056
2265
|
}
|
|
2057
2266
|
String normalized = mode.toUpperCase(Locale.US);
|
|
2058
2267
|
|
|
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);
|
|
2268
|
+
Camera2CameraControl c2 = Camera2CameraControl.from(
|
|
2269
|
+
camera.getCameraControl()
|
|
2270
|
+
);
|
|
2271
|
+
switch (normalized) {
|
|
2272
|
+
case "LOCK": {
|
|
2273
|
+
CaptureRequestOptions opts = new CaptureRequestOptions.Builder()
|
|
2274
|
+
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_LOCK, true)
|
|
2275
|
+
.setCaptureRequestOption(
|
|
2276
|
+
CaptureRequest.CONTROL_AE_MODE,
|
|
2277
|
+
CaptureRequest.CONTROL_AE_MODE_ON
|
|
2278
|
+
)
|
|
2279
|
+
.build();
|
|
2280
|
+
mainExecutor.execute(() -> c2.setCaptureRequestOptions(opts));
|
|
2281
|
+
currentExposureMode = "LOCK";
|
|
2282
|
+
break;
|
|
2090
2283
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2284
|
+
case "CONTINUOUS": {
|
|
2285
|
+
CaptureRequestOptions opts = new CaptureRequestOptions.Builder()
|
|
2286
|
+
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_LOCK, false)
|
|
2287
|
+
.setCaptureRequestOption(
|
|
2288
|
+
CaptureRequest.CONTROL_AE_MODE,
|
|
2289
|
+
CaptureRequest.CONTROL_AE_MODE_ON
|
|
2290
|
+
)
|
|
2291
|
+
.build();
|
|
2292
|
+
mainExecutor.execute(() -> c2.setCaptureRequestOptions(opts));
|
|
2293
|
+
currentExposureMode = "CONTINUOUS";
|
|
2294
|
+
break;
|
|
2295
|
+
}
|
|
2296
|
+
default:
|
|
2297
|
+
throw new Exception("Unsupported exposure mode: " + mode);
|
|
2093
2298
|
}
|
|
2094
2299
|
}
|
|
2095
2300
|
|
|
@@ -2100,9 +2305,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2100
2305
|
ExposureState state = camera.getCameraInfo().getExposureState();
|
|
2101
2306
|
Range<Integer> idxRange = state.getExposureCompensationRange();
|
|
2102
2307
|
Rational step = state.getExposureCompensationStep();
|
|
2103
|
-
float evStep = step
|
|
2104
|
-
? (float) step.getNumerator() / (float) step.getDenominator()
|
|
2105
|
-
: 1.0f;
|
|
2308
|
+
float evStep = (float) step.getNumerator() / (float) step.getDenominator();
|
|
2106
2309
|
float min = idxRange.getLower() * evStep;
|
|
2107
2310
|
float max = idxRange.getUpper() * evStep;
|
|
2108
2311
|
return new float[] { min, max, evStep };
|
|
@@ -2115,9 +2318,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2115
2318
|
ExposureState state = camera.getCameraInfo().getExposureState();
|
|
2116
2319
|
int idx = state.getExposureCompensationIndex();
|
|
2117
2320
|
Rational step = state.getExposureCompensationStep();
|
|
2118
|
-
float evStep = step
|
|
2119
|
-
? (float) step.getNumerator() / (float) step.getDenominator()
|
|
2120
|
-
: 1.0f;
|
|
2321
|
+
float evStep = (float) step.getNumerator() / (float) step.getDenominator();
|
|
2121
2322
|
return idx * evStep;
|
|
2122
2323
|
}
|
|
2123
2324
|
|
|
@@ -2128,9 +2329,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2128
2329
|
ExposureState state = camera.getCameraInfo().getExposureState();
|
|
2129
2330
|
Range<Integer> idxRange = state.getExposureCompensationRange();
|
|
2130
2331
|
Rational step = state.getExposureCompensationStep();
|
|
2131
|
-
float evStep = step
|
|
2132
|
-
? (float) step.getNumerator() / (float) step.getDenominator()
|
|
2133
|
-
: 1.0f;
|
|
2332
|
+
float evStep = (float) step.getNumerator() / (float) step.getDenominator();
|
|
2134
2333
|
if (evStep <= 0f) evStep = 1.0f;
|
|
2135
2334
|
int idx = Math.round(ev / evStep);
|
|
2136
2335
|
// clamp
|
|
@@ -2139,13 +2338,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2139
2338
|
camera.getCameraControl().setExposureCompensationIndex(idx);
|
|
2140
2339
|
}
|
|
2141
2340
|
|
|
2142
|
-
private
|
|
2143
|
-
|
|
2144
|
-
return;
|
|
2145
|
-
}
|
|
2341
|
+
private long showFocusIndicator(float x, float y) {
|
|
2342
|
+
// If preview is gone (e.g., stopping/closing), bail out safely
|
|
2146
2343
|
if (previewContainer == null) {
|
|
2147
2344
|
Log.w(TAG, "showFocusIndicator: previewContainer is null");
|
|
2148
|
-
return;
|
|
2345
|
+
return focusIndicatorAnimationId;
|
|
2346
|
+
}
|
|
2347
|
+
if (sessionConfig.getDisableFocusIndicator()) {
|
|
2348
|
+
return focusIndicatorAnimationId;
|
|
2149
2349
|
}
|
|
2150
2350
|
|
|
2151
2351
|
// Check if container has been laid out
|
|
@@ -2155,17 +2355,24 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2155
2355
|
"showFocusIndicator: previewContainer not laid out yet, posting to run after layout"
|
|
2156
2356
|
);
|
|
2157
2357
|
previewContainer.post(() -> showFocusIndicator(x, y));
|
|
2158
|
-
return;
|
|
2358
|
+
return focusIndicatorAnimationId;
|
|
2159
2359
|
}
|
|
2160
2360
|
|
|
2161
|
-
// Remove any existing focus
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2361
|
+
// Remove any existing focus indicators (ensure only one is visible)
|
|
2362
|
+
try {
|
|
2363
|
+
for (int i = previewContainer.getChildCount() - 1; i >= 0; i--) {
|
|
2364
|
+
View child = previewContainer.getChildAt(i);
|
|
2365
|
+
CharSequence desc = child.getContentDescription();
|
|
2366
|
+
if (desc != null && FOCUS_INDICATOR_TAG.contentEquals(desc)) {
|
|
2367
|
+
previewContainer.removeViewAt(i);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
if (focusIndicatorView != null) {
|
|
2371
|
+
ViewGroup parent = (ViewGroup) focusIndicatorView.getParent();
|
|
2372
|
+
if (parent != null) parent.removeView(focusIndicatorView);
|
|
2373
|
+
focusIndicatorView = null;
|
|
2374
|
+
}
|
|
2375
|
+
} catch (Exception ignore) {}
|
|
2169
2376
|
|
|
2170
2377
|
// Create an elegant focus indicator
|
|
2171
2378
|
FrameLayout container = new FrameLayout(context);
|
|
@@ -2178,11 +2385,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2178
2385
|
|
|
2179
2386
|
params.leftMargin = Math.max(
|
|
2180
2387
|
0,
|
|
2181
|
-
Math.min((int) (x - size / 2), containerWidth - size)
|
|
2388
|
+
Math.min((int) (x - (float) size / 2), containerWidth - size)
|
|
2182
2389
|
);
|
|
2183
2390
|
params.topMargin = Math.max(
|
|
2184
2391
|
0,
|
|
2185
|
-
Math.min((int) (y - size / 2), containerHeight - size)
|
|
2392
|
+
Math.min((int) (y - (float) size / 2), containerHeight - size)
|
|
2186
2393
|
);
|
|
2187
2394
|
|
|
2188
2395
|
// iOS Camera style: square with mid-edge ticks
|
|
@@ -2197,7 +2404,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2197
2404
|
// Add 4 tiny mid-edge ticks inside the square
|
|
2198
2405
|
int tickLen = (int) (12 *
|
|
2199
2406
|
context.getResources().getDisplayMetrics().density);
|
|
2200
|
-
|
|
2407
|
+
// ticks should touch the sides
|
|
2201
2408
|
// Top tick (perpendicular): vertical inward from top edge
|
|
2202
2409
|
View topTick = new View(context);
|
|
2203
2410
|
FrameLayout.LayoutParams topParams = new FrameLayout.LayoutParams(
|
|
@@ -2205,7 +2412,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2205
2412
|
tickLen
|
|
2206
2413
|
);
|
|
2207
2414
|
topParams.leftMargin = (size - stroke) / 2;
|
|
2208
|
-
topParams.topMargin =
|
|
2415
|
+
topParams.topMargin = stroke;
|
|
2209
2416
|
topTick.setLayoutParams(topParams);
|
|
2210
2417
|
topTick.setBackgroundColor(Color.YELLOW);
|
|
2211
2418
|
container.addView(topTick);
|
|
@@ -2216,7 +2423,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2216
2423
|
tickLen
|
|
2217
2424
|
);
|
|
2218
2425
|
bottomParams.leftMargin = (size - stroke) / 2;
|
|
2219
|
-
bottomParams.topMargin = size -
|
|
2426
|
+
bottomParams.topMargin = size - stroke - tickLen;
|
|
2220
2427
|
bottomTick.setLayoutParams(bottomParams);
|
|
2221
2428
|
bottomTick.setBackgroundColor(Color.YELLOW);
|
|
2222
2429
|
container.addView(bottomTick);
|
|
@@ -2226,7 +2433,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2226
2433
|
tickLen,
|
|
2227
2434
|
stroke
|
|
2228
2435
|
);
|
|
2229
|
-
leftParams.leftMargin =
|
|
2436
|
+
leftParams.leftMargin = stroke;
|
|
2230
2437
|
leftParams.topMargin = (size - stroke) / 2;
|
|
2231
2438
|
leftTick.setLayoutParams(leftParams);
|
|
2232
2439
|
leftTick.setBackgroundColor(Color.YELLOW);
|
|
@@ -2237,21 +2444,22 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2237
2444
|
tickLen,
|
|
2238
2445
|
stroke
|
|
2239
2446
|
);
|
|
2240
|
-
rightParams.leftMargin = size -
|
|
2447
|
+
rightParams.leftMargin = size - stroke - tickLen;
|
|
2241
2448
|
rightParams.topMargin = (size - stroke) / 2;
|
|
2242
2449
|
rightTick.setLayoutParams(rightParams);
|
|
2243
2450
|
rightTick.setBackgroundColor(Color.YELLOW);
|
|
2244
2451
|
container.addView(rightTick);
|
|
2245
2452
|
|
|
2453
|
+
container.setContentDescription(FOCUS_INDICATOR_TAG);
|
|
2246
2454
|
focusIndicatorView = container;
|
|
2247
2455
|
// Bump animation token; everything after this must validate against this token
|
|
2248
2456
|
final long thisAnimationId = ++focusIndicatorAnimationId;
|
|
2249
2457
|
final View thisIndicatorView = focusIndicatorView;
|
|
2250
2458
|
|
|
2251
|
-
//
|
|
2252
|
-
focusIndicatorView.setAlpha(
|
|
2253
|
-
focusIndicatorView.setScaleX(
|
|
2254
|
-
focusIndicatorView.setScaleY(
|
|
2459
|
+
// Show immediately (avoid complex animations that can race with teardown)
|
|
2460
|
+
focusIndicatorView.setAlpha(1f);
|
|
2461
|
+
focusIndicatorView.setScaleX(1f);
|
|
2462
|
+
focusIndicatorView.setScaleY(1f);
|
|
2255
2463
|
focusIndicatorView.setVisibility(View.VISIBLE);
|
|
2256
2464
|
|
|
2257
2465
|
// Ensure container doesn't intercept touch events
|
|
@@ -2259,12 +2467,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2259
2467
|
container.setFocusable(false);
|
|
2260
2468
|
|
|
2261
2469
|
// 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
|
-
}
|
|
2470
|
+
focusIndicatorView.setElevation(10f);
|
|
2268
2471
|
|
|
2269
2472
|
// Add to container first
|
|
2270
2473
|
previewContainer.addView(focusIndicatorView, params);
|
|
@@ -2275,88 +2478,43 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2275
2478
|
// Force a layout pass to ensure the view is properly positioned
|
|
2276
2479
|
previewContainer.requestLayout();
|
|
2277
2480
|
|
|
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
|
|
2481
|
+
// Do not schedule delayed cleanup; indicator will be removed when focus completes
|
|
2482
|
+
return thisAnimationId;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
private void hideFocusIndicator(long token) {
|
|
2486
|
+
// If we're stopping or not running anymore, don't attempt to touch the view tree
|
|
2487
|
+
if (stopRequested || !isRunning) {
|
|
2488
|
+
focusIndicatorView = null;
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
try {
|
|
2492
|
+
mainExecutor.execute(() -> {
|
|
2493
|
+
try {
|
|
2293
2494
|
if (
|
|
2294
|
-
focusIndicatorView
|
|
2295
|
-
|
|
2296
|
-
|
|
2495
|
+
focusIndicatorView == null ||
|
|
2496
|
+
previewContainer == null ||
|
|
2497
|
+
token != focusIndicatorAnimationId
|
|
2297
2498
|
) {
|
|
2298
|
-
|
|
2299
|
-
.animate()
|
|
2300
|
-
.alpha(0.3f)
|
|
2301
|
-
.setDuration(200)
|
|
2302
|
-
.withEndAction(
|
|
2303
|
-
new Runnable() {
|
|
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
|
-
);
|
|
2499
|
+
return;
|
|
2355
2500
|
}
|
|
2501
|
+
// If the view hierarchy is already being torn down, skip safely
|
|
2502
|
+
if (!previewContainer.isAttachedToWindow()) {
|
|
2503
|
+
focusIndicatorView = null;
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
ViewGroup parent = (ViewGroup) focusIndicatorView.getParent();
|
|
2507
|
+
if (parent != null) {
|
|
2508
|
+
parent.removeView(focusIndicatorView);
|
|
2509
|
+
}
|
|
2510
|
+
} catch (Exception ignore) {} finally {
|
|
2511
|
+
focusIndicatorView = null;
|
|
2356
2512
|
}
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2513
|
+
});
|
|
2514
|
+
} catch (Exception ignore) {
|
|
2515
|
+
// Executor or Looper not available; just null out the reference
|
|
2516
|
+
focusIndicatorView = null;
|
|
2517
|
+
}
|
|
2360
2518
|
}
|
|
2361
2519
|
|
|
2362
2520
|
public static List<Size> getSupportedPictureSizes(String facing) {
|
|
@@ -2562,7 +2720,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2562
2720
|
sessionConfig.isToBack(), // toBack
|
|
2563
2721
|
sessionConfig.isStoreToFile(), // storeToFile
|
|
2564
2722
|
sessionConfig.isEnableOpacity(), // enableOpacity
|
|
2565
|
-
sessionConfig.isEnableZoom(), // enableZoom
|
|
2566
2723
|
sessionConfig.isDisableExifHeaderStripping(), // disableExifHeaderStripping
|
|
2567
2724
|
sessionConfig.isDisableAudio(), // disableAudio
|
|
2568
2725
|
sessionConfig.getZoomFactor(), // zoomFactor
|
|
@@ -2722,7 +2879,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2722
2879
|
sessionConfig.getToBack(),
|
|
2723
2880
|
sessionConfig.getStoreToFile(),
|
|
2724
2881
|
sessionConfig.getEnableOpacity(),
|
|
2725
|
-
sessionConfig.getEnableZoom(),
|
|
2726
2882
|
sessionConfig.getDisableExifHeaderStripping(),
|
|
2727
2883
|
sessionConfig.getDisableAudio(),
|
|
2728
2884
|
sessionConfig.getZoomFactor(),
|
|
@@ -2737,7 +2893,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2737
2893
|
if (isRunning && previewContainer != null) {
|
|
2738
2894
|
mainExecutor.execute(() -> {
|
|
2739
2895
|
// First update the UI layout - always pass null for x,y to force auto-centering (matching iOS)
|
|
2740
|
-
updatePreviewLayoutForAspectRatio(aspectRatio
|
|
2896
|
+
updatePreviewLayoutForAspectRatio(aspectRatio);
|
|
2741
2897
|
|
|
2742
2898
|
// Then rebind the camera with new aspect ratio configuration
|
|
2743
2899
|
Log.d(
|
|
@@ -2826,7 +2982,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2826
2982
|
sessionConfig.getToBack(),
|
|
2827
2983
|
sessionConfig.getStoreToFile(),
|
|
2828
2984
|
sessionConfig.getEnableOpacity(),
|
|
2829
|
-
sessionConfig.getEnableZoom(),
|
|
2830
2985
|
sessionConfig.getDisableExifHeaderStripping(),
|
|
2831
2986
|
sessionConfig.getDisableAudio(),
|
|
2832
2987
|
sessionConfig.getZoomFactor(),
|
|
@@ -2841,7 +2996,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2841
2996
|
if (isRunning && previewContainer != null) {
|
|
2842
2997
|
mainExecutor.execute(() -> {
|
|
2843
2998
|
// First update the UI layout - always pass null for x,y to force auto-centering (matching iOS)
|
|
2844
|
-
updatePreviewLayoutForAspectRatio(aspectRatio
|
|
2999
|
+
updatePreviewLayoutForAspectRatio(aspectRatio);
|
|
2845
3000
|
|
|
2846
3001
|
// Then rebind the camera with new aspect ratio configuration
|
|
2847
3002
|
Log.d(
|
|
@@ -2902,7 +3057,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2902
3057
|
sessionConfig.getToBack(),
|
|
2903
3058
|
sessionConfig.getStoreToFile(),
|
|
2904
3059
|
sessionConfig.getEnableOpacity(),
|
|
2905
|
-
sessionConfig.getEnableZoom(),
|
|
2906
3060
|
sessionConfig.getDisableExifHeaderStripping(),
|
|
2907
3061
|
sessionConfig.getDisableAudio(),
|
|
2908
3062
|
sessionConfig.getZoomFactor(),
|
|
@@ -3010,6 +3164,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3010
3164
|
// Swap dimensions if in portrait mode to match how PreviewView displays it
|
|
3011
3165
|
if (isPortrait) {
|
|
3012
3166
|
int temp = cameraWidth;
|
|
3167
|
+
//noinspection SuspiciousNameCombination,ReassignedVariable
|
|
3013
3168
|
cameraWidth = cameraHeight;
|
|
3014
3169
|
cameraHeight = temp;
|
|
3015
3170
|
}
|
|
@@ -3286,7 +3441,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3286
3441
|
sessionConfig.getToBack(),
|
|
3287
3442
|
sessionConfig.getStoreToFile(),
|
|
3288
3443
|
sessionConfig.getEnableOpacity(),
|
|
3289
|
-
sessionConfig.getEnableZoom(),
|
|
3290
3444
|
sessionConfig.getDisableExifHeaderStripping(),
|
|
3291
3445
|
sessionConfig.getDisableAudio(),
|
|
3292
3446
|
sessionConfig.getZoomFactor(),
|
|
@@ -3318,7 +3472,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3318
3472
|
previewContainer.post(callback);
|
|
3319
3473
|
});
|
|
3320
3474
|
} else {
|
|
3321
|
-
previewContainer.post(
|
|
3475
|
+
previewContainer.post(this::updateGridOverlayBounds);
|
|
3322
3476
|
}
|
|
3323
3477
|
} else {
|
|
3324
3478
|
// No camera rebinding needed, wait for layout to complete then call callback
|
|
@@ -3359,29 +3513,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3359
3513
|
}
|
|
3360
3514
|
|
|
3361
3515
|
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
3516
|
if (previewContainer == null || aspectRatio == null) return;
|
|
3371
3517
|
|
|
3372
3518
|
Log.d(
|
|
3373
3519
|
TAG,
|
|
3374
3520
|
"======================== UPDATE PREVIEW LAYOUT FOR ASPECT RATIO ========================"
|
|
3375
3521
|
);
|
|
3376
|
-
Log.d(
|
|
3377
|
-
TAG,
|
|
3378
|
-
"Input parameters - aspectRatio: " +
|
|
3379
|
-
aspectRatio +
|
|
3380
|
-
", x: " +
|
|
3381
|
-
x +
|
|
3382
|
-
", y: " +
|
|
3383
|
-
y
|
|
3384
|
-
);
|
|
3522
|
+
Log.d(TAG, "Input parameters - aspectRatio: " + aspectRatio);
|
|
3385
3523
|
|
|
3386
3524
|
// Get comprehensive display information
|
|
3387
3525
|
WindowManager windowManager = (WindowManager) this.context.getSystemService(
|
|
@@ -3491,115 +3629,57 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3491
3629
|
);
|
|
3492
3630
|
|
|
3493
3631
|
// Get available space from webview dimensions
|
|
3494
|
-
int availableWidth = webViewWidth;
|
|
3495
|
-
int availableHeight = webViewHeight;
|
|
3496
3632
|
|
|
3497
3633
|
Log.d(
|
|
3498
3634
|
TAG,
|
|
3499
|
-
"Available space from WebView: " +
|
|
3500
|
-
availableWidth +
|
|
3501
|
-
"x" +
|
|
3502
|
-
availableHeight
|
|
3635
|
+
"Available space from WebView: " + webViewWidth + "x" + webViewHeight
|
|
3503
3636
|
);
|
|
3504
3637
|
|
|
3505
3638
|
// Calculate position and size
|
|
3506
3639
|
int finalX, finalY, finalWidth, finalHeight;
|
|
3640
|
+
// Auto-center mode - match iOS behavior exactly
|
|
3641
|
+
Log.d(TAG, "Auto-center mode");
|
|
3507
3642
|
|
|
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;
|
|
3643
|
+
// Calculate maximum size that fits the aspect ratio in available space
|
|
3644
|
+
double maxWidthByHeight = webViewHeight * ratio;
|
|
3645
|
+
double maxHeightByWidth = webViewWidth / ratio;
|
|
3533
3646
|
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
finalHeight = (int) (maxWidth / ratio);
|
|
3542
|
-
|
|
3543
|
-
if (finalHeight > maxHeight) {
|
|
3544
|
-
// Height constraint is tighter, fit by height
|
|
3545
|
-
finalHeight = maxHeight;
|
|
3546
|
-
finalWidth = (int) (maxHeight * ratio);
|
|
3547
|
-
Log.d(TAG, "Height-constrained sizing");
|
|
3548
|
-
} else {
|
|
3549
|
-
Log.d(TAG, "Width-constrained sizing");
|
|
3550
|
-
}
|
|
3647
|
+
Log.d(
|
|
3648
|
+
TAG,
|
|
3649
|
+
"Aspect ratio calculations - maxWidthByHeight: " +
|
|
3650
|
+
maxWidthByHeight +
|
|
3651
|
+
", maxHeightByWidth: " +
|
|
3652
|
+
maxHeightByWidth
|
|
3653
|
+
);
|
|
3551
3654
|
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3655
|
+
if (maxWidthByHeight <= webViewWidth) {
|
|
3656
|
+
// Height is the limiting factor
|
|
3657
|
+
finalWidth = (int) maxWidthByHeight;
|
|
3658
|
+
finalHeight = webViewHeight;
|
|
3659
|
+
Log.d(TAG, "Height-limited sizing: " + finalWidth + "x" + finalHeight);
|
|
3555
3660
|
} 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
|
-
}
|
|
3661
|
+
// Width is the limiting factor
|
|
3662
|
+
finalWidth = webViewWidth;
|
|
3663
|
+
finalHeight = (int) maxHeightByWidth;
|
|
3664
|
+
Log.d(TAG, "Width-limited sizing: " + finalWidth + "x" + finalHeight);
|
|
3665
|
+
}
|
|
3585
3666
|
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3667
|
+
// Center the preview
|
|
3668
|
+
finalX = (webViewWidth - finalWidth) / 2;
|
|
3669
|
+
finalY = (webViewHeight - finalHeight) / 2;
|
|
3589
3670
|
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
}
|
|
3671
|
+
Log.d(
|
|
3672
|
+
TAG,
|
|
3673
|
+
"Auto-center mode: calculated size " +
|
|
3674
|
+
finalWidth +
|
|
3675
|
+
"x" +
|
|
3676
|
+
finalHeight +
|
|
3677
|
+
" at position (" +
|
|
3678
|
+
finalX +
|
|
3679
|
+
", " +
|
|
3680
|
+
finalY +
|
|
3681
|
+
")"
|
|
3682
|
+
);
|
|
3603
3683
|
|
|
3604
3684
|
Log.d(
|
|
3605
3685
|
TAG,
|
|
@@ -3627,24 +3707,22 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3627
3707
|
);
|
|
3628
3708
|
|
|
3629
3709
|
// 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
|
-
}
|
|
3710
|
+
String[] parts = aspectRatio.split(":");
|
|
3711
|
+
if (parts.length == 2) {
|
|
3712
|
+
double expectedDisplayRatio = isPortrait
|
|
3713
|
+
? (ratioHeight / ratioWidth)
|
|
3714
|
+
: (ratioWidth / ratioHeight);
|
|
3715
|
+
double difference = Math.abs(displayedRatio - expectedDisplayRatio);
|
|
3716
|
+
Log.d(
|
|
3717
|
+
TAG,
|
|
3718
|
+
"Display ratio check - Expected: " +
|
|
3719
|
+
expectedDisplayRatio +
|
|
3720
|
+
", Actual: " +
|
|
3721
|
+
displayedRatio +
|
|
3722
|
+
", Difference: " +
|
|
3723
|
+
difference +
|
|
3724
|
+
" (tolerance should be < 0.01)"
|
|
3725
|
+
);
|
|
3648
3726
|
}
|
|
3649
3727
|
|
|
3650
3728
|
// Update layout params
|
|
@@ -3796,93 +3874,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3796
3874
|
|
|
3797
3875
|
Log.d(
|
|
3798
3876
|
TAG,
|
|
3799
|
-
"updateGridOverlayBounds: Updated grid bounds to " +
|
|
3800
|
-
cameraBounds.toString()
|
|
3877
|
+
"updateGridOverlayBounds: Updated grid bounds to " + cameraBounds
|
|
3801
3878
|
);
|
|
3802
3879
|
}
|
|
3803
3880
|
}
|
|
3804
3881
|
|
|
3805
|
-
|
|
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)
|
|
3879
|
-
);
|
|
3880
|
-
} catch (Exception e) {
|
|
3881
|
-
currentFocusFuture = null;
|
|
3882
|
-
Log.e(TAG, "triggerAutoFocus: Failed to trigger autofocus", e);
|
|
3883
|
-
}
|
|
3884
|
-
}
|
|
3885
|
-
|
|
3882
|
+
/** @noinspection ResultOfMethodCallIgnored*/
|
|
3886
3883
|
public void startRecordVideo() throws Exception {
|
|
3887
3884
|
if (videoCapture == null) {
|
|
3888
3885
|
throw new Exception("VideoCapture is not initialized");
|
|
@@ -3921,6 +3918,15 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3921
3918
|
|
|
3922
3919
|
// Start recording
|
|
3923
3920
|
if (sessionConfig != null && !sessionConfig.isDisableAudio()) {
|
|
3921
|
+
if (
|
|
3922
|
+
ActivityCompat.checkSelfPermission(
|
|
3923
|
+
context,
|
|
3924
|
+
Manifest.permission.RECORD_AUDIO
|
|
3925
|
+
) !=
|
|
3926
|
+
PackageManager.PERMISSION_GRANTED
|
|
3927
|
+
) {
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3924
3930
|
currentRecording = videoCapture
|
|
3925
3931
|
.getOutput()
|
|
3926
3932
|
.prepareRecording(context, outputOptions)
|
|
@@ -3945,60 +3951,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3945
3951
|
);
|
|
3946
3952
|
}
|
|
3947
3953
|
|
|
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
3954
|
public void stopRecordVideo(VideoRecordingCallback callback) {
|
|
4003
3955
|
if (currentRecording == null) {
|
|
4004
3956
|
callback.onError("No video recording in progress");
|