Wie gehe ich mit dem Lebenszyklus von Camerax um?Java

Java-Forum
Anonymous
 Wie gehe ich mit dem Lebenszyklus von Camerax um?

Post by Anonymous »

Ich versuche, ein Camerax -Plugin zu erstellen, das ich in Einheit verwenden möchte. Die Kamera scheint zu beginnen (einige Probleme beim Einstellen von Auflösung), aber wenn ich die App in den Hintergrund stelle, soll die Kamera aufhören, und dies tut jedoch mit diesem Fehler < /p>
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession Session 0: Exception while stopping repeating:
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession android.hardware.camera2.CameraAccessException: CAMERA_ERROR (3): The camera device has encountered a serious error
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at android.hardware.camera2.impl.CameraDeviceImpl.checkIfCameraClosedOrInError(CameraDeviceImpl.java:2612)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at android.hardware.camera2.impl.CameraDeviceImpl.stopRepeating(CameraDeviceImpl.java:1469)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at android.hardware.camera2.impl.CameraCaptureSessionImpl.close(CameraCaptureSessionImpl.java:579)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at androidx.camera.camera2.internal.SynchronizedCaptureSessionBaseImpl.close(SynchronizedCaptureSessionBaseImpl.java:476)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at androidx.camera.camera2.internal.CaptureSession.release(CaptureSession.java:526)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at androidx.camera.camera2.internal.Camera2CameraImpl.releaseSession(Camera2CameraImpl.java:555)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at androidx.camera.camera2.internal.Camera2CameraImpl.resetCaptureSession(Camera2CameraImpl.java:1329)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at androidx.camera.camera2.internal.Camera2CameraImpl.closeCamera(Camera2CameraImpl.java:468)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at androidx.camera.camera2.internal.Camera2CameraImpl$StateCallback.handleErrorOnOpen(Camera2CameraImpl.java:1798)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at androidx.camera.camera2.internal.Camera2CameraImpl$StateCallback.onError(Camera2CameraImpl.java:1754)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at androidx.camera.camera2.internal.CameraDeviceStateCallbacks$ComboDeviceStateCallback.onError(CameraDeviceStateCallbacks.java:121)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at android.hardware.camera2.impl.CameraDeviceImpl$ClientStateCallback$4.run(CameraDeviceImpl.java:338)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at androidx.camera.core.impl.utils.executor.SequentialExecutor$1.run(SequentialExecutor.java:111)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at androidx.camera.core.impl.utils.executor.SequentialExecutor$QueueWorker.workOnQueue(SequentialExecutor.java:231)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at androidx.camera.core.impl.utils.executor.SequentialExecutor$QueueWorker.run(SequentialExecutor.java:173)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
> 2025-09-22 16:17:29.347 30471 30594 Error CameraCaptureSession at java.lang.Thread.run(Thread.java:1012)
< /code>
Hier ist mein Code < /p>
package com.android.cameraplugin;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.util.Size;

import androidx.annotation.NonNull;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;

import com.google.common.util.concurrent.ListenableFuture;

