You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java11开发Godot安卓库 CameraX获取相机帧报Camera is closed

问题背景

我正在为Godot Engine开发一款Android库,目标实现流程如下:

  • 从相机获取帧数据
  • 将帧转换为Bitmap
  • 按需调整Bitmap尺寸(可选步骤)
  • 将数据发送至Godot Engine

当前卡在第一步,需要无UI预览、直接通过代码获取相机帧的完整Java实现,运行环境为Java 11。


已尝试的方案与遇到的问题

初始测试代码

我最初编写的实现代码如下,运行时应用启动闪退、无法捕获帧:

public class MainActivity extends AppCompatActivity
{
    private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
    private ImageCapture imageCapture;
    private Button bCapture;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bCapture = findViewById(R.id.bCapture);
        bCapture.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view)
            {
                capturePhoto();
            }
        });
        cameraProviderFuture = ProcessCameraProvider.getInstance(this);
        cameraProviderFuture.addListener(() -> {
            try {
                ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
                startCameraX(cameraProvider);
            } catch (ExecutionException | InterruptedException e) {
                e.printStackTrace();
            }
        }, getExecutor());

    }

    Executor getExecutor() {
        return ContextCompat.getMainExecutor(this);
    }


    private void startCameraX(ProcessCameraProvider cameraProvider)
    {
        cameraProvider.unbindAll();
        CameraSelector cameraSelector = new CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                .build();

        imageCapture = new ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build();

        ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build();

        imageAnalysis.setAnalyzer(getExecutor(), new ImageAnalysis.Analyzer() {
            @Override
            public void analyze(@NonNull ImageProxy image) {

                Log.d("TAG", "analyze: got the frame at: " + image.getImageInfo().getTimestamp());
                image.close();
            }
        });

        cameraProvider.bindToLifecycle((LifecycleOwner) this, cameraSelector, imageCapture);
    }

    private void capturePhoto()
    {
        imageCapture.takePicture(getExecutor(), new ImageCapture.OnImageCapturedCallback()
        {
            @Override
            public void onCaptureSuccess(@NonNull ImageProxy image)
            {
                Log.d("Test"," one success capture");
                super.onCaptureSuccess(image);
                Bitmap bitmap = getBitmap(image);
                Log.d("Test"," Capture Success ");
            }

            @Override
            public void onError(@NonNull ImageCaptureException exception) {
                super.onError(exception);
                Log.d("Test"," Error capture");
            }
        });
    }

    //convert to Bitmap
    private Bitmap getBitmap(ImageProxy image)
    {
        ByteBuffer buffer = image.getPlanes()[0].getBuffer();
        buffer.rewind();
        byte[] bytes = new byte[buffer.capacity()];
        buffer.get(bytes);
        byte[] clonedBytes = bytes.clone();
        return BitmapFactory.decodeByteArray(clonedBytes, 0, clonedBytes.length);
    }
}

初始配置文件

Module级build.gradle配置

plugins {
    id 'com.android.application'
}

