Android NDK 相机实战:用 AImageReader 从首帧走到 JPEG
Android NDK 相机实战用 AImageReader 从首帧走到 JPEG【免费下载链接】ndk-samplesAndroid NDK samples with Android Studio项目地址: https://gitcode.com/gh_mirrors/nd/ndk-samples玩过 NDK 相机的人多半卡在这三件事上预览画面横转 90 度、拖动曝光滑条时掉帧、按下快门后预览直接冻住。ndk-samples 仓库里的camera/basic样例是一个纯 NativeActivity 实现基于 NdkCamera API 和 AImageReader 搭成上面三个问题的处理都写在代码里。本文就以它为线索把整条管线走一遍。【环境准备】克隆样例仓库git clone https://gitcode.com/gh_mirrors/nd/ndk-samples样例在camera/basic/下camera/texture-view/是 TextureView 预览版本两者共用同一套设备与会话层。 预览横转 90 度先把显示旋转角算对现象首帧出来画面是躺着的。一句话结论传感器在板卡上的安装角度是固定的SENSOR_ORIENTATION是只读常量最终显示角 传感器角度 ± 设备旋转方向前摄还要再来一次镜像。【Native 侧】CreateCamera 时算出最终显示角摘自camera/basic/src/main/cpp/camera_engine.cppint32_t facing 0, angle 0, imageRotation 0; if (camera_-GetSensorOrientation(facing, angle)) { if (facing ACAMERA_LENS_FACING_FRONT) { imageRotation (angle rotation_) % 360; imageRotation (360 - imageRotation) % 360; // 前摄镜像旋转方向要反过来 } else { imageRotation (angle - rotation_ 360) % 360; } } // 角度交给 reader由 YUV→RGB 循环做 CPU 旋转 yuvReader_-SetPresentRotation(imageRotation);为什么用 CPU 旋转因为这条预览通路不走 GPUYUV420 从 AImageReader 出来在渲染循环里逐像素转成 RGBA 再 lock 窗口 post 上去旋转就顺理成章地做在转换循环里四个角度对应四套坐标映射【Native 侧】90° 版转换循环的核心摘自image_reader.cppuint32_t* out static_castuint32_t*(buf-bits); out height - 1; for (int32_t y 0; y height; y) { const uint8_t* pY yPixel yStride * (y srcRect.top) srcRect.left; // U/V 平面指针与 uv_offset 的推导从略 for (int32_t x 0; x width; x) { // (x, y) → (-y, x)逆时针 90°源图的一行变成目标的一列 out[x * buf-stride] YUV2RGB(pY[x], pU[uv_offset], pV[uv_offset]); } out - 1; // 一行写完列指针回退一格 }注意imageRotation 不是常量。设备方向在APP_CMD_CONFIG_CHANGED里变化时必须重算样例的处理最直白——把相机整个拆掉重建。验证方法把角度打日志转一下设备确认数值按 90 步进变化如果画面还是躺的十有八九是漏了前摄的镜像分支。⚡ AImageReader 两种取帧姿势与掉帧修复旋转问题解决了紧接着会撞上掉帧。一句话结论AImageReader 本质是一条 ring buffer 队列相机是生产者你的渲染循环是消费者掉帧都出在消费者比生产者慢。创建 reader 时构造参数就定了三件事【Native 侧】创建 reader 并注册帧可用回调摘自image_reader.cppmedia_status_t status AImageReader_new(res-width, res-height, format, MAX_BUF_COUNT, reader_); // 4 个缓冲 AImageReader_ImageListener listener{ .context this, .onImageAvailable OnImageCallback, // JPEG reader 靠它触发落盘 }; AImageReader_setImageListener(reader_, listener);为什么是 4 个缓冲相机要有空闲 buffer 才肯继续产帧消费者不及时AImage_delete归还队列塞满后就是相机侧丢帧——表现恰好就是预览卡顿。取帧 API 给的是两个选错是最常见的翻车点场景API积压时的行为实时预览只关心最新画面acquireLatestImageGetLatestImage丢弃队列里的旧帧只给最新离线逐帧处理不能丢acquireNextImageGetNextImage顺序取、不丢帧但延迟会越攒越长样例预览走的是GetNextImage但渲染循环的姿态值得照抄【Native 侧】渲染主循环的每帧一次拉取摘自camera_engine.cppvoid CameraEngine::DrawFrame(void) { if (!cameraReady_ || !yuvReader_) return; AImage* image yuvReader_-GetNextImage(); if (!image) return; // 没新帧就跳过本轮不阻塞、不重绘旧帧 ANativeWindow_acquire(app_-window); ANativeWindow_Buffer buf; if (ANativeWindow_lock(app_-window, buf, nullptr) 0) { yuvReader_-DeleteImage(image); // 失败也要归还缓冲 return; } yuvReader_-DisplayImage(buf, image); // YUV→RGB 旋转内含 AImage_delete ANativeWindow_unlockAndPost(app_-window); ANativeWindow_release(app_-window); }这段跑在 android_main 的 poll 循环里。验证方法给 DrawFrame 打平均耗时日志稳定超过 33ms 说明瓶颈在 CPU 转换侧此时可换GetLatestImage丢旧帧或降低预览分辨率——样例的MatchCaptureSizeRequest特意选与屏幕同宽高比的最小 YUV 分辨率就是为了压转换量。 JPEG 拍照单次捕获与恢复预览的坑现象按下快门照片存好了预览冻住不动。原因setRepeatingRequest是重复请求单次捕获必须先把 repeating 停掉而且capture()是异步的提交完就返回真正的完成信号走onCaptureSequenceCompleted回调。【Native 侧】提交单次拍照请求摘自camera_manager.cppbool NDKCamera::TakePhoto(void) { if (captureSessionState_ CaptureSessionState::ACTIVE) { ACameraCaptureSession_stopRepeating(captureSession_); // 先停 repeating } CALL_SESSION(capture(captureSession_, GetCaptureCallback(), 1, requests_[JPG_CAPTURE_REQUEST_IDX].request_, requests_[JPG_CAPTURE_REQUEST_IDX].sessionSequenceId_)); return true; // 只是提交了并不代表拍完 }恢复预览的动作放在回调里而不是调用方【Native 侧】序列结束时重新挂上重复请求摘自camera_listeners.cppvoid NDKCamera::OnCaptureSequenceEnd(ACameraCaptureSession* session, int sequenceId, int64_t frameNumber) { if (sequenceId ! requests_[JPG_CAPTURE_REQUEST_IDX].sessionSequenceId_) return; // 只处理属于我们的那次拍照 CALL_SESSION(setRepeatingRequest(captureSession_, nullptr, 1, requests_[PREVIEW_REQUEST_IDX].request_, nullptr)); }JPEG 这条路上还有一个容易忽略的点照片生成后onImageAvailable回调被触发样例起一个线程把 AImage 的 plane 0 直接写盘——JPEG 格式只有一个 plane无需任何转换。曝光/灵敏度的滑条也在这条链路上生效预览请求处于手动模式ACAMERA_CONTROL_AE_MODE_OFF加两次setEntry写曝光与感光度滑条改动后重新提交一次 repeating request 即可即时生效而 JPEG 请求只写了ACAMERA_JPEG_ORIENTATION一直留在自动模式。注意想让拍出的照片也跟随手动曝光需要把那三条 setEntry 同样抄到 JPG 请求上样例作者选择拍照留自动理由是观感更好。 从打开设备到出首帧完整调用时序前面三个环节串起来就是这一条调用链图里五个 NDK 对象Manager / Device / Session / Request / Output与 Camera2 模型一一对应样例只是在其上加了一层自己的 reader 封装。排查问题时可以对着这张表过一遍症状可能原因修法预览横转或上下颠倒旋转角没跟设备方向、前摄镜像按角度公式重算CONFIG_CHANGED 时重建相机掉帧、预览卡顿CPU 转 YUV 太慢消费者堵队列换 GetLatestImage 丢旧帧或降预览分辨率拍照后预览冻住忘了重新挂 repeating 请求在 onCaptureSequenceCompleted 里恢复颜色偏蓝或偏紫U/V 平面取反检查 plane 下标与 pixelStride 的推导资源释放是这条链路的镜像顺序停 repeating → 关 session → 逐个 free request/target → 释放创建 session 时ANativeWindow_acquire过的引用 → 关设备 → delete manager。样例析构函数一次做完最容易漏的就是窗口引用没 release。【免费下载链接】ndk-samplesAndroid NDK samples with Android Studio项目地址: https://gitcode.com/gh_mirrors/nd/ndk-samples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考