import java.nio.ByteBuffer;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CameraPlugin {
private static final String TAG = "CameraPlugin";
private static CameraPlugin instance;

private final Activity activity;
private final Context context;

// CameraX components
private ProcessCameraProvider cameraProvider;
private Camera camera;
private ImageAnalysis imageAnalysis;
private CameraSelector cameraSelector;

// Threading
private final ExecutorService cameraExecutor;
private final Handler mainHandler = new Handler(Looper.getMainLooper());

// Our explicit lifecycle for CameraX
private final CustomLifecycle camLifecycle = new CustomLifecycle();

// Camera state
private boolean isCameraStarted = false;
private boolean isFrontCamera = false;
private int targetWidth = 1280;
private int targetHeight = 720;

// Rapid-cycling detection (kept from your original)
private long lastBackgroundTime = 0;
private int backgroundCount = 0;
private static final long BACKGROUND_DETECTION_WINDOW = 5000; // 5 seconds
private boolean isRapidCyclingMode = false;
private long rapidCyclingStartTime = 0;
private static final long RAPID_CYCLING_COOLDOWN = 10000; // 10 seconds

// RGB data storage
private volatile byte[] latestRGBData;
private volatile int frameWidth = 640;
private volatile int frameHeight = 480;

// Image analysis callback (YUV_420_888 guaranteed)
private final ImageAnalysis.Analyzer imageAnalyzer = new ImageAnalysis.Analyzer() {
private long lastProcessTime = 0;
private static final long MIN_PROCESS_INTERVAL = 50; // ~20 FPS max
private int frameSkipCounter = 0;
private static final int FRAME_SKIP_RATIO = 2; // Process every 2nd frame

@Override
public void analyze(@NonNull ImageProxy image) {
try {
// Log first few frames to debug
if (frameSkipCounter < 5) {
Log.d(TAG, "ImageAnalyzer received frame #" + frameSkipCounter +
" - format: " + image.getFormat() +
", size: " + image.getWidth() + "x" + image.getHeight());
}

// Throttle/skip for CPU budget
frameSkipCounter++;
if (frameSkipCounter % FRAME_SKIP_RATIO != 0) return;

long now = System.currentTimeMillis();
if (now - lastProcessTime < MIN_PROCESS_INTERVAL) return;
lastProcessTime = now;

if (image.getFormat() != ImageFormat.YUV_420_888) {
Log.w(TAG, "Unexpected format: " + image.getFormat() + " (expected YUV_420_888)");
}

byte[] rgb = yuv420888ToRgb(image);
if (rgb != null) {
latestRGBData = rgb;
frameWidth = image.getWidth();
frameHeight = image.getHeight();
Log.v(TAG, "Frame " + frameWidth + "x" + frameHeight + " -> RGB bytes=" + rgb.length);
}
} catch (Throwable t) {
Log.e(TAG, "analyze error: " + t.getMessage(), t);
} finally {
image.close();
}
}
};

private CameraPlugin(Activity activity) {
this.activity = activity;
this.context = activity.getApplicationContext();
this.cameraExecutor = Executors.newSingleThreadExecutor();

// Initialize CustomLifecycle on main thread
if (Looper.myLooper() == Looper.getMainLooper()) {
camLifecycle.initialize();
} else {
mainHandler.post(camLifecycle::initialize);
}

initializeCameraProvider();
}

public static CameraPlugin getInstance(Activity activity) {
if (instance == null) {
instance = new CameraPlugin(activity);
}
return instance;
}

private void initializeCameraProvider() {
ListenableFuture
cameraProviderFuture =
ProcessCameraProvider.getInstance(context);

cameraProviderFuture.addListener(() -> {
try {
cameraProvider = cameraProviderFuture.get();
Log.d(TAG, "CameraX provider initialized");
try {
var infos = cameraProvider.getAvailableCameraInfos();
Log.d(TAG, "Available cameras: " + infos.size());
for (int i = 0; i < infos.size(); i++) {
Log.d(TAG, "Camera " + i + ": " + infos.get(i));
}
} catch (Exception e) {
Log.w(TAG, "Could not list camera infos: " + e.getMessage());
}
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, "Failed to init CameraX provider: " + e.getMessage(), e);
}
}, ContextCompat.getMainExecutor(context));
}

public boolean checkCameraPermission() {
return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED;
}

public void requestCameraPermission() {
if (!checkCameraPermission()) {
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.CAMERA}, 100);
}
}

public boolean startCamera() {
return startCamera(targetWidth, targetHeight);
}

public boolean startCamera(int targetWidth, int targetHeight) {
Log.d(TAG, "Starting CameraX with " + targetWidth + "x" + targetHeight);

if (isRapidCyclingMode) {
Log.w(TAG, "Blocked: rapid cycling mode");
return false;
}
if (!checkCameraPermission()) {
Log.e(TAG, "Camera permission not granted");
return false;
}
if (cameraProvider == null) {
Log.e(TAG, "Camera provider not ready");
return false;
}
if (isCameraStarted) {
Log.d(TAG, "Camera already started");
return true;
}

if (Looper.myLooper() != Looper.getMainLooper()) {
mainHandler.post(() -> startCameraOnMainThread(targetWidth, targetHeight));
return true;
} else {
return startCameraOnMainThread(targetWidth, targetHeight);
}
}

