如何在Android NDK Camera2(C++)中无视图捕获预览帧并编码为JPEG二进制缓冲区
使用Android NDK Camera2在C++中捕获预览帧并编码为JPEG二进制缓冲区
不需要依赖Android视图或JNIEnv的核心思路是利用NDK Camera2的ACameraImageReader组件——它可以直接接收相机的预览帧数据,无需绑定到SurfaceView/TextureView这类UI组件。以下是完整的实现步骤和关键代码:
1. 初始化NDK Camera环境
首先获取相机管理器并枚举可用相机:
#include <camera/NdkCameraManager.h> #include <camera/NdkCameraDevice.h> #include <camera/NdkCameraImageReader.h> // 全局变量示例(实际项目建议封装成类) ACameraManager* camera_manager = nullptr; const char* target_camera_id = nullptr; ACameraDevice* camera_device = nullptr; ACameraCaptureSession* capture_session = nullptr; ACameraImageReader* image_reader = nullptr; // 初始化相机管理器 camera_manager = ACameraManager_create(); if (camera_manager == nullptr) { // 处理错误 return; } // 枚举相机ID int32_t camera_count = 0; const char** camera_ids = nullptr; if (ACameraManager_getCameraIdList(camera_manager, &camera_count, &camera_ids) != ACAMERA_OK) { // 处理错误 ACameraManager_delete(camera_manager); return; } // 选择后置相机(示例逻辑,可根据需求调整) for (int i = 0; i < camera_count; i++) { ACameraMetadata* metadata = nullptr; if (ACameraManager_getCameraCharacteristics(camera_manager, camera_ids[i], &metadata) == ACAMERA_OK) { int32_t facing = 0; ACameraMetadata_getInt32(metadata, ACAMERA_LENS_FACING, &facing); if (facing == ACAMERA_LENS_FACING_BACK) { target_camera_id = camera_ids[i]; break; } ACameraMetadata_free(metadata); } }
2. 创建ACameraImageReader(核心:无视图接收帧)
创建图像阅读器来接收预览帧,指定合适的尺寸、格式和缓存数量:
// 选择相机支持的预览尺寸和格式(示例用1920x1080,YUV420格式) int width = 1920; int height = 1080; // YUV420是相机预览常用格式,兼容性好,适合转JPEG int format = AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420; int max_images = 2; // 缓存2帧,避免丢帧 image_reader = ACameraImageReader_new(width, height, format, max_images); if (image_reader == nullptr) { // 处理错误 ACameraManager_deleteCameraIdList(camera_ids); ACameraManager_delete(camera_manager); return; } // 设置图像可用回调,当有预览帧到达时触发 ACameraImageReader_setImageAvailableListener(image_reader, [](ACameraImageReader* reader, void* context) { // 帧处理逻辑见下文 onImageAvailable(reader, context); }, nullptr);
3. 启动相机预览会话
打开相机设备,创建捕获请求并启动重复预览:
// 打开相机设备 ACameraManager_openCamera(camera_manager, target_camera_id, [](ACameraDevice* device, void* context, ACameraDevice_status_t status) { if (status != ACAMERA_OK) { // 处理相机打开错误 return; } camera_device = device; // 获取图像阅读器的Surface ANativeWindow* reader_surface = ACameraImageReader_getSurface(image_reader); ANativeWindow* surfaces[] = {reader_surface}; // 创建捕获会话 ACameraDevice_createCaptureSession(camera_device, 1, surfaces, [](ACameraCaptureSession* session, void* context, ACameraCaptureSession_status_t status) { if (status != ACAMERA_OK) { // 处理会话创建错误 return; } capture_session = session; // 创建重复捕获请求(预览) ACaptureRequest* request = nullptr; ACameraDevice_createCaptureRequest(camera_device, TEMPLATE_PREVIEW, &request); ACaptureRequest_addTarget(request, reader_surface); // 启动重复预览 ACameraCaptureSession_setRepeatingRequest(capture_session, nullptr, request, nullptr); ACaptureRequest_free(request); }, nullptr); ANativeWindow_release(reader_surface); }, nullptr);
4. 帧处理与JPEG编码
在图像可用回调中,获取帧数据并转码为JPEG二进制缓冲区:
#include <jpeglib.h> #include <hardware/gralloc.h> void onImageAvailable(ACameraImageReader* reader, void* context) { // 获取下一个图像帧 ACameraImage* image = ACameraImageReader_acquireNextImage(reader); if (image == nullptr) return; // 获取硬件缓冲区 AHardwareBuffer* buffer = ACameraImage_getHardwareBuffer(image); if (buffer == nullptr) { ACameraImage_close(image); return; } // 描述缓冲区信息 AHardwareBuffer_Desc desc; AHardwareBuffer_describe(buffer, &desc); // 锁定缓冲区,获取CPU可访问的像素数据 void* pixels; if (AHardwareBuffer_lock(buffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, -1, nullptr, &pixels) != 0) { AHardwareBuffer_release(buffer); ACameraImage_close(image); return; } // 将YUV数据编码为JPEG,输出到二进制缓冲区 uint8_t* jpeg_buffer = nullptr; size_t jpeg_size = 0; convertYUV420ToJPEG(static_cast<uint8_t*>(pixels), desc.width, desc.height, &jpeg_buffer, &jpeg_size); // 这里可以使用jpeg_buffer(二进制JPEG数据),比如保存、传输等 // 示例:打印JPEG大小 // printf("JPEG buffer size: %zu bytes\n", jpeg_size); // 释放资源 free(jpeg_buffer); AHardwareBuffer_unlock(buffer); AHardwareBuffer_release(buffer); ACameraImage_close(image); } // YUV420转JPEG的实现(基于libjpeg) void convertYUV420ToJPEG(uint8_t* yuv_data, int width, int height, uint8_t** jpeg_out, size_t* jpeg_size_out) { struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; // 初始化JPEG压缩结构体 cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); // 设置内存输出 unsigned char* buffer = nullptr; long buffer_size = 0; jpeg_mem_dest(&cinfo, &buffer, &buffer_size); // 配置图像参数 cinfo.image_width = width; cinfo.image_height = height; cinfo.input_components = 3; cinfo.in_color_space = JCS_YCbCr; // libjpeg原生支持YCbCr输入 jpeg_set_defaults(&cinfo); jpeg_set_quality(&cinfo, 90, TRUE); // 设置JPEG质量(0-100) // 开始压缩 jpeg_start_compress(&cinfo, TRUE); JSAMPROW row_ptr[1]; int row_stride = width * 3; // 每行YCbCr数据的字节数 uint8_t* row_buffer = static_cast<uint8_t*>(malloc(row_stride)); uint8_t* y_plane = yuv_data; uint8_t* uv_plane = y_plane + width * height; while (cinfo.next_scanline < cinfo.image_height) { int y_row = cinfo.next_scanline; int uv_row = y_row / 2; // 填充一行YCbCr数据 for (int x = 0; x < width; x++) { // Y分量 row_buffer[x*3] = y_plane[y_row * width + x]; // Cb/Cr分量(YUV420是隔行采样) int uv_idx = uv_row * width + (x / 2) * 2; row_buffer[x*3+1] = uv_plane[uv_idx]; row_buffer[x*3+2] = uv_plane[uv_idx + 1]; } row_ptr[0] = row_buffer; jpeg_write_scanlines(&cinfo, row_ptr, 1); } // 完成压缩 jpeg_finish_compress(&cinfo); jpeg_destroy_compress(&cinfo); free(row_buffer); *jpeg_out = buffer; *jpeg_size_out = buffer_size; }
5. 资源清理
在退出时务必释放所有NDK Camera资源:
// 停止预览会话 if (capture_session != nullptr) { ACameraCaptureSession_stopRepeating(capture_session); ACameraCaptureSession_close(capture_session); } // 关闭相机设备 if (camera_device != nullptr) { ACameraDevice_close(camera_device); } // 销毁图像阅读器 if (image_reader != nullptr) { ACameraImageReader_delete(image_reader); } // 释放相机ID列表和管理器 if (camera_ids != nullptr) { ACameraManager_deleteCameraIdList(camera_ids); } if (camera_manager != nullptr) { ACameraManager_delete(camera_manager); }
关键注意事项
- 权限要求:必须在
AndroidManifest.xml中声明CAMERA权限,Android 6及以上版本需要动态申请权限,否则无法打开相机。 - 格式兼容性:务必通过
ACameraDevice_getCharacteristics验证相机支持你指定的图像格式和尺寸,避免创建ACameraImageReader失败。 - 线程优化:JPEG编码属于CPU密集型操作,建议将转码逻辑放到单独的线程池执行,避免阻塞相机的回调线程导致丢帧。
- 硬件编码替代:如果追求更高性能,可以使用NDK的
MediaCodec接口进行硬件加速的JPEG编码,比libjpeg的软编码更快。 - 内存安全:所有NDK Camera对象和动态分配的缓冲区都要确保正确释放,避免内存泄漏。
内容的提问来源于stack exchange,提问作者DarkSoda
相关产品推荐
相关产品推荐

