3. 设置图像格式

任务说明

本案例要求将摄像头输出格式设置为 NV21、分辨率 640x480,并验证设置是否生效。NV21 是 Android 相机默认支持的 YUV 格式,Y 平面在前,VU 交错平面在后。我们将通过 Camera2 API 实现配置,并在回调中检查实际输出格式与分辨率。

实现代码

// CameraFormatSetter.java
// 依赖:Android 5.0+ (API 21+),Camera2 API
import android.content.Context;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.Image;
import android.media.ImageReader;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Size;
import android.view.Surface;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.Collections;

public class CameraFormatSetter {
    private CameraDevice cameraDevice;
    private CameraCaptureSession captureSession;
    private ImageReader imageReader;
    private HandlerThread backgroundThread;
    private Handler backgroundHandler;
    private String cameraId;
    private Size targetSize = new Size(640, 480);
    private boolean formatVerified = false;

    // 验证结果回调
    public interface VerificationCallback {
        void onVerified(boolean success, String message);
    }

    public void openCamera(Context context, VerificationCallback callback) {
        CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
        try {
            // 选择后置摄像头
            for (String id : manager.getCameraIdList()) {
                CameraCharacteristics chars = manager.getCameraCharacteristics(id);
                Integer facing = chars.get(CameraCharacteristics.LENS_FACING);
                if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) {
                    cameraId = id;
                    break;
                }
            }
            if (cameraId == null) { callback.onVerified(false, "未找到后置摄像头"); return; }

            // 检查 NV21 和 640x480 是否受支持
            StreamConfigurationMap map = manager.getCameraCharacteristics(cameraId)
                    .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            Size[] nv21Sizes = map.getOutputSizes(ImageFormat.NV21);
            boolean sizeSupported = false;
            if (nv21Sizes != null) {
                for (Size s : nv21Sizes) {
                    if (s.getWidth() == 640 && s.getHeight() == 480) {
                        sizeSupported = true;
                        break;
                    }
                }
            }
            if (!sizeSupported) {
                callback.onVerified(false, "摄像头不支持 NV21 640x480");
                return;
            }

            // 启动后台线程
            startBackgroundThread();

            // 创建 ImageReader,格式为 NV21
            imageReader = ImageReader.newInstance(640, 480, ImageFormat.NV21, 2);
            imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
                @Override
                public void onImageAvailable(ImageReader reader) {
                    Image image = reader.acquireLatestImage();
                    if (image != null) {
                        // 验证实际格式与分辨率
                        int actualFormat = image.getFormat();
                        int width = image.getWidth();
                        int height = image.getHeight();
                        String msg = "实际格式: " + actualFormat + " (NV21=" + ImageFormat.NV21
                                + "), 分辨率: " + width + "x" + height;
                        boolean ok = (actualFormat == ImageFormat.NV21)
                                && (width == 640) && (height == 480);
                        formatVerified = ok;
                        callback.onVerified(ok, msg);
                        image.close();
                    }
                }
            }, backgroundHandler);

            // 打开摄像头
            if (checkSelfPermission(context)) return;
            manager.openCamera(cameraId, new CameraDevice.StateCallback() {
                @Override
                public void onOpened(@NonNull CameraDevice camera) {
                    cameraDevice = camera;
                    createCaptureSession(callback);
                }
                @Override
                public void onDisconnected(@NonNull CameraDevice camera) {
                    camera.close();
                    cameraDevice = null;
                }
                @Override
                public void onError(@NonNull CameraDevice camera, int error) {
                    camera.close();
                    cameraDevice = null;
                    callback.onVerified(false, "摄像头打开失败,错误码: " + error);
                }
            }, backgroundHandler);

        } catch (CameraAccessException e) {
            callback.onVerified(false, "摄像头访问异常: " + e.getMessage());
        }
    }

    private void createCaptureSession(VerificationCallback callback) {
        try {
            Surface surface = imageReader.getSurface();
            CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            builder.addTarget(surface);

            cameraDevice.createCaptureSession(Collections.singletonList(surface),
                    new CameraCaptureSession.StateCallback() {
                        @Override
                        public void onConfigured(@NonNull CameraCaptureSession session) {
                            captureSession = session;
                            try {
                                // 设置 NV21 格式(通过 ImageReader 已隐式指定)
                                // 设置分辨率(通过 ImageReader 已隐式指定)
                                // 提交请求
                                CaptureRequest request = builder.build();
                                session.setRepeatingRequest(request, null, backgroundHandler);
                            } catch (CameraAccessException e) {
                                callback.onVerified(false, "请求提交失败: " + e.getMessage());
                            }
                        }
                        @Override
                        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                            callback.onVerified(false, "会话配置失败");
                        }
                    }, backgroundHandler);
        } catch (CameraAccessException e) {
            callback.onVerified(false, "创建请求失败: " + e.getMessage());
        }
    }

    private void startBackgroundThread() {
        backgroundThread = new HandlerThread("CameraBackground");
        backgroundThread.start();
        backgroundHandler = new Handler(backgroundThread.getLooper());
    }

    public void closeCamera() {
        try {
            if (captureSession != null) { captureSession.close(); captureSession = null; }
            if (cameraDevice != null) { cameraDevice.close(); cameraDevice = null; }
            if (imageReader != null) { imageReader.close(); imageReader = null; }
            if (backgroundThread != null) {
                backgroundThread.quitSafely();
                try { backgroundThread.join(); } catch (InterruptedException e) { }
                backgroundThread = null;
                backgroundHandler = null;
            }
        } catch (Exception e) { }
    }

    // 简化权限检查(实际需在 Activity 中处理)
    private boolean checkSelfPermission(Context context) {
        return false; // 实际项目中需检查 CAMERA 权限
    }
}

调用方式

// 在 Activity 或 Fragment 中调用
CameraFormatSetter setter = new CameraFormatSetter();
setter.openCamera(this, new CameraFormatSetter.VerificationCallback() {
    @Override
    public void onVerified(boolean success, String message) {
        // 在 UI 线程显示结果
        runOnUiThread(() -> {
            if (success) {
                // 设置生效
                Log.i("CameraFormat", "验证通过: " + message);
            } else {
                Log.e("CameraFormat", "验证失败: " + message);
            }
        });
    }
});

// 页面销毁时释放
@Override
protected void onDestroy() {
    super.onDestroy();
    setter.closeCamera();
}

验证方法

运行应用后,观察 Logcat 中 CameraFormat 标签的输出。若打印 验证通过: 实际格式: 17 (NV21=17), 分辨率: 640x480,则说明设置生效。其中 17 是 ImageFormat.NV21 的常量值。若输出 验证失败 或摄像头不支持提示,则说明设备不支持该组合或设置未生效。

判定标准

  • Logcat 中显示格式常量为 17(NV21),且宽高为 640x480,即判定为通过。
  • 若设备不支持 NV21 640x480,代码会提前返回失败信息,此时应更换设备或调整目标参数。
  • 实际预览画面无需显示,本案例仅验证格式配置是否正确。