private boolean startCameraOnMainThread(int targetWidth, int targetHeight) {
try {
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;

Log.d(TAG, "Requesting camera resolution: " + targetWidth + "x" + targetHeight);

cameraSelector = new CameraSelector.Builder()
.requireLensFacing(isFrontCamera ? CameraSelector.LENS_FACING_FRONT
: CameraSelector.LENS_FACING_BACK)
.build();

// Force a standard resolution that's commonly supported
// Use 1280x720 (HD) which is almost universally supported
Size targetSize = new Size(1280, 720);
Log.d(TAG, "Forcing target size to: " + targetSize + " (ignoring requested " + targetWidth + "x" + targetHeight + ")");

imageAnalysis = new ImageAnalysis.Builder()
.setTargetResolution(targetSize)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
// Force YUV_420_888 output to the analyzer
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
.build();
imageAnalysis.setAnalyzer(cameraExecutor, imageAnalyzer);

cameraProvider.unbindAll();

try {
camera = cameraProvider.bindToLifecycle(
camLifecycle,
cameraSelector,
imageAnalysis
);

// Move custom lifecycle to RESUMED state to actually start the camera
mainHandler.post(camLifecycle::toResumed);

isCameraStarted = true;
Log.d(TAG, "CameraX started (YUV_420_888, analysis only)");
return true;
} catch (Exception bindingError) {
Log.e(TAG, "Failed to bind camera to lifecycle: " + bindingError.getMessage(), bindingError);
// Clean up on binding failure
if (cameraProvider != null) {
try {
cameraProvider.unbindAll();
} catch (Exception cleanupError) {
Log.e(TAG, "Cleanup error after binding failure: " + cleanupError.getMessage());
}
}
return false;
}

} catch (Exception e) {
Log.e(TAG, "Failed to start CameraX: " + e.getMessage(), e);
return false;
}
}

public void stopCamera() {
Log.d(TAG, "Stopping CameraX");
if (Looper.myLooper() != Looper.getMainLooper()) {
mainHandler.post(this::stopCameraOnMainThread);
} else {
stopCameraOnMainThread();
}
}

private void stopCameraOnMainThread() {
Log.d(TAG, "Stopping CameraX (sync)");

// 0) Prevent new analyze callbacks while tearing down
try {
if (imageAnalysis != null) imageAnalysis.clearAnalyzer();
} catch (Exception ignore) {}

// 1) Unbind all use cases NOW (not posted)
if (cameraProvider != null) {
try { cameraProvider.unbindAll(); } catch (Exception e) {
Log.e(TAG, "unbindAll error: " + e.getMessage(), e);
}
}

// 2) Drive lifecycle down NOW (not posted)
try { camLifecycle.toCreated(); } catch (Exception e) {
Log.e(TAG, "Error setting lifecycle to CREATED: " + e.getMessage());
}

// 3) Null state
isCameraStarted = false;
camera = null;
imageAnalysis = null;

Log.d(TAG, "CameraX stopped");
}

private void stopCameraImmediately() {
Log.d(TAG, "Immediate stop (break rapid cycling)");
if (cameraProvider != null) {
try {
Log.d(TAG, "Immediate stop - unbinding all use cases");
cameraProvider.unbindAll();
} catch (Exception e) {
Log.e(TAG, "Immediate unbindAll error: " + e.getMessage(), e);
}
}

// Move custom lifecycle to CREATED state immediately
try {
if (Looper.myLooper() == Looper.getMainLooper()) {
camLifecycle.toCreated();
} else {
mainHandler.post(camLifecycle::toCreated);
}
} catch (Exception e) {
Log.e(TAG, "Error setting lifecycle to CREATED (immediate): " + e.getMessage());
}

isCameraStarted = false;
camera = null;
imageAnalysis = null;
}

public void pauseCamera() { Log.d(TAG, "pauseCamera (lifecycle-driven)"); }
public void resumeCamera() { Log.d(TAG, "resumeCamera (lifecycle-driven)"); }

public void switchCamera() {
Log.d(TAG, "Switching camera");
if (isCameraStarted) {
stopCamera();
isFrontCamera = !isFrontCamera;
startCamera();
} else {
isFrontCamera = !isFrontCamera;
}
}

