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核心原因有四个:
- 创建了
ImageAnalysis用例但绑定生命周期时没传入,且CameraX无预览场景下必须绑定至少一个关联Surface的用例,否则相机会被直接回收关闭。 - 原有YUV转Bitmap逻辑错误,直接取Plane0的字节流解码拿不到正确Bitmap,CameraX默认输出是YUV_420_888格式,需要做格式转换。
- 依赖存在重复冲突,同时引入两个不同版本的camera-core,会触发运行时异常。
- 没有做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
相关产品推荐
相关产品推荐

