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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 05:55:56