public boolean isUsingFrontCamera() { return isFrontCamera; }
public boolean isCameraRunning() { return isCameraStarted && camera != null; }

// Unity-facing frame accessors
public boolean hasNewFrame() { return latestRGBData != null; }
public byte[] getLatestFrameData() { return latestRGBData; }
public byte[] getLatestRGBFrame() { return latestRGBData; }
public int getFrameWidth() { return frameWidth; }
public int getFrameHeight() { return frameHeight; }
public void clearFrameData() { latestRGBData = null; }

public boolean isCameraInitialized() { return isCameraStarted; }
public boolean isCameraReady() { return isCameraStarted && camera != null && latestRGBData != null; }

public void setTargetResolution(int width, int height) {
this.targetWidth = width;
this.targetHeight = height;
}
public int getTargetWidth() { return targetWidth; }
public int getTargetHeight() { return targetHeight; }

public void setZoom(float zoomLevel) {
Log.d(TAG, "setZoom called with: " + zoomLevel);
// Implement via CameraControl if/when needed
}

public void switchCamera(String direction) {
Log.d(TAG, "switchCamera(direction=" + direction + ")");
switchCamera();
}

public void breakRapidCycling() {
Log.d(TAG, "breakRapidCycling called");
backgroundCount = 0;
lastBackgroundTime = 0;
isRapidCyclingMode = false;
rapidCyclingStartTime = 0;
if (isCameraStarted) stopCameraImmediately();
}

public boolean isInRapidCycling() { return isRapidCyclingMode; }

// Manual camera control methods
public void startCameraManually() {
Log.d(TAG, "Manually starting camera via custom lifecycle");
if (!isRapidCyclingMode) {
mainHandler.post(camLifecycle::toResumed);
}
}

public void stopCameraManually() {
Log.d(TAG, "Manually stopping camera via custom lifecycle");
mainHandler.post(camLifecycle::toStarted);
}

// Handle camera errors and recovery
public void handleCameraError() {
Log.e(TAG, "Handling camera error - stopping and resetting");

// Stop camera immediately
stopCameraImmediately();

// Reset rapid cycling mode
isRapidCyclingMode = false;
backgroundCount = 0;
lastBackgroundTime = 0;
rapidCyclingStartTime = 0;

// Wait a bit before allowing restart
mainHandler.postDelayed(() -> {
Log.d(TAG, "Camera error recovery - ready for restart");
}, 2000); // 2 second delay
}

// --------- YUV_420_888 -> RGB (interleaved) WITHOUT JPEG ----------
// Handles arbitrary row/pixel strides for Y, U, V planes.
private static byte[] yuv420888ToRgb(ImageProxy image) {
final int width = image.getWidth();
final int height = image.getHeight();

ImageProxy.PlaneProxy yPlane = image.getPlanes()[0];
ImageProxy.PlaneProxy uPlane = image.getPlanes()[1];
ImageProxy.PlaneProxy vPlane = image.getPlanes()[2];

ByteBuffer yBuf = yPlane.getBuffer();
ByteBuffer uBuf = uPlane.getBuffer();
ByteBuffer vBuf = vPlane.getBuffer();

int yRowStride = yPlane.getRowStride();
int yPixStride = yPlane.getPixelStride(); // usually 1

int uRowStride = uPlane.getRowStride();
int uPixStride = uPlane.getPixelStride(); // usually 2

int vRowStride = vPlane.getRowStride();
int vPixStride = vPlane.getPixelStride(); // usually 2

byte[] out = new byte[width * height * 3];

// Standard BT.601 full-range conversion
// R = 1.164*(Y-16) + 1.596*(V-128)
// G = 1.164*(Y-16) - 0.392*(U-128) - 0.813*(V-128)
// B = 1.164*(Y-16) + 2.017*(U-128)
//
// Use integer math approximation for speed:
// C = Y - 16; D = U - 128; E = V - 128;
// R = (298*C + 409*E + 128) >> 8
// G = (298*C - 100*D - 208*E + 128) >> 8
// B = (298*C + 516*D + 128) >> 8

for (int y = 0; y < height; y++) {
int yRowStart = yRowStride * y;
int uvRow = (y >> 1); // /2
int uRowStart = uRowStride * uvRow;
int vRowStart = vRowStride * uvRow;

for (int x = 0; x < width; x++) {
int yCol = yRowStart + x * yPixStride;

int uvCol = (x >> 1); // /2
int uIndex = uRowStart + uvCol * uPixStride;
int vIndex = vRowStart + uvCol * vPixStride;

int Y = (yBuf.get(yCol) & 0xFF);
int U = (uBuf.get(uIndex) & 0xFF);
int V = (vBuf.get(vIndex) & 0xFF);

int C = Y - 16;
int D = U - 128;
int E = V - 128;

if (C < 0) C = 0;

int R = (298 * C + 409 * E + 128) >> 8;
int G = (298 * C - 100 * D - 208 * E + 128) >> 8;
int B = (298 * C + 516 * D + 128) >> 8;

if (R < 0) R = 0; else if (R > 255) R = 255;
if (G < 0) G = 0; else if (G > 255) G = 255;
if (B < 0) B = 0; else if (B > 255) B = 255;

int outIdx = (y * width + x) * 3;
out[outIdx] = (byte) R;
out[outIdx + 1] = (byte) G;
out[outIdx + 2] = (byte) B;
}
}

return out;
}

