@capgo/camera-preview 7.3.9 → 7.4.0-beta.2
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/CapgoCameraPreview.podspec +16 -13
- package/README.md +306 -70
- package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
- package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.14.2/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
- package/android/.gradle/8.14.2/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
- package/android/.gradle/file-system.probe +0 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +9 -0
- package/android/src/main/AndroidManifest.xml +5 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +260 -551
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +968 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +54 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +70 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +65 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +34 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +34 -0
- package/dist/docs.json +729 -153
- package/dist/esm/definitions.d.ts +337 -80
- package/dist/esm/definitions.js +10 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +27 -1
- package/dist/esm/web.js +248 -4
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +256 -4
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +256 -4
- package/dist/plugin.js.map +1 -1
- package/ios/{Plugin → Sources/CapgoCameraPreview}/CameraController.swift +359 -34
- package/ios/{Plugin → Sources/CapgoCameraPreview}/Plugin.swift +348 -42
- package/ios/Tests/CameraPreviewPluginTests/CameraPreviewPluginTests.swift +15 -0
- package/package.json +1 -1
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraActivity.java +0 -1279
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CustomSurfaceView.java +0 -29
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CustomTextureView.java +0 -39
- package/android/src/main/java/com/ahm/capacitor/camera/preview/Preview.java +0 -461
- package/android/src/main/java/com/ahm/capacitor/camera/preview/TapGestureDetector.java +0 -24
- package/ios/Plugin/Info.plist +0 -24
- package/ios/Plugin/Plugin.h +0 -10
- package/ios/Plugin/Plugin.m +0 -18
- package/ios/Plugin.xcodeproj/project.pbxproj +0 -593
- package/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/ios/Plugin.xcworkspace/contents.xcworkspacedata +0 -10
- package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/ios/PluginTests/Info.plist +0 -22
- package/ios/PluginTests/PluginTests.swift +0 -83
- package/ios/Podfile +0 -13
- package/ios/Podfile.lock +0 -23
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
package com.ahm.capacitor.camera.preview;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.hardware.camera2.CameraAccessException;
|
|
5
|
+
import android.hardware.camera2.CameraManager;
|
|
6
|
+
import android.os.Build;
|
|
7
|
+
import android.util.Base64;
|
|
8
|
+
import android.util.Log;
|
|
9
|
+
import android.util.Size;
|
|
10
|
+
import android.view.ViewGroup;
|
|
11
|
+
import android.webkit.WebView;
|
|
12
|
+
import androidx.annotation.NonNull;
|
|
13
|
+
import androidx.annotation.OptIn;
|
|
14
|
+
import androidx.camera.core.Camera;
|
|
15
|
+
import androidx.camera.core.CameraInfo;
|
|
16
|
+
import androidx.camera.core.CameraSelector;
|
|
17
|
+
import androidx.camera.core.ImageCapture;
|
|
18
|
+
import androidx.camera.core.ImageCaptureException;
|
|
19
|
+
import androidx.camera.core.ImageProxy;
|
|
20
|
+
import androidx.camera.core.Preview;
|
|
21
|
+
import androidx.camera.core.resolutionselector.ResolutionSelector;
|
|
22
|
+
import androidx.camera.core.resolutionselector.ResolutionStrategy;
|
|
23
|
+
import androidx.camera.lifecycle.ProcessCameraProvider;
|
|
24
|
+
import androidx.camera.view.PreviewView;
|
|
25
|
+
import androidx.core.content.ContextCompat;
|
|
26
|
+
import androidx.lifecycle.Lifecycle;
|
|
27
|
+
import androidx.lifecycle.LifecycleOwner;
|
|
28
|
+
import androidx.lifecycle.LifecycleRegistry;
|
|
29
|
+
import com.ahm.capacitor.camera.preview.model.CameraSessionConfiguration;
|
|
30
|
+
import com.ahm.capacitor.camera.preview.model.LensInfo;
|
|
31
|
+
import com.ahm.capacitor.camera.preview.model.ZoomFactors;
|
|
32
|
+
import com.google.common.util.concurrent.ListenableFuture;
|
|
33
|
+
import java.nio.ByteBuffer;
|
|
34
|
+
import java.util.Arrays;
|
|
35
|
+
import java.util.Collections;
|
|
36
|
+
import java.util.List;
|
|
37
|
+
import java.util.ArrayList;
|
|
38
|
+
import java.util.Objects;
|
|
39
|
+
import java.util.concurrent.Executor;
|
|
40
|
+
import java.util.concurrent.ExecutorService;
|
|
41
|
+
import java.util.concurrent.Executors;
|
|
42
|
+
import androidx.camera.camera2.interop.Camera2CameraInfo;
|
|
43
|
+
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
|
|
44
|
+
import android.hardware.camera2.CameraCharacteristics;
|
|
45
|
+
import java.util.Set;
|
|
46
|
+
import androidx.camera.core.ZoomState;
|
|
47
|
+
import androidx.camera.core.ResolutionInfo;
|
|
48
|
+
import android.content.Intent;
|
|
49
|
+
import android.net.Uri;
|
|
50
|
+
import android.os.Environment;
|
|
51
|
+
import java.io.File;
|
|
52
|
+
import java.io.FileOutputStream;
|
|
53
|
+
import java.io.IOException;
|
|
54
|
+
import java.text.SimpleDateFormat;
|
|
55
|
+
import java.util.Locale;
|
|
56
|
+
import androidx.exifinterface.media.ExifInterface;
|
|
57
|
+
import org.json.JSONObject;
|
|
58
|
+
import java.nio.file.Files;
|
|
59
|
+
|
|
60
|
+
public class CameraXView implements LifecycleOwner {
|
|
61
|
+
private static final String TAG = "CameraPreview CameraXView";
|
|
62
|
+
|
|
63
|
+
public interface CameraXViewListener {
|
|
64
|
+
void onPictureTaken(String base64, JSONObject exif);
|
|
65
|
+
void onPictureTakenError(String message);
|
|
66
|
+
void onSampleTaken(String result);
|
|
67
|
+
void onSampleTakenError(String message);
|
|
68
|
+
void onCameraStarted();
|
|
69
|
+
void onCameraStartError(String message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private ProcessCameraProvider cameraProvider;
|
|
73
|
+
private Camera camera;
|
|
74
|
+
private ImageCapture imageCapture;
|
|
75
|
+
private ImageCapture sampleImageCapture;
|
|
76
|
+
private PreviewView previewView;
|
|
77
|
+
private CameraSelector currentCameraSelector;
|
|
78
|
+
private String currentDeviceId;
|
|
79
|
+
private int currentFlashMode = ImageCapture.FLASH_MODE_OFF;
|
|
80
|
+
private CameraSessionConfiguration sessionConfig;
|
|
81
|
+
private CameraXViewListener listener;
|
|
82
|
+
private final Context context;
|
|
83
|
+
private final WebView webView;
|
|
84
|
+
private final LifecycleRegistry lifecycleRegistry;
|
|
85
|
+
private final Executor mainExecutor;
|
|
86
|
+
private ExecutorService cameraExecutor;
|
|
87
|
+
private boolean isRunning = false;
|
|
88
|
+
|
|
89
|
+
public CameraXView(Context context, WebView webView) {
|
|
90
|
+
this.context = context;
|
|
91
|
+
this.webView = webView;
|
|
92
|
+
this.lifecycleRegistry = new LifecycleRegistry(this);
|
|
93
|
+
this.mainExecutor = ContextCompat.getMainExecutor(context);
|
|
94
|
+
|
|
95
|
+
mainExecutor.execute(() -> lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@NonNull
|
|
99
|
+
@Override
|
|
100
|
+
public Lifecycle getLifecycle() {
|
|
101
|
+
return lifecycleRegistry;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public void setListener(CameraXViewListener listener) {
|
|
105
|
+
this.listener = listener;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public boolean isRunning() {
|
|
109
|
+
return isRunning;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private void saveImageToGallery(byte[] data) {
|
|
113
|
+
try {
|
|
114
|
+
File photo = new File(
|
|
115
|
+
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
|
116
|
+
"IMG_" + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new java.util.Date()) + ".jpg"
|
|
117
|
+
);
|
|
118
|
+
FileOutputStream fos = new FileOutputStream(photo);
|
|
119
|
+
fos.write(data);
|
|
120
|
+
fos.close();
|
|
121
|
+
|
|
122
|
+
// Notify the gallery of the new image
|
|
123
|
+
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
|
124
|
+
Uri contentUri = Uri.fromFile(photo);
|
|
125
|
+
mediaScanIntent.setData(contentUri);
|
|
126
|
+
context.sendBroadcast(mediaScanIntent);
|
|
127
|
+
} catch (IOException e) {
|
|
128
|
+
Log.e(TAG, "Error saving image to gallery", e);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public void startSession(CameraSessionConfiguration config) {
|
|
133
|
+
this.sessionConfig = config;
|
|
134
|
+
cameraExecutor = Executors.newSingleThreadExecutor();
|
|
135
|
+
mainExecutor.execute(() -> {
|
|
136
|
+
lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
|
|
137
|
+
setupCamera();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public void stopSession() {
|
|
142
|
+
isRunning = false;
|
|
143
|
+
mainExecutor.execute(() -> {
|
|
144
|
+
if (cameraProvider != null) {
|
|
145
|
+
cameraProvider.unbindAll();
|
|
146
|
+
}
|
|
147
|
+
lifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
|
|
148
|
+
if (cameraExecutor != null) {
|
|
149
|
+
cameraExecutor.shutdownNow();
|
|
150
|
+
}
|
|
151
|
+
removePreviewView();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private void setupCamera() {
|
|
156
|
+
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(context);
|
|
157
|
+
cameraProviderFuture.addListener(() -> {
|
|
158
|
+
try {
|
|
159
|
+
cameraProvider = cameraProviderFuture.get();
|
|
160
|
+
setupPreviewView();
|
|
161
|
+
bindCameraUseCases();
|
|
162
|
+
} catch (Exception e) {
|
|
163
|
+
if (listener != null) {
|
|
164
|
+
listener.onCameraStartError("Error initializing camera: " + e.getMessage());
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}, mainExecutor);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private void setupPreviewView() {
|
|
171
|
+
if (previewView != null) {
|
|
172
|
+
removePreviewView();
|
|
173
|
+
}
|
|
174
|
+
if (sessionConfig.isToBack()) {
|
|
175
|
+
webView.setBackgroundColor(android.graphics.Color.TRANSPARENT);
|
|
176
|
+
}
|
|
177
|
+
previewView = new PreviewView(context);
|
|
178
|
+
previewView.setScaleType(PreviewView.ScaleType.FILL_CENTER);
|
|
179
|
+
ViewGroup parent = (ViewGroup) webView.getParent();
|
|
180
|
+
if (parent != null) {
|
|
181
|
+
parent.addView(previewView, new ViewGroup.LayoutParams(sessionConfig.getWidth(), sessionConfig.getHeight()));
|
|
182
|
+
if(sessionConfig.isToBack()) webView.bringToFront();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private void removePreviewView() {
|
|
187
|
+
if (previewView != null) {
|
|
188
|
+
ViewGroup parent = (ViewGroup) previewView.getParent();
|
|
189
|
+
if (parent != null) {
|
|
190
|
+
parent.removeView(previewView);
|
|
191
|
+
}
|
|
192
|
+
previewView = null;
|
|
193
|
+
}
|
|
194
|
+
webView.setBackgroundColor(android.graphics.Color.WHITE);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
198
|
+
private void bindCameraUseCases() {
|
|
199
|
+
if (cameraProvider == null) return;
|
|
200
|
+
mainExecutor.execute(() -> {
|
|
201
|
+
try {
|
|
202
|
+
Log.d(TAG, "Building camera selector with deviceId: " + sessionConfig.getDeviceId() + " and position: " + sessionConfig.getPosition());
|
|
203
|
+
currentCameraSelector = buildCameraSelector();
|
|
204
|
+
ResolutionSelector resolutionSelector = new ResolutionSelector.Builder()
|
|
205
|
+
.setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY)
|
|
206
|
+
.build();
|
|
207
|
+
Preview preview = new Preview.Builder().setResolutionSelector(resolutionSelector).build();
|
|
208
|
+
imageCapture = new ImageCapture.Builder().setResolutionSelector(resolutionSelector).setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY).setFlashMode(currentFlashMode).build();
|
|
209
|
+
sampleImageCapture = imageCapture;
|
|
210
|
+
preview.setSurfaceProvider(previewView.getSurfaceProvider());
|
|
211
|
+
// Unbind any existing use cases and bind new ones
|
|
212
|
+
cameraProvider.unbindAll();
|
|
213
|
+
camera = cameraProvider.bindToLifecycle(this, currentCameraSelector, preview, imageCapture);
|
|
214
|
+
|
|
215
|
+
// Log details about the active camera
|
|
216
|
+
Log.d(TAG, "Use cases bound. Inspecting active camera and use cases.");
|
|
217
|
+
CameraInfo cameraInfo = camera.getCameraInfo();
|
|
218
|
+
Log.d(TAG, "Bound Camera ID: " + Camera2CameraInfo.from(cameraInfo).getCameraId());
|
|
219
|
+
|
|
220
|
+
// Log zoom state
|
|
221
|
+
ZoomState zoomState = cameraInfo.getZoomState().getValue();
|
|
222
|
+
if (zoomState != null) {
|
|
223
|
+
Log.d(TAG, "Active Zoom State: " +
|
|
224
|
+
"min=" + zoomState.getMinZoomRatio() + ", " +
|
|
225
|
+
"max=" + zoomState.getMaxZoomRatio() + ", " +
|
|
226
|
+
"current=" + zoomState.getZoomRatio());
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Log physical cameras of the active camera
|
|
230
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
231
|
+
Set<CameraInfo> physicalCameras = cameraInfo.getPhysicalCameraInfos();
|
|
232
|
+
Log.d(TAG, "Active camera has " + physicalCameras.size() + " physical cameras.");
|
|
233
|
+
for (CameraInfo physical : physicalCameras) {
|
|
234
|
+
Log.d(TAG, " - Physical camera ID: " + Camera2CameraInfo.from(physical).getCameraId());
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Log resolution info
|
|
239
|
+
ResolutionInfo previewResolution = preview.getResolutionInfo();
|
|
240
|
+
if (previewResolution != null) {
|
|
241
|
+
Log.d(TAG, "Preview resolution: " + previewResolution.getResolution());
|
|
242
|
+
}
|
|
243
|
+
ResolutionInfo imageCaptureResolution = imageCapture.getResolutionInfo();
|
|
244
|
+
if (imageCaptureResolution != null) {
|
|
245
|
+
Log.d(TAG, "Image capture resolution: " + imageCaptureResolution.getResolution());
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Set initial zoom if specified, prioritizing targetZoom over default zoomFactor
|
|
249
|
+
float initialZoom = sessionConfig.getTargetZoom() != 1.0f ? sessionConfig.getTargetZoom() : sessionConfig.getZoomFactor();
|
|
250
|
+
if (initialZoom != 1.0f) {
|
|
251
|
+
Log.d(TAG, "Applying initial zoom of " + initialZoom);
|
|
252
|
+
setZoomInternal(initialZoom);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
isRunning = true;
|
|
256
|
+
Log.d(TAG, "bindCameraUseCases: Camera bound successfully");
|
|
257
|
+
if (listener != null) listener.onCameraStarted();
|
|
258
|
+
} catch (Exception e) {
|
|
259
|
+
if (listener != null) listener.onCameraStartError("Error binding camera: " + e.getMessage());
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
265
|
+
private CameraSelector buildCameraSelector() {
|
|
266
|
+
CameraSelector.Builder builder = new CameraSelector.Builder();
|
|
267
|
+
final String deviceId = sessionConfig.getDeviceId();
|
|
268
|
+
|
|
269
|
+
if (deviceId != null && !deviceId.isEmpty()) {
|
|
270
|
+
builder.addCameraFilter(cameraInfos -> {
|
|
271
|
+
for (CameraInfo cameraInfo : cameraInfos) {
|
|
272
|
+
if (deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
|
|
273
|
+
return Collections.singletonList(cameraInfo);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return Collections.emptyList();
|
|
277
|
+
});
|
|
278
|
+
} else {
|
|
279
|
+
String position = sessionConfig.getPosition();
|
|
280
|
+
int requiredFacing = "front".equals(position) ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
|
|
281
|
+
builder.requireLensFacing(requiredFacing);
|
|
282
|
+
}
|
|
283
|
+
return builder.build();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private static String getCameraId(androidx.camera.core.CameraInfo cameraInfo) {
|
|
287
|
+
try {
|
|
288
|
+
// Generate a stable ID based on camera characteristics
|
|
289
|
+
boolean isBack = isBackCamera(cameraInfo);
|
|
290
|
+
float minZoom = Objects.requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio();
|
|
291
|
+
float maxZoom = cameraInfo.getZoomState().getValue().getMaxZoomRatio();
|
|
292
|
+
|
|
293
|
+
// Create a unique ID based on camera properties
|
|
294
|
+
String position = isBack ? "back" : "front";
|
|
295
|
+
return position + "_" + minZoom + "_" + maxZoom;
|
|
296
|
+
} catch (Exception e) {
|
|
297
|
+
return "unknown_camera";
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private static boolean isBackCamera(androidx.camera.core.CameraInfo cameraInfo) {
|
|
302
|
+
try {
|
|
303
|
+
// Check if this camera matches the back camera selector
|
|
304
|
+
CameraSelector backSelector = new CameraSelector.Builder()
|
|
305
|
+
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
|
306
|
+
.build();
|
|
307
|
+
|
|
308
|
+
// Try to filter cameras with back selector - if this camera is included, it's a back camera
|
|
309
|
+
List<androidx.camera.core.CameraInfo> backCameras = backSelector.filter(Collections.singletonList(cameraInfo));
|
|
310
|
+
return !backCameras.isEmpty();
|
|
311
|
+
} catch (Exception e) {
|
|
312
|
+
Log.w(TAG, "Error determining camera direction, assuming back camera", e);
|
|
313
|
+
return true; // Default to back camera
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
public void capturePhoto(int quality, final boolean saveToGallery) {
|
|
318
|
+
Log.d(TAG, "capturePhoto: Starting photo capture with quality: " + quality);
|
|
319
|
+
|
|
320
|
+
if (imageCapture == null) {
|
|
321
|
+
if (listener != null) {
|
|
322
|
+
listener.onPictureTakenError("Camera not ready");
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
File tempFile = new File(context.getCacheDir(), "temp_image.jpg");
|
|
328
|
+
ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(tempFile).build();
|
|
329
|
+
|
|
330
|
+
imageCapture.takePicture(
|
|
331
|
+
outputFileOptions,
|
|
332
|
+
cameraExecutor,
|
|
333
|
+
new ImageCapture.OnImageSavedCallback() {
|
|
334
|
+
@Override
|
|
335
|
+
public void onError(@NonNull ImageCaptureException exception) {
|
|
336
|
+
Log.e(TAG, "capturePhoto: Photo capture failed", exception);
|
|
337
|
+
if (listener != null) {
|
|
338
|
+
listener.onPictureTakenError("Photo capture failed: " + exception.getMessage());
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@Override
|
|
343
|
+
public void onImageSaved(@NonNull ImageCapture.OutputFileResults output) {
|
|
344
|
+
try {
|
|
345
|
+
byte[] bytes = Files.readAllBytes(tempFile.toPath());
|
|
346
|
+
ExifInterface exifInterface = new ExifInterface(tempFile.getAbsolutePath());
|
|
347
|
+
JSONObject exifData = getExifData(exifInterface);
|
|
348
|
+
|
|
349
|
+
if (saveToGallery) {
|
|
350
|
+
saveImageToGallery(bytes);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
354
|
+
|
|
355
|
+
tempFile.delete();
|
|
356
|
+
|
|
357
|
+
if (listener != null) {
|
|
358
|
+
listener.onPictureTaken(base64, exifData);
|
|
359
|
+
}
|
|
360
|
+
} catch (Exception e) {
|
|
361
|
+
Log.e(TAG, "capturePhoto: Error processing image", e);
|
|
362
|
+
if (listener != null) {
|
|
363
|
+
listener.onPictureTakenError("Error processing image: " + e.getMessage());
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private JSONObject getExifData(ExifInterface exifInterface) {
|
|
372
|
+
JSONObject exifData = new JSONObject();
|
|
373
|
+
try {
|
|
374
|
+
// Add all available exif tags to a JSON object
|
|
375
|
+
for (String[] tag : EXIF_TAGS) {
|
|
376
|
+
String value = exifInterface.getAttribute(tag[0]);
|
|
377
|
+
if (value != null) {
|
|
378
|
+
exifData.put(tag[1], value);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} catch (Exception e) {
|
|
382
|
+
Log.e(TAG, "getExifData: Error reading exif data", e);
|
|
383
|
+
}
|
|
384
|
+
return exifData;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private static final String[][] EXIF_TAGS = new String[][]{
|
|
388
|
+
{ExifInterface.TAG_APERTURE_VALUE, "ApertureValue"},
|
|
389
|
+
{ExifInterface.TAG_ARTIST, "Artist"},
|
|
390
|
+
{ExifInterface.TAG_BITS_PER_SAMPLE, "BitsPerSample"},
|
|
391
|
+
{ExifInterface.TAG_BRIGHTNESS_VALUE, "BrightnessValue"},
|
|
392
|
+
{ExifInterface.TAG_CFA_PATTERN, "CFAPattern"},
|
|
393
|
+
{ExifInterface.TAG_COLOR_SPACE, "ColorSpace"},
|
|
394
|
+
{ExifInterface.TAG_COMPONENTS_CONFIGURATION, "ComponentsConfiguration"},
|
|
395
|
+
{ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL, "CompressedBitsPerPixel"},
|
|
396
|
+
{ExifInterface.TAG_COMPRESSION, "Compression"},
|
|
397
|
+
{ExifInterface.TAG_CONTRAST, "Contrast"},
|
|
398
|
+
{ExifInterface.TAG_COPYRIGHT, "Copyright"},
|
|
399
|
+
{ExifInterface.TAG_CUSTOM_RENDERED, "CustomRendered"},
|
|
400
|
+
{ExifInterface.TAG_DATETIME, "DateTime"},
|
|
401
|
+
{ExifInterface.TAG_DATETIME_DIGITIZED, "DateTimeDigitized"},
|
|
402
|
+
{ExifInterface.TAG_DATETIME_ORIGINAL, "DateTimeOriginal"},
|
|
403
|
+
{ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION, "DeviceSettingDescription"},
|
|
404
|
+
{ExifInterface.TAG_DIGITAL_ZOOM_RATIO, "DigitalZoomRatio"},
|
|
405
|
+
{ExifInterface.TAG_DNG_VERSION, "DNGVersion"},
|
|
406
|
+
{ExifInterface.TAG_EXIF_VERSION, "ExifVersion"},
|
|
407
|
+
{ExifInterface.TAG_EXPOSURE_BIAS_VALUE, "ExposureBiasValue"},
|
|
408
|
+
{ExifInterface.TAG_EXPOSURE_INDEX, "ExposureIndex"},
|
|
409
|
+
{ExifInterface.TAG_EXPOSURE_MODE, "ExposureMode"},
|
|
410
|
+
{ExifInterface.TAG_EXPOSURE_PROGRAM, "ExposureProgram"},
|
|
411
|
+
{ExifInterface.TAG_EXPOSURE_TIME, "ExposureTime"},
|
|
412
|
+
{ExifInterface.TAG_FILE_SOURCE, "FileSource"},
|
|
413
|
+
{ExifInterface.TAG_FLASH, "Flash"},
|
|
414
|
+
{ExifInterface.TAG_FLASHPIX_VERSION, "FlashpixVersion"},
|
|
415
|
+
{ExifInterface.TAG_FLASH_ENERGY, "FlashEnergy"},
|
|
416
|
+
{ExifInterface.TAG_FOCAL_LENGTH, "FocalLength"},
|
|
417
|
+
{ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM, "FocalLengthIn35mmFilm"},
|
|
418
|
+
{ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT, "FocalPlaneResolutionUnit"},
|
|
419
|
+
{ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION, "FocalPlaneXResolution"},
|
|
420
|
+
{ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION, "FocalPlaneYResolution"},
|
|
421
|
+
{ExifInterface.TAG_F_NUMBER, "FNumber"},
|
|
422
|
+
{ExifInterface.TAG_GAIN_CONTROL, "GainControl"},
|
|
423
|
+
{ExifInterface.TAG_GPS_ALTITUDE, "GPSAltitude"},
|
|
424
|
+
{ExifInterface.TAG_GPS_ALTITUDE_REF, "GPSAltitudeRef"},
|
|
425
|
+
{ExifInterface.TAG_GPS_AREA_INFORMATION, "GPSAreaInformation"},
|
|
426
|
+
{ExifInterface.TAG_GPS_DATESTAMP, "GPSDateStamp"},
|
|
427
|
+
{ExifInterface.TAG_GPS_DEST_BEARING, "GPSDestBearing"},
|
|
428
|
+
{ExifInterface.TAG_GPS_DEST_BEARING_REF, "GPSDestBearingRef"},
|
|
429
|
+
{ExifInterface.TAG_GPS_DEST_DISTANCE, "GPSDestDistance"},
|
|
430
|
+
{ExifInterface.TAG_GPS_DEST_DISTANCE_REF, "GPSDestDistanceRef"},
|
|
431
|
+
{ExifInterface.TAG_GPS_DEST_LATITUDE, "GPSDestLatitude"},
|
|
432
|
+
{ExifInterface.TAG_GPS_DEST_LATITUDE_REF, "GPSDestLatitudeRef"},
|
|
433
|
+
{ExifInterface.TAG_GPS_DEST_LONGITUDE, "GPSDestLongitude"},
|
|
434
|
+
{ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, "GPSDestLongitudeRef"},
|
|
435
|
+
{ExifInterface.TAG_GPS_DIFFERENTIAL, "GPSDifferential"},
|
|
436
|
+
{ExifInterface.TAG_GPS_DOP, "GPSDOP"},
|
|
437
|
+
{ExifInterface.TAG_GPS_IMG_DIRECTION, "GPSImgDirection"},
|
|
438
|
+
{ExifInterface.TAG_GPS_IMG_DIRECTION_REF, "GPSImgDirectionRef"},
|
|
439
|
+
{ExifInterface.TAG_GPS_LATITUDE, "GPSLatitude"},
|
|
440
|
+
{ExifInterface.TAG_GPS_LATITUDE_REF, "GPSLatitudeRef"},
|
|
441
|
+
{ExifInterface.TAG_GPS_LONGITUDE, "GPSLongitude"},
|
|
442
|
+
{ExifInterface.TAG_GPS_LONGITUDE_REF, "GPSLongitudeRef"},
|
|
443
|
+
{ExifInterface.TAG_GPS_MAP_DATUM, "GPSMapDatum"},
|
|
444
|
+
{ExifInterface.TAG_GPS_MEASURE_MODE, "GPSMeasureMode"},
|
|
445
|
+
{ExifInterface.TAG_GPS_PROCESSING_METHOD, "GPSProcessingMethod"},
|
|
446
|
+
{ExifInterface.TAG_GPS_SATELLITES, "GPSSatellites"},
|
|
447
|
+
{ExifInterface.TAG_GPS_SPEED, "GPSSpeed"},
|
|
448
|
+
{ExifInterface.TAG_GPS_SPEED_REF, "GPSSpeedRef"},
|
|
449
|
+
{ExifInterface.TAG_GPS_STATUS, "GPSStatus"},
|
|
450
|
+
{ExifInterface.TAG_GPS_TIMESTAMP, "GPSTimeStamp"},
|
|
451
|
+
{ExifInterface.TAG_GPS_TRACK, "GPSTrack"},
|
|
452
|
+
{ExifInterface.TAG_GPS_TRACK_REF, "GPSTrackRef"},
|
|
453
|
+
{ExifInterface.TAG_GPS_VERSION_ID, "GPSVersionID"},
|
|
454
|
+
{ExifInterface.TAG_IMAGE_DESCRIPTION, "ImageDescription"},
|
|
455
|
+
{ExifInterface.TAG_IMAGE_LENGTH, "ImageLength"},
|
|
456
|
+
{ExifInterface.TAG_IMAGE_UNIQUE_ID, "ImageUniqueID"},
|
|
457
|
+
{ExifInterface.TAG_IMAGE_WIDTH, "ImageWidth"},
|
|
458
|
+
{ExifInterface.TAG_INTEROPERABILITY_INDEX, "InteroperabilityIndex"},
|
|
459
|
+
{ExifInterface.TAG_ISO_SPEED, "ISOSpeed"},
|
|
460
|
+
{ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY, "ISOSpeedLatitudeyyy"},
|
|
461
|
+
{ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ, "ISOSpeedLatitudezzz"},
|
|
462
|
+
{ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, "JPEGInterchangeFormat"},
|
|
463
|
+
{ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, "JPEGInterchangeFormatLength"},
|
|
464
|
+
{ExifInterface.TAG_LIGHT_SOURCE, "LightSource"},
|
|
465
|
+
{ExifInterface.TAG_MAKE, "Make"},
|
|
466
|
+
{ExifInterface.TAG_MAKER_NOTE, "MakerNote"},
|
|
467
|
+
{ExifInterface.TAG_MAX_APERTURE_VALUE, "MaxApertureValue"},
|
|
468
|
+
{ExifInterface.TAG_METERING_MODE, "MeteringMode"},
|
|
469
|
+
{ExifInterface.TAG_MODEL, "Model"},
|
|
470
|
+
{ExifInterface.TAG_NEW_SUBFILE_TYPE, "NewSubfileType"},
|
|
471
|
+
{ExifInterface.TAG_OECF, "OECF"},
|
|
472
|
+
{ExifInterface.TAG_OFFSET_TIME, "OffsetTime"},
|
|
473
|
+
{ExifInterface.TAG_OFFSET_TIME_DIGITIZED, "OffsetTimeDigitized"},
|
|
474
|
+
{ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "OffsetTimeOriginal"},
|
|
475
|
+
{ExifInterface.TAG_ORF_ASPECT_FRAME, "ORFAspectFrame"},
|
|
476
|
+
{ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH, "ORFPreviewImageLength"},
|
|
477
|
+
{ExifInterface.TAG_ORF_PREVIEW_IMAGE_START, "ORFPreviewImageStart"},
|
|
478
|
+
{ExifInterface.TAG_ORF_THUMBNAIL_IMAGE, "ORFThumbnailImage"},
|
|
479
|
+
{ExifInterface.TAG_ORIENTATION, "Orientation"},
|
|
480
|
+
{ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation"},
|
|
481
|
+
{ExifInterface.TAG_PIXEL_X_DIMENSION, "PixelXDimension"},
|
|
482
|
+
{ExifInterface.TAG_PIXEL_Y_DIMENSION, "PixelYDimension"},
|
|
483
|
+
{ExifInterface.TAG_PLANAR_CONFIGURATION, "PlanarConfiguration"},
|
|
484
|
+
{ExifInterface.TAG_PRIMARY_CHROMATICITIES, "PrimaryChromaticities"},
|
|
485
|
+
{ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX, "RecommendedExposureIndex"},
|
|
486
|
+
{ExifInterface.TAG_REFERENCE_BLACK_WHITE, "ReferenceBlackWhite"},
|
|
487
|
+
{ExifInterface.TAG_RELATED_SOUND_FILE, "RelatedSoundFile"},
|
|
488
|
+
{ExifInterface.TAG_RESOLUTION_UNIT, "ResolutionUnit"},
|
|
489
|
+
{ExifInterface.TAG_ROWS_PER_STRIP, "RowsPerStrip"},
|
|
490
|
+
{ExifInterface.TAG_RW2_ISO, "RW2ISO"},
|
|
491
|
+
{ExifInterface.TAG_RW2_JPG_FROM_RAW, "RW2JpgFromRaw"},
|
|
492
|
+
{ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER, "RW2SensorBottomBorder"},
|
|
493
|
+
{ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER, "RW2SensorLeftBorder"},
|
|
494
|
+
{ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER, "RW2SensorRightBorder"},
|
|
495
|
+
{ExifInterface.TAG_RW2_SENSOR_TOP_BORDER, "RW2SensorTopBorder"},
|
|
496
|
+
{ExifInterface.TAG_SAMPLES_PER_PIXEL, "SamplesPerPixel"},
|
|
497
|
+
{ExifInterface.TAG_SATURATION, "Saturation"},
|
|
498
|
+
{ExifInterface.TAG_SCENE_CAPTURE_TYPE, "SceneCaptureType"},
|
|
499
|
+
{ExifInterface.TAG_SCENE_TYPE, "SceneType"},
|
|
500
|
+
{ExifInterface.TAG_SENSING_METHOD, "SensingMethod"},
|
|
501
|
+
{ExifInterface.TAG_SENSITIVITY_TYPE, "SensitivityType"},
|
|
502
|
+
{ExifInterface.TAG_SHARPNESS, "Sharpness"},
|
|
503
|
+
{ExifInterface.TAG_SHUTTER_SPEED_VALUE, "ShutterSpeedValue"},
|
|
504
|
+
{ExifInterface.TAG_SOFTWARE, "Software"},
|
|
505
|
+
{ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE, "SpatialFrequencyResponse"},
|
|
506
|
+
{ExifInterface.TAG_SPECTRAL_SENSITIVITY, "SpectralSensitivity"},
|
|
507
|
+
{ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY, "StandardOutputSensitivity"},
|
|
508
|
+
{ExifInterface.TAG_STRIP_BYTE_COUNTS, "StripByteCounts"},
|
|
509
|
+
{ExifInterface.TAG_STRIP_OFFSETS, "StripOffsets"},
|
|
510
|
+
{ExifInterface.TAG_SUBFILE_TYPE, "SubfileType"},
|
|
511
|
+
{ExifInterface.TAG_SUBJECT_AREA, "SubjectArea"},
|
|
512
|
+
{ExifInterface.TAG_SUBJECT_DISTANCE, "SubjectDistance"},
|
|
513
|
+
{ExifInterface.TAG_SUBJECT_DISTANCE_RANGE, "SubjectDistanceRange"},
|
|
514
|
+
{ExifInterface.TAG_SUBJECT_LOCATION, "SubjectLocation"},
|
|
515
|
+
{ExifInterface.TAG_SUBSEC_TIME, "SubSecTime"},
|
|
516
|
+
{ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, "SubSecTimeDigitized"},
|
|
517
|
+
{ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, "SubSecTimeOriginal"},
|
|
518
|
+
{ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH, "ThumbnailImageLength"},
|
|
519
|
+
{ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH, "ThumbnailImageWidth"},
|
|
520
|
+
{ExifInterface.TAG_TRANSFER_FUNCTION, "TransferFunction"},
|
|
521
|
+
{ExifInterface.TAG_USER_COMMENT, "UserComment"},
|
|
522
|
+
{ExifInterface.TAG_WHITE_BALANCE, "WhiteBalance"},
|
|
523
|
+
{ExifInterface.TAG_WHITE_POINT, "WhitePoint"},
|
|
524
|
+
{ExifInterface.TAG_X_RESOLUTION, "XResolution"},
|
|
525
|
+
{ExifInterface.TAG_Y_CB_CR_COEFFICIENTS, "YCbCrCoefficients"},
|
|
526
|
+
{ExifInterface.TAG_Y_CB_CR_POSITIONING, "YCbCrPositioning"},
|
|
527
|
+
{ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING, "YCbCrSubSampling"},
|
|
528
|
+
{ExifInterface.TAG_Y_RESOLUTION, "YResolution"}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
public void captureSample(int quality) {
|
|
532
|
+
Log.d(TAG, "captureSample: Starting sample capture with quality: " + quality);
|
|
533
|
+
|
|
534
|
+
if (sampleImageCapture == null) {
|
|
535
|
+
if (listener != null) {
|
|
536
|
+
listener.onSampleTakenError("Camera not ready");
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
sampleImageCapture.takePicture(
|
|
542
|
+
cameraExecutor,
|
|
543
|
+
new ImageCapture.OnImageCapturedCallback() {
|
|
544
|
+
@Override
|
|
545
|
+
public void onError(@NonNull ImageCaptureException exception) {
|
|
546
|
+
Log.e(TAG, "captureSample: Sample capture failed", exception);
|
|
547
|
+
if (listener != null) {
|
|
548
|
+
listener.onSampleTakenError("Sample capture failed: " + exception.getMessage());
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
@Override
|
|
553
|
+
public void onCaptureSuccess(@NonNull ImageProxy image) {
|
|
554
|
+
try {
|
|
555
|
+
// Convert ImageProxy to byte array
|
|
556
|
+
byte[] bytes = imageProxyToByteArray(image);
|
|
557
|
+
String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
558
|
+
|
|
559
|
+
if (listener != null) {
|
|
560
|
+
listener.onSampleTaken(base64);
|
|
561
|
+
}
|
|
562
|
+
} catch (Exception e) {
|
|
563
|
+
Log.e(TAG, "captureSample: Error processing sample", e);
|
|
564
|
+
if (listener != null) {
|
|
565
|
+
listener.onSampleTakenError("Error processing sample: " + e.getMessage());
|
|
566
|
+
}
|
|
567
|
+
} finally {
|
|
568
|
+
image.close();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private byte[] imageProxyToByteArray(ImageProxy image) {
|
|
576
|
+
ImageProxy.PlaneProxy[] planes = image.getPlanes();
|
|
577
|
+
ByteBuffer buffer = planes[0].getBuffer();
|
|
578
|
+
byte[] bytes = new byte[buffer.remaining()];
|
|
579
|
+
buffer.get(bytes);
|
|
580
|
+
return bytes;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// not workin for xiaomi https://xiaomi.eu/community/threads/mi-11-ultra-unable-to-access-camera-lenses-in-apps-camera2-api.61456/
|
|
584
|
+
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
585
|
+
public static List<com.ahm.capacitor.camera.preview.model.CameraDevice> getAvailableDevicesStatic(Context context) {
|
|
586
|
+
Log.d(TAG, "getAvailableDevicesStatic: Starting CameraX device enumeration with getPhysicalCameraInfos.");
|
|
587
|
+
List<com.ahm.capacitor.camera.preview.model.CameraDevice> devices = new ArrayList<>();
|
|
588
|
+
try {
|
|
589
|
+
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(context);
|
|
590
|
+
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
|
|
591
|
+
CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
|
|
592
|
+
|
|
593
|
+
for (CameraInfo cameraInfo : cameraProvider.getAvailableCameraInfos()) {
|
|
594
|
+
String logicalCameraId = Camera2CameraInfo.from(cameraInfo).getCameraId();
|
|
595
|
+
String position = isBackCamera(cameraInfo) ? "rear" : "front";
|
|
596
|
+
|
|
597
|
+
// Add logical camera
|
|
598
|
+
float minZoom = Objects.requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio();
|
|
599
|
+
float maxZoom = cameraInfo.getZoomState().getValue().getMaxZoomRatio();
|
|
600
|
+
List<LensInfo> logicalLenses = new ArrayList<>();
|
|
601
|
+
logicalLenses.add(new LensInfo(4.25f, "wideAngle", 1.0f, maxZoom));
|
|
602
|
+
devices.add(new com.ahm.capacitor.camera.preview.model.CameraDevice(
|
|
603
|
+
logicalCameraId, "Logical Camera (" + position + ")", position, logicalLenses, minZoom, maxZoom, true
|
|
604
|
+
));
|
|
605
|
+
Log.d(TAG, "Found logical camera: " + logicalCameraId + " (" + position + ") with zoom " + minZoom + "-" + maxZoom);
|
|
606
|
+
|
|
607
|
+
// Get and add physical cameras
|
|
608
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
609
|
+
Set<CameraInfo> physicalCameraInfos = cameraInfo.getPhysicalCameraInfos();
|
|
610
|
+
if (physicalCameraInfos.isEmpty()) continue;
|
|
611
|
+
|
|
612
|
+
Log.d(TAG, "Logical camera " + logicalCameraId + " has " + physicalCameraInfos.size() + " physical cameras.");
|
|
613
|
+
|
|
614
|
+
for (CameraInfo physicalCameraInfo : physicalCameraInfos) {
|
|
615
|
+
String physicalId = Camera2CameraInfo.from(physicalCameraInfo).getCameraId();
|
|
616
|
+
if (physicalId.equals(logicalCameraId)) continue; // Already added as logical
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(physicalId);
|
|
620
|
+
String deviceType = "wideAngle";
|
|
621
|
+
float[] focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
|
|
622
|
+
android.util.SizeF sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE);
|
|
623
|
+
|
|
624
|
+
if (focalLengths != null && focalLengths.length > 0 && sensorSize != null && sensorSize.getWidth() > 0) {
|
|
625
|
+
double fov = 2 * Math.toDegrees(Math.atan(sensorSize.getWidth() / (2 * focalLengths[0])));
|
|
626
|
+
if (fov > 90) deviceType = "ultraWide";
|
|
627
|
+
else if (fov < 40) deviceType = "telephoto";
|
|
628
|
+
} else if (focalLengths != null && focalLengths.length > 0) {
|
|
629
|
+
if (focalLengths[0] < 3.0f) deviceType = "ultraWide";
|
|
630
|
+
else if (focalLengths[0] > 5.0f) deviceType = "telephoto";
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
float physicalMinZoom = 1.0f;
|
|
634
|
+
float physicalMaxZoom = 1.0f;
|
|
635
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
|
636
|
+
android.util.Range<Float> zoomRange = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE);
|
|
637
|
+
if (zoomRange != null) {
|
|
638
|
+
physicalMinZoom = zoomRange.getLower();
|
|
639
|
+
physicalMaxZoom = zoomRange.getUpper();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
String label = "Physical " + deviceType + " (" + position + ")";
|
|
644
|
+
List<LensInfo> physicalLenses = new ArrayList<>();
|
|
645
|
+
physicalLenses.add(new LensInfo(focalLengths != null ? focalLengths[0] : 4.25f, deviceType, 1.0f, physicalMaxZoom));
|
|
646
|
+
|
|
647
|
+
devices.add(new com.ahm.capacitor.camera.preview.model.CameraDevice(
|
|
648
|
+
physicalId, label, position, physicalLenses, physicalMinZoom, physicalMaxZoom, false
|
|
649
|
+
));
|
|
650
|
+
Log.d(TAG, "Found physical camera: " + physicalId + " (" + label + ")");
|
|
651
|
+
} catch (CameraAccessException e) {
|
|
652
|
+
Log.e(TAG, "Failed to access characteristics for physical camera " + physicalId, e);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return devices;
|
|
658
|
+
} catch (Exception e) {
|
|
659
|
+
Log.e(TAG, "getAvailableDevicesStatic: Error getting devices", e);
|
|
660
|
+
return Collections.emptyList();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
public static ZoomFactors getZoomFactorsStatic() {
|
|
665
|
+
try {
|
|
666
|
+
// For static method, return default zoom factors
|
|
667
|
+
// We can try to detect if ultra-wide is available by checking device list
|
|
668
|
+
|
|
669
|
+
float minZoom = 1.0f;
|
|
670
|
+
float maxZoom = 10.0f;
|
|
671
|
+
|
|
672
|
+
Log.d(TAG, "getZoomFactorsStatic: Final range - minZoom: " + minZoom + ", maxZoom: " + maxZoom);
|
|
673
|
+
LensInfo defaultLens = new LensInfo(4.25f, "wideAngle", 1.0f, 1.0f);
|
|
674
|
+
return new ZoomFactors(minZoom, maxZoom, 1.0f, defaultLens);
|
|
675
|
+
} catch (Exception e) {
|
|
676
|
+
Log.e(TAG, "getZoomFactorsStatic: Error getting zoom factors", e);
|
|
677
|
+
LensInfo defaultLens = new LensInfo(4.25f, "wideAngle", 1.0f, 1.0f);
|
|
678
|
+
return new ZoomFactors(1.0f, 10.0f, 1.0f, defaultLens);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
public ZoomFactors getZoomFactors() {
|
|
683
|
+
if (camera == null) {
|
|
684
|
+
return getZoomFactorsStatic();
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
// Get the current zoom from active camera
|
|
689
|
+
float currentZoom = Objects.requireNonNull(camera.getCameraInfo().getZoomState().getValue()).getZoomRatio();
|
|
690
|
+
float minZoom = camera.getCameraInfo().getZoomState().getValue().getMinZoomRatio();
|
|
691
|
+
float maxZoom = camera.getCameraInfo().getZoomState().getValue().getMaxZoomRatio();
|
|
692
|
+
|
|
693
|
+
Log.d(TAG, "getZoomFactors: Combined range - minZoom: " + minZoom + ", maxZoom: " + maxZoom + ", currentZoom: " + currentZoom);
|
|
694
|
+
|
|
695
|
+
return new ZoomFactors(minZoom, maxZoom, currentZoom, getCurrentLensInfo());
|
|
696
|
+
} catch (Exception e) {
|
|
697
|
+
Log.e(TAG, "getZoomFactors: Error getting zoom factors", e);
|
|
698
|
+
return new ZoomFactors(1.0f, 1.0f, 1.0f, getCurrentLensInfo());
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
private LensInfo getCurrentLensInfo() {
|
|
705
|
+
if (camera == null) {
|
|
706
|
+
return new LensInfo(4.25f, "wideAngle", 1.0f, 1.0f);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
float currentZoom = Objects.requireNonNull(camera.getCameraInfo().getZoomState().getValue()).getZoomRatio();
|
|
711
|
+
|
|
712
|
+
// Determine device type based on zoom capabilities
|
|
713
|
+
String deviceType = "wideAngle";
|
|
714
|
+
float baseZoomRatio = 1.0f;
|
|
715
|
+
|
|
716
|
+
float digitalZoom = currentZoom / baseZoomRatio;
|
|
717
|
+
|
|
718
|
+
return new LensInfo(4.25f, deviceType, baseZoomRatio, digitalZoom);
|
|
719
|
+
} catch (Exception e) {
|
|
720
|
+
Log.e(TAG, "getCurrentLensInfo: Error getting lens info", e);
|
|
721
|
+
return new LensInfo(4.25f, "wideAngle", 1.0f, 1.0f);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
public void setZoom(float zoomRatio) throws Exception {
|
|
726
|
+
if (camera == null) {
|
|
727
|
+
throw new Exception("Camera not initialized");
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
Log.d(TAG, "setZoom: Requested zoom ratio: " + zoomRatio);
|
|
731
|
+
|
|
732
|
+
// Just let CameraX handle everything - it should automatically switch lenses
|
|
733
|
+
try {
|
|
734
|
+
ListenableFuture<Void> zoomFuture = camera.getCameraControl().setZoomRatio(zoomRatio);
|
|
735
|
+
|
|
736
|
+
// Add callback to see what actually happened
|
|
737
|
+
zoomFuture.addListener(() -> {
|
|
738
|
+
try {
|
|
739
|
+
float actualZoom = Objects.requireNonNull(camera.getCameraInfo().getZoomState().getValue()).getZoomRatio();
|
|
740
|
+
Log.d(TAG, "setZoom: CameraX set zoom to " + actualZoom + " (requested: " + zoomRatio + ")");
|
|
741
|
+
if (Math.abs(actualZoom - zoomRatio) > 0.1f) {
|
|
742
|
+
Log.w(TAG, "setZoom: CameraX clamped zoom from " + zoomRatio + " to " + actualZoom);
|
|
743
|
+
} else {
|
|
744
|
+
Log.d(TAG, "setZoom: CameraX successfully set requested zoom");
|
|
745
|
+
}
|
|
746
|
+
} catch (Exception e) {
|
|
747
|
+
Log.e(TAG, "setZoom: Error checking final zoom", e);
|
|
748
|
+
}
|
|
749
|
+
}, mainExecutor);
|
|
750
|
+
|
|
751
|
+
} catch (Exception e) {
|
|
752
|
+
Log.e(TAG, "setZoom: Failed to set zoom to " + zoomRatio, e);
|
|
753
|
+
throw e;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
public static List<Size> getSupportedPictureSizes(String facing) {
|
|
758
|
+
List<Size> sizes = new ArrayList<>();
|
|
759
|
+
try {
|
|
760
|
+
CameraSelector.Builder builder = new CameraSelector.Builder();
|
|
761
|
+
if ("front".equals(facing)) {
|
|
762
|
+
builder.requireLensFacing(CameraSelector.LENS_FACING_FRONT);
|
|
763
|
+
} else {
|
|
764
|
+
builder.requireLensFacing(CameraSelector.LENS_FACING_BACK);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// This part is complex because we need characteristics, which are not directly on CameraInfo.
|
|
768
|
+
// For now, returning a static list of common sizes.
|
|
769
|
+
// A more advanced implementation would use Camera2interop to get StreamConfigurationMap.
|
|
770
|
+
sizes.add(new Size(4032, 3024));
|
|
771
|
+
sizes.add(new Size(1920, 1080));
|
|
772
|
+
sizes.add(new Size(1280, 720));
|
|
773
|
+
sizes.add(new Size(640, 480));
|
|
774
|
+
|
|
775
|
+
} catch (Exception e) {
|
|
776
|
+
Log.e(TAG, "Error getting supported picture sizes", e);
|
|
777
|
+
}
|
|
778
|
+
return sizes;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private void setZoomInternal(float zoomRatio) {
|
|
782
|
+
if (camera != null) {
|
|
783
|
+
try {
|
|
784
|
+
float minZoom = Objects.requireNonNull(camera.getCameraInfo().getZoomState().getValue()).getMinZoomRatio();
|
|
785
|
+
float maxZoom = camera.getCameraInfo().getZoomState().getValue().getMaxZoomRatio();
|
|
786
|
+
float currentZoom = camera.getCameraInfo().getZoomState().getValue().getZoomRatio();
|
|
787
|
+
|
|
788
|
+
Log.d(TAG, "setZoomInternal: Current camera range: " + minZoom + "-" + maxZoom + ", current: " + currentZoom);
|
|
789
|
+
Log.d(TAG, "setZoomInternal: Requesting zoom: " + zoomRatio);
|
|
790
|
+
|
|
791
|
+
// Try to set zoom directly - let CameraX handle lens switching
|
|
792
|
+
ListenableFuture<Void> zoomFuture = camera.getCameraControl().setZoomRatio(zoomRatio);
|
|
793
|
+
|
|
794
|
+
zoomFuture.addListener(() -> {
|
|
795
|
+
try {
|
|
796
|
+
zoomFuture.get(); // Check if zoom was successful
|
|
797
|
+
float newZoom = Objects.requireNonNull(camera.getCameraInfo().getZoomState().getValue()).getZoomRatio();
|
|
798
|
+
Log.d(TAG, "setZoomInternal: Zoom set successfully to " + newZoom + " (requested: " + zoomRatio + ")");
|
|
799
|
+
|
|
800
|
+
// Check if CameraX switched cameras
|
|
801
|
+
String newCameraId = getCameraId(camera.getCameraInfo());
|
|
802
|
+
if (!newCameraId.equals(currentDeviceId)) {
|
|
803
|
+
currentDeviceId = newCameraId;
|
|
804
|
+
Log.d(TAG, "setZoomInternal: CameraX switched to camera: " + newCameraId);
|
|
805
|
+
}
|
|
806
|
+
} catch (Exception e) {
|
|
807
|
+
Log.w(TAG, "setZoomInternal: Zoom operation failed: " + e.getMessage());
|
|
808
|
+
// Fallback: clamp to current camera's range
|
|
809
|
+
float clampedZoom = Math.max(minZoom, Math.min(zoomRatio, maxZoom));
|
|
810
|
+
camera.getCameraControl().setZoomRatio(clampedZoom);
|
|
811
|
+
Log.d(TAG, "setZoomInternal: Fallback - clamped zoom to " + clampedZoom);
|
|
812
|
+
}
|
|
813
|
+
}, mainExecutor);
|
|
814
|
+
|
|
815
|
+
} catch (Exception e) {
|
|
816
|
+
Log.e(TAG, "setZoomInternal: Error setting zoom", e);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
public static List<String> getSupportedFlashModesStatic() {
|
|
822
|
+
try {
|
|
823
|
+
// For static method, we can return common flash modes
|
|
824
|
+
// Most modern cameras support these modes
|
|
825
|
+
return Arrays.asList("off", "on", "auto");
|
|
826
|
+
} catch (Exception e) {
|
|
827
|
+
Log.e(TAG, "getSupportedFlashModesStatic: Error getting flash modes", e);
|
|
828
|
+
return Collections.singletonList("off");
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
public List<String> getSupportedFlashModes() {
|
|
833
|
+
if (camera == null) {
|
|
834
|
+
return getSupportedFlashModesStatic();
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
try {
|
|
838
|
+
boolean hasFlash = camera.getCameraInfo().hasFlashUnit();
|
|
839
|
+
if (hasFlash) {
|
|
840
|
+
return Arrays.asList("off", "on", "auto");
|
|
841
|
+
} else {
|
|
842
|
+
return Collections.singletonList("off");
|
|
843
|
+
}
|
|
844
|
+
} catch (Exception e) {
|
|
845
|
+
Log.e(TAG, "getSupportedFlashModes: Error getting flash modes", e);
|
|
846
|
+
return Collections.singletonList("off");
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
public String getFlashMode() {
|
|
851
|
+
switch (currentFlashMode) {
|
|
852
|
+
case ImageCapture.FLASH_MODE_ON:
|
|
853
|
+
return "on";
|
|
854
|
+
case ImageCapture.FLASH_MODE_AUTO:
|
|
855
|
+
return "auto";
|
|
856
|
+
default:
|
|
857
|
+
return "off";
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
public void setFlashMode(String mode) {
|
|
862
|
+
int flashMode;
|
|
863
|
+
switch (mode) {
|
|
864
|
+
case "on":
|
|
865
|
+
flashMode = ImageCapture.FLASH_MODE_ON;
|
|
866
|
+
break;
|
|
867
|
+
case "auto":
|
|
868
|
+
flashMode = ImageCapture.FLASH_MODE_AUTO;
|
|
869
|
+
break;
|
|
870
|
+
default:
|
|
871
|
+
flashMode = ImageCapture.FLASH_MODE_OFF;
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
currentFlashMode = flashMode;
|
|
876
|
+
|
|
877
|
+
if (imageCapture != null) {
|
|
878
|
+
imageCapture.setFlashMode(flashMode);
|
|
879
|
+
}
|
|
880
|
+
if (sampleImageCapture != null) {
|
|
881
|
+
sampleImageCapture.setFlashMode(flashMode);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
public String getCurrentDeviceId() {
|
|
886
|
+
return currentDeviceId != null ? currentDeviceId : "unknown";
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
890
|
+
public void switchToDevice(String deviceId) {
|
|
891
|
+
Log.d(TAG, "switchToDevice: Attempting to switch to device " + deviceId);
|
|
892
|
+
|
|
893
|
+
mainExecutor.execute(() -> {
|
|
894
|
+
try {
|
|
895
|
+
// Standard physical device selection logic...
|
|
896
|
+
List<CameraInfo> cameraInfos = cameraProvider.getAvailableCameraInfos();
|
|
897
|
+
CameraInfo targetCameraInfo = null;
|
|
898
|
+
for (CameraInfo cameraInfo : cameraInfos) {
|
|
899
|
+
if (deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
|
|
900
|
+
targetCameraInfo = cameraInfo;
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (targetCameraInfo != null) {
|
|
906
|
+
Log.d(TAG, "switchToDevice: Found matching CameraInfo for deviceId: " + deviceId);
|
|
907
|
+
final CameraInfo finalTarget = targetCameraInfo;
|
|
908
|
+
|
|
909
|
+
// This filter will receive a list of all cameras and must return the one we want.
|
|
910
|
+
|
|
911
|
+
currentCameraSelector = new CameraSelector.Builder()
|
|
912
|
+
.addCameraFilter(cameras -> {
|
|
913
|
+
// This filter will receive a list of all cameras and must return the one we want.
|
|
914
|
+
return Collections.singletonList(finalTarget);
|
|
915
|
+
}).build();
|
|
916
|
+
currentDeviceId = deviceId;
|
|
917
|
+
bindCameraUseCases(); // Rebind with the new, highly specific selector
|
|
918
|
+
} else {
|
|
919
|
+
Log.e(TAG, "switchToDevice: Could not find any CameraInfo matching deviceId: " + deviceId);
|
|
920
|
+
}
|
|
921
|
+
} catch (Exception e) {
|
|
922
|
+
Log.e(TAG, "switchToDevice: Error switching camera", e);
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
public void flipCamera() {
|
|
928
|
+
Log.d(TAG, "flipCamera: Flipping camera");
|
|
929
|
+
|
|
930
|
+
// Determine current position based on session config and flip it
|
|
931
|
+
String currentPosition = sessionConfig.getPosition();
|
|
932
|
+
String newPosition = "front".equals(currentPosition) ? "rear" : "front";
|
|
933
|
+
|
|
934
|
+
Log.d(TAG, "flipCamera: Switching from " + currentPosition + " to " + newPosition);
|
|
935
|
+
|
|
936
|
+
sessionConfig = new CameraSessionConfiguration(
|
|
937
|
+
null, // deviceId - clear device ID to force position-based selection
|
|
938
|
+
newPosition, // position
|
|
939
|
+
sessionConfig.getX(), // x
|
|
940
|
+
sessionConfig.getY(), // y
|
|
941
|
+
sessionConfig.getWidth(), // width
|
|
942
|
+
sessionConfig.getHeight(), // height
|
|
943
|
+
sessionConfig.getPaddingBottom(), // paddingBottom
|
|
944
|
+
sessionConfig.isToBack(), // toBack
|
|
945
|
+
sessionConfig.isStoreToFile(), // storeToFile
|
|
946
|
+
sessionConfig.isEnableOpacity(), // enableOpacity
|
|
947
|
+
sessionConfig.isEnableZoom(), // enableZoom
|
|
948
|
+
sessionConfig.isDisableExifHeaderStripping(), // disableExifHeaderStripping
|
|
949
|
+
sessionConfig.isDisableAudio(), // disableAudio
|
|
950
|
+
sessionConfig.getZoomFactor() // zoomFactor
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
// Clear current device ID to force position-based selection
|
|
954
|
+
currentDeviceId = null;
|
|
955
|
+
|
|
956
|
+
// Camera operations must run on main thread
|
|
957
|
+
cameraExecutor.execute(() -> {
|
|
958
|
+
currentCameraSelector = buildCameraSelector();
|
|
959
|
+
bindCameraUseCases();
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
public void setOpacity(float opacity) {
|
|
964
|
+
if (previewView != null) {
|
|
965
|
+
previewView.setAlpha(opacity);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|