android {
    namespace 'com.example.myapplication3'
    compileSdk 32

    defaultConfig {
        applicationId "com.example.myapplication3"
        minSdk 21
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.4.2'
    implementation 'com.google.android.material:material:1.6.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    implementation 'androidx.camera:camera-core:1.0.2'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'


    def cameraxVersion = "1.1.0-alpha05"
    implementation "androidx.camera:camera-core:${cameraxVersion}"
    implementation "androidx.camera:camera-camera2:${cameraxVersion}"
    implementation "androidx.camera:camera-lifecycle:${cameraxVersion}"

    // CameraX View class
    implementation 'androidx.camera:camera-view:1.0.0-alpha25'

    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

}

AndroidManifest.xml配置

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        tools:targetApi="32">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

问题排查进展

  • 启动闪退原因已定位:Manifest中<application>标签缺失主题配置android:theme="@style/Theme.MyApplication3",补全后启动正常。
  • 补全配置后捕获帧失败,错误回调打印日志:捕获错误 *Camera is closed.*,无法正常获取相机帧数据。

解决方案

报Camera is closed核心原因有四个:

  1. 创建了ImageAnalysis用例但绑定生命周期时没传入,且CameraX无预览场景下必须绑定至少一个关联Surface的用例,否则相机会被直接回收关闭。
  2. 原有YUV转Bitmap逻辑错误,直接取Plane0的字节流解码拿不到正确Bitmap,CameraX默认输出是YUV_420_888格式,需要做格式转换。
  3. 依赖存在重复冲突,同时引入两个不同版本的camera-core,会触发运行时异常。
  4. 没有做Android 6.0+的动态相机权限申请,直接调用相机会被系统拦截。

第一步:修正依赖配置

把Module级build.gradle的dependencies块替换为以下内容,统一CameraX版本,移除重复依赖:

dependencies {

    implementation 'androidx.appcompat:appcompat:1.4.2'
    implementation 'com.google.android.material:material:1.6.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

    // 统一CameraX为稳定版,移除冲突的alpha版本和重复依赖
    def cameraxVersion = "1.2.3"
    implementation "androidx.camera:camera-core:${cameraxVersion}"
    implementation "androidx.camera:camera-camera2:${cameraxVersion}"
    implementation "androidx.camera:camera-lifecycle:${cameraxVersion}"
}

第二步:补全Manifest配置

在<application>标签加上缺失的主题配置,同时补充相机特性声明:

<!-- 根节点下添加相机特性声明 -->
<uses-feature android:name="android.hardware.camera" android:required="true" />

<application
    android:allowBackup="true"
    android:dataExtractionRules="@xml/data_extraction_rules"
    android:fullBackupContent="@xml/backup_rules"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.MyApplication3"
    tools:targetApi="32">
    <!-- 原有activity配置保持不变 -->
</application>

第三步:完整无预览取帧实现代码

不需要在布局中添加PreviewView,直接用ImageAnalysis做连续帧获取,内置正确的YUV转Bitmap逻辑,同时包含动态权限申请:

import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MainActivity extends AppCompatActivity {
    private static final int CAMERA_PERMISSION_CODE = 1001;
    private ExecutorService cameraExecutor;
    private boolean isFrameProcessing = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 不需要绑定布局,完全无UI预览
        cameraExecutor = Executors.newSingleThreadExecutor();

        // 检查相机权限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED) {
            startCamera();
        } else {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.CAMERA}, CAMERA_PERMISSION_CODE);
        }
    }

    private void startCamera() {
        ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this);
        cameraProviderFuture.addListener(() -> {
            try {
                ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
                // 解绑所有原有相机用例
                cameraProvider.unbindAll();

                // 选择后置摄像头,需要前置则改为LENS_FACING_FRONT
                CameraSelector cameraSelector = new CameraSelector.Builder()
                        .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                        .build();

                // 配置ImageAnalysis,仅保留最新帧降低延迟
                ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
                        .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                        // 可自定义输出分辨率,例:.setTargetResolution(new Size(1280,720))
                        .build();

                // 设置帧分析回调
                imageAnalysis.setAnalyzer(cameraExecutor, image -> {
                    if (isFrameProcessing) {
                        image.close();
                        return;
                    }
                    isFrameProcessing = true;
                    // YUV帧转Bitmap
                    Bitmap frameBitmap = yuv420ToBitmap(image);
                    // 此处可添加Bitmap缩放、传给Godot引擎的业务逻辑
                    Log.d("CameraFrame", "获取帧成功,尺寸:" + frameBitmap.getWidth() + "*" + frameBitmap.getHeight());
                    
                    // 处理完成必须关闭ImageProxy,重置标记
                    image.close();
                    isFrameProcessing = false;
                });

                // 关键:无预览场景下需要绑定一个空Preview用例提供Surface,否则相机会被系统回收
                // 该Preview不需要绑定到任何视图,不会显示预览画面
                Preview emptyPreview = new Preview.Builder().build();

                // 绑定生命周期,传入所有需要的用例
                cameraProvider.bindToLifecycle(this, cameraSelector, emptyPreview, imageAnalysis);

            } catch (ExecutionException | InterruptedException e) {
                Log.e("CameraError", "相机启动失败", e);
            }
        }, ContextCompat.getMainExecutor(this));
    }

    // YUV_420_888转Bitmap正确实现
    private Bitmap yuv420ToBitmap(ImageProxy image) {
        ByteBuffer yBuffer = image.getPlanes()[0].getBuffer();
        ByteBuffer uBuffer = image.getPlanes()[1].getBuffer();
        ByteBuffer vBuffer = image.getPlanes()[2].getBuffer();

        int ySize = yBuffer.remaining();
        int uSize = uBuffer.remaining();
        int vSize = vBuffer.remaining();

        byte[] nv21 = new byte[ySize + uSize + vSize];
        // 拷贝Y通道数据
        yBuffer.get(nv21, 0, ySize);
        // NV21格式V通道在前、U通道在后
        vBuffer.get(nv21, ySize, vSize);
        uBuffer.get(nv21, ySize + vSize, uSize);

        YuvImage yuvImage = new YuvImage(nv21, ImageFormat.NV21, image.getWidth(), image.getHeight(), null);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        yuvImage.compressToJpeg(new Rect(0, 0, image.getWidth(), image.getHeight()), 100, out);
        byte[] imageBytes = out.toByteArray();
        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == CAMERA_PERMISSION_CODE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                startCamera();
            } else {
                Log.e("Permission", "用户拒绝相机权限");
                finish();
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        cameraExecutor.shutdown();
    }
}

如果不需要连续取帧,仅需按需拍摄单张,把ImageAnalysis替换为ImageCapture即可,绑定生命周期时同样需要传入空Preview用例,避免相机被回收。


内容的提问来源于stack exchange,提问作者azooz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 23:09:23