// ---- Forward these from your Activity/Unity bridge ----

public void onCreate() {
Log.d(TAG, "CameraPlugin onCreate");
// CustomLifecycle starts at CREATED
// Don't automatically move to RESUMED - let startCamera() handle it
}

public void onResume() {
Log.d(TAG, "CameraPlugin onResume - restarting camera for foreground");
long now = System.currentTimeMillis();
if (isRapidCyclingMode && (now - rapidCyclingStartTime) < RAPID_CYCLING_COOLDOWN) {
Log.d(TAG, "In cooldown; skip resume transition");
return;
}

// Restart camera when app comes to foreground
Log.d(TAG, "Moving camera to RESUMED state (started) for foreground");
mainHandler.post(camLifecycle::toResumed);
}

public void onPause() {
Log.d(TAG, "CameraPlugin onPause - stopping camera for background");
// Don’t post just a lifecycle bump; actually stop now.
if (Looper.myLooper() == Looper.getMainLooper()) {
stopCameraOnMainThread();
} else {
mainHandler.post(this::stopCameraOnMainThread);
}
}

public void onStop() {
Log.d(TAG, "CameraPlugin onStop");
mainHandler.post(camLifecycle::toCreated);
}

public void onDestroy() {
Log.d(TAG, "CameraPlugin onDestroy");
stopCamera();
if (cameraExecutor != null) cameraExecutor.shutdown();
mainHandler.post(camLifecycle::toDestroyed);
}
}

// Explicit lifecycle you control from Unity/Activity
class CustomLifecycle implements LifecycleOwner {
private final LifecycleRegistry lifecycleRegistry;
private final Handler mainHandler = new Handler(Looper.getMainLooper());

public CustomLifecycle() {
lifecycleRegistry = new LifecycleRegistry(this);
// Don't set state in constructor - do it on main thread
}

// Initialize on main thread
public void initialize() {
if (Looper.myLooper() == Looper.getMainLooper()) {
lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
} else {
mainHandler.post(() -> lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED));
}
}

public void toResumed() {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.d("CustomLifecycle", "Setting state to RESUMED");
lifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED);
} else {
mainHandler.post(() -> {
Log.d("CustomLifecycle", "Setting state to RESUMED (posted)");
lifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED);
});
}
}

public void toStarted() {
if (Looper.myLooper() == Looper.getMainLooper()) {
lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
} else {
mainHandler.post(() -> lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED));
}
}

public void toCreated() {
if (Looper.myLooper() == Looper.getMainLooper()) {
lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
} else {
mainHandler.post(() -> lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED));
}
}

public void toDestroyed() {
if (Looper.myLooper() == Looper.getMainLooper()) {
lifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
} else {
mainHandler.post(() -> lifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED));
}
}

@Override
public Lifecycle getLifecycle() {
return lifecycleRegistry;
}
}

< /code>
Gibt es eine Dokumentation, in die man sich befassen muss? Oder weiß jemand, was ich hier falsch mache?

Quick Reply

Change Text Case: 
   
  • Similar Topics
    Replies
    Views
    Last post