摘要:本文记录了在 RK3588 + Android 14 + Redroid 云手机环境下,开发虚拟相机 HAL 实现过程中遇到的一系列问题及解决方案。涉及 Mali Gralloc 的 buffer 权限问题、格式覆盖机制、Shutter 通知等关键知识点,对 Android Camera HAL 开发者具有参考价值。
一、背景介绍
1.1 项目目标
基于 Redroid(Docker 化的 Android 容器方案)实现云手机的虚拟相机功能。目标是让相机应用打开时能显示自定义的虚拟画面(本文以红色画面为例进行调试)。
1.2 硬件与软件环境
- 硬件平台:RK3588
- 操作系统:Android 14
- GPU:Mali (使用 Mali Gralloc)
- Camera HAL 版本:HIDL Camera HAL 3.2
1.3 开发思路
参考 Android Emulator 的 goldfish 虚拟相机实现,编写一个 VirtualCameraSession 类来处理相机请求,将虚拟画面填充到 buffer 中返回给 Camera Framework。
二、踩坑之旅
2.1 第一个错误:Usage 不匹配
错误日志
E mali_gralloc: Usage not a subset. Buffer usage = 0x100, Descriptor usage = 0x20000
E GraphicBufferMapper: validateBufferSize(0x7a07a21448) failed: 3
E VirtualCameraSession: importBuffer failed: 3 (trying without import)
问题分析
Buffer usage = 0x100 → GRALLOC_USAGE_HW_TEXTURE (Buffer 创建时的 usage)
Descriptor usage = 0x20000 → GRALLOC_USAGE_HW_CAMERA_WRITE (我代码中使用的 usage)
Mali Gralloc 要求 importBuffer 时的 usage 必须是原始 buffer usage 的子集 。由于 buffer 创建时只有 HW_TEXTURE 权限,没有 SW_WRITE 或 HW_CAMERA_WRITE 权限,导致 import 失败。
错误代码
cpp
// ❌ 错误:使用了 buffer 创建时不包含的 usage
err = mapper.importBuffer(handle, ..., GRALLOC_USAGE_HW_CAMERA_WRITE, ...);
解决方案
从源头解决! 在 configureStreams 中通过 producerUsage 告诉 Framework 分配 buffer 时需要包含 CPU 写入权限:
cpp
// ✅ 正确:在 configureStreams 中请求 SW_WRITE 权限
halConfig.streams[i].producerUsage = GRALLOC_USAGE_SW_WRITE_OFTEN;
这样 Framework 分配 buffer 时就会包含 SW_WRITE_OFTEN 权限,后续 importBuffer 使用相同的 usage 就不会报错了。
2.2 第二个错误:Lock 前未 Import
修改 usage 后,尝试跳过 import 直接 lock,又遇到新问题:
错误日志
E mali_gralloc: Attempt to lock buffer before importBuffer
E mali_gralloc: Locking buffer failed with error: -22
问题分析
这涉及到 跨进程 Buffer 管理机制:
┌─────────────────────────────────────────────────────────────────────┐
│ 跨进程 Buffer 传递流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Camera Framework (cameraserver) Camera HAL (你的进程) │
│ │
│ 1. allocateBuffer() │
│ └─ buffer 分配在 cameraserver │
│ │
│ 2. HIDL 传递 native_handle_t ──────────> 3. 收到 handle │
│ (只是文件描述符的引用) (未 import 状态) │
│ │
│ 4. 必须 importBuffer() │
│ (在本进程建立映射) │
│ │
│ 5. lock() → 写入 → unlock │
│ │
│ 6. freeBuffer() │
│ (释放本进程的映射) │
│ │
└─────────────────────────────────────────────────────────────────────┘
关键认知 :Camera Framework 传过来的 native_handle_t 只是一个文件描述符的引用,在 HAL 进程中必须先 import 才能建立本地映射,然后才能 lock。
解决方案
cpp
// 步骤 1: Import buffer 到当前进程
buffer_handle_t importedHandle = nullptr;
uint64_t usage = GRALLOC_USAGE_SW_WRITE_OFTEN;
err = mapper.importBuffer(handle, width, height, 1, format, usage, width, &importedHandle);
if (err != android::OK) {
ALOGE("importBuffer failed");
return;
}
// 步骤 2: Lock 并写入数据
err = mapper.lockYCbCr(importedHandle, usage, bounds, &ycbcr);
if (err == android::OK) {
// 填充数据...
mapper.unlock(importedHandle);
}
// 步骤 3: 释放 imported handle
mapper.freeBuffer(importedHandle);
2.3 第三个错误:格式不匹配
解决了 import 问题后,又遇到新的错误:
错误日志
I VirtualCameraSession: Stream 1: overriding IMPLEMENTATION_DEFINED(34) to YCBCR_420_888(35)
I VirtualCameraSession: Configured stream 1: 1280x720 format=34->35
...
I VirtualCameraSession: Processing stream 1: 1280x720 format=34
E mali_gralloc: Buffer requested format: 0x23 does not match descriptor format: 0x22
- 0x23 = 35 =
YCBCR_420_888(覆盖后的格式) - 0x22 = 34 =
IMPLEMENTATION_DEFINED(原始格式)
问题分析
在 configureStreams 中设置了 overrideFormat = YCBCR_420_888,Framework 会用覆盖后的格式分配 buffer。
但在 processCaptureRequest 中,我还在使用 mStreams[i].format(原始格式)来 import,导致格式不匹配!
解决方案
保存覆盖后的格式,在处理请求时使用正确的格式:
cpp
// configureStreams 中保存覆盖后的格式
std::map<int32_t, int32_t> mStreamFormatMap;
for (size_t i = 0; i < mStreams.size(); i++) {
int32_t overrideFormat;
if (mStreams[i].format == PixelFormat::IMPLEMENTATION_DEFINED) {
overrideFormat = HAL_PIXEL_FORMAT_YCbCr_420_888;
halConfig.streams[i].overrideFormat = PixelFormat::YCBCR_420_888;
} else {
overrideFormat = static_cast<int32_t>(mStreams[i].format);
}
// ⭐ 保存覆盖后的格式
mStreamFormatMap[mStreams[i].id] = overrideFormat;
}
// processCaptureRequest 中使用覆盖后的格式
auto it = mStreamFormatMap.find(stream.id);
int32_t format = (it != mStreamFormatMap.end()) ? it->second : stream.format;
2.4 第四个错误:lockYCbCr 返回空指针
格式匹配后,日志显示 import 和 lock 都成功了,但程序崩溃:
错误日志
I VirtualCameraSession: importBuffer success: imported=0xb400007b7a5addd0
I VirtualCameraSession: lockYCbCr success: y=0x0, cb=0x0, cr=0x0 ← 指针全是 NULL!
...
E Camera3-Device: Unable to submit capture request 0 to HAL device: Broken pipe (-32)
问题分析
lockYCbCr 返回成功(err == OK),但 ycbcr.y、ycbcr.cb、ycbcr.cr 都是 NULL!代码直接写入 NULL 指针导致 进程崩溃(Broken pipe)。
这是因为 Mali Gralloc 对某些格式(如 IMPLEMENTATION_DEFINED)可能返回无效的 YCbCr 结构。
解决方案
添加空指针检查,如果指针无效则回退到普通 lock:
cpp
err = mapper.lockYCbCr(importedHandle, usage, bounds, &ycbcr);
if (err == android::OK) {
// ⭐ 关键:检查指针是否有效
if (ycbcr.y != nullptr && ycbcr.cb != nullptr && ycbcr.cr != nullptr) {
fillYUVRed(ycbcr, width, height);
mapper.unlock(importedHandle);
} else {
ALOGW("lockYCbCr returned OK but pointers are NULL!");
mapper.unlock(importedHandle);
// 回退到普通 lock...
}
}
2.5 第五个错误:Buffer 返回超时
前面的问题都解决后,画面填充成功了,但 Framework 报超时:
错误日志
I VirtualCameraSession: ✔ Filled YUV red frame 1280x720
I VirtualCameraSession: Frame 0: returning 1 filled buffers
I VirtualCameraSession: Frame 1: returning 1 filled buffers
I VirtualCameraSession: Frame 2: returning 1 filled buffers
I VirtualCameraSession: Frame 3: returning 1 filled buffers
E Camera3-Stream: getBuffer: wait for output buffer return timed out after 8000ms (max_buffers 4)
4 个 buffer 都填充成功并"返回"了,但 Framework 等待 8 秒后超时!
问题分析
对比 Google 官方的 goldfish 虚拟相机实现,发现两个关键差异:
差异 1:缺少 Shutter 通知
Goldfish 在返回结果之前 会先发送 notify(SHUTTER) 通知:
cpp
// goldfish 代码
notifyShutter(&*mCb, frameNumber, shutterTimestampNs); // ⭐ 先发 Shutter
// ... 处理 buffer ...
mCb->processCaptureResult(results); // 再返回结果
Camera Framework 期望的流程是:
- 收到
notify(SHUTTER) - 收到
processCaptureResult()
缺少 Shutter 通知,Framework 会一直等待!
差异 2:返回 buffer 时不应包含 handle
cpp
// ❌ 错误
result.outputBuffers[i].buffer = request.outputBuffers[i].buffer;
// ✅ 正确:返回时设为 nullptr,Framework 通过 bufferId 识别
result.outputBuffers[i].buffer = nullptr;
解决方案
cpp
Return<void> VirtualCameraSession::processCaptureRequest(...) {
for (const auto& request : requests) {
const uint32_t frameNumber = request.frameNumber;
int64_t timestamp = getTimestampNs();
// ⭐ 第一步:发送 Shutter 通知
{
hidl_vec<NotifyMsg> msgs(1);
msgs[0].type = MsgType::SHUTTER;
msgs[0].msg.shutter.frameNumber = frameNumber;
msgs[0].msg.shutter.timestamp = timestamp;
mCallback->notify(msgs);
}
// 第二步:填充 buffer
for (const auto& outputBuffer : request.outputBuffers) {
fillRedFrame(outputBuffer.buffer, width, height, format);
}
// 第三步:返回结果
CaptureResult result;
result.frameNumber = frameNumber;
// ⭐ buffer 返回时设为 nullptr
for (size_t i = 0; i < request.outputBuffers.size(); i++) {
result.outputBuffers[i].streamId = request.outputBuffers[i].streamId;
result.outputBuffers[i].bufferId = request.outputBuffers[i].bufferId;
result.outputBuffers[i].buffer = nullptr; // ⭐ 关键
result.outputBuffers[i].status = BufferStatus::OK;
}
mCallback->processCaptureResult(results);
}
}
三、完整解决方案
3.1 核心修复点总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Usage 不匹配 | Buffer 创建时没有 SW_WRITE 权限 | configureStreams 中设置 producerUsage = SW_WRITE_OFTEN |
| Lock 前未 Import | 跨进程 buffer 必须先 import | 先 importBuffer,再 lock,最后 freeBuffer |
| 格式不匹配 | 使用原始格式而非覆盖后格式 | 用 map 保存覆盖后的格式 |
| 空指针崩溃 | lockYCbCr 返回空指针 |
检查 ycbcr.y/cb/cr 是否为空 |
| Buffer 返回超时 | 缺少 Shutter 通知 | 先 notify(SHUTTER) 再 processCaptureResult |
3.2 关键代码片段
configureStreams
cpp
Return<void> VirtualCameraSession::configureStreams(
const StreamConfiguration& requestedConfiguration,
configureStreams_cb _hidl_cb) {
for (size_t i = 0; i < mStreams.size(); i++) {
// 格式覆盖
if (mStreams[i].format == PixelFormat::IMPLEMENTATION_DEFINED) {
halConfig.streams[i].overrideFormat = PixelFormat::YCBCR_420_888;
mStreamFormatMap[mStreams[i].id] = HAL_PIXEL_FORMAT_YCbCr_420_888;
}
// ⭐ 请求 CPU 写入权限
halConfig.streams[i].producerUsage = GRALLOC_USAGE_SW_WRITE_OFTEN;
halConfig.streams[i].maxBuffers = 4;
}
_hidl_cb(Status::OK, halConfig);
return Void();
}
processCaptureRequest
cpp
Return<void> VirtualCameraSession::processCaptureRequest(
const hidl_vec<CaptureRequest>& requests, ...) {
for (const auto& request : requests) {
int64_t timestamp = getTimestampNs();
// ⭐ 1. 发送 Shutter 通知
{
hidl_vec<NotifyMsg> msgs(1);
msgs[0].type = MsgType::SHUTTER;
msgs[0].msg.shutter.frameNumber = request.frameNumber;
msgs[0].msg.shutter.timestamp = timestamp;
mCallback->notify(msgs);
}
// ⭐ 2. 填充 buffer
for (const auto& outputBuffer : request.outputBuffers) {
int32_t format = mStreamFormatMap[outputBuffer.streamId];
fillRedFrame(outputBuffer.buffer, width, height, format);
}
// ⭐ 3. 返回结果
hidl_vec<CaptureResult> results(1);
results[0].frameNumber = request.frameNumber;
results[0].outputBuffers.resize(request.outputBuffers.size());
for (size_t i = 0; i < request.outputBuffers.size(); i++) {
results[0].outputBuffers[i].buffer = nullptr; // ⭐ 关键
results[0].outputBuffers[i].status = BufferStatus::OK;
}
mCallback->processCaptureResult(results);
}
return Void();
}
fillRedFrame
cpp
void VirtualCameraSession::fillRedFrame(const native_handle_t* handle,
uint32_t width, uint32_t height,
int32_t format) {
GraphicBufferMapper& mapper = GraphicBufferMapper::get();
// ⭐ 1. Import buffer
buffer_handle_t importedHandle = nullptr;
uint64_t usage = GRALLOC_USAGE_SW_WRITE_OFTEN;
mapper.importBuffer(handle, width, height, 1, format, usage, width, &importedHandle);
// ⭐ 2. Lock 并写入
android_ycbcr ycbcr;
if (mapper.lockYCbCr(importedHandle, usage, bounds, &ycbcr) == OK) {
// ⭐ 检查空指针
if (ycbcr.y && ycbcr.cb && ycbcr.cr) {
fillYUVRed(ycbcr, width, height);
}
mapper.unlock(importedHandle);
}
// ⭐ 3. 释放
mapper.freeBuffer(importedHandle);
}
四、经验总结
4.1 调试技巧
-
善用 logcat 过滤:
bashadb logcat -v threadtime VirtualCameraSession Camera3-Device mali_gralloc GraphicBufferMapper *:S -
理解错误码 :Mali Gralloc 的错误信息很详细,如
Usage not a subset直接指明了问题。 -
参考官方实现 :Google 的 goldfish 虚拟相机是最好的参考,尤其是
CameraDeviceSession和FakeRotatingCamera类。
4.2 关键知识点
-
Gralloc Buffer 权限管理:
importBuffer时的 usage 必须是 buffer 创建时 usage 的子集- 需要 CPU 写入权限时,必须在
configureStreams中通过producerUsage请求
-
Camera HAL 回调时序:
- 必须先发送
notify(SHUTTER) - 然后才能调用
processCaptureResult
- 必须先发送
-
Buffer 返回规范:
- 返回结果时
buffer字段设为nullptr - Framework 通过
bufferId识别 buffer
- 返回结果时
-
格式覆盖机制:
IMPLEMENTATION_DEFINED需要覆盖为具体格式processCaptureRequest中要使用覆盖后的格式
4.3 避坑清单
-
configureStreams中设置正确的producerUsage -
importBuffer使用与producerUsage相同的 usage - 保存并使用覆盖后的格式
-
lockYCbCr后检查返回的指针是否为空 - 先发送
notify(SHUTTER)再返回结果 - 返回 buffer 时设置
buffer = nullptr - 使用
CLOCK_BOOTTIME获取时间戳
五、参考资料
作者:FrameNotWork
环境:RK3588 + Android 14 + Redroid
关键词:虚拟相机、Camera HAL、Mali Gralloc、RK3588、Android 14、Redroid
如果这篇文章对你有帮助,欢迎点赞收藏!有问题欢迎在评论区交流讨论。