04_摄像头适配(二)
第一阶段:先让系统真正识别两个摄像头
刚开始打开 Camera App,最大的问题不是拍照,而是它甚至没有完整地把两个摄像头作为前后摄来认识。于是第一步就是确认底层到底暴露了几个 cameraId。
使用:
hidumper -s CameraService
可以看到系统里面有两个逻辑摄像头:
lcam001
lcam002
这里就有一个很关键的问题:OpenHarmony 上层并不知道 lcam001 应该是后摄,lcam002 应该是前摄。对人来说很好理解,对代码来说,不写它就不知道。
于是先在 V4L2 source node 中补上逻辑摄像头映射:
lcam001 -> CAMERA_FIRST
lcam002 -> CAMERA_SECOND
lcam003 -> CAMERA_THIRD
对应修改位置:
drivers/peripheral/camera/vdi_base/common/adapter/platform/v4l2/src/pipeline_core/nodes/v4l2_source_node/v4l2_source_node.cpp
与此同时,原来这里还有一部分逻辑被 V4L2_EMULATOR 之类的宏限制住,实际在 MUSEPaper2 上走的是真实 V4L2 设备路径,所以这部分也需要放开,不然上层以为自己在开摄像头,底层则像是在参加一场没有观众的演出。
第二阶段:让 V4L2 设备和 OpenHarmony 的格式对上
摄像头这部分特别容易出现一个现象:明明设备存在,日志也说打开了,但是就是没有图像。这个时候大概率要看格式。
MUSEPaper2 上对应的 V4L2 设备名称中有 Spacemit 的虚拟视频设备,例如:
spacemit vivi
spacemit vivi2
为了让 Camera Host 能够正确找到它们,我在 V4L2 file format 逻辑中加入了 fallback 匹配:
spacemit vivi -> svivi-vid-cap-00
spacemit vivi2 -> svivi-vid-cap-01
修改位置:
drivers/peripheral/camera/vdi_base/common/adapter/platform/v4l2/src/driver_adapter/src/v4l2_fileformat.cpp
接下来是格式映射。OpenHarmony Camera Framework 里面使用的是自己的 Camera Format,例如:
CAMERA_FORMAT_YCBCR_420_SP
CAMERA_FORMAT_YCRCB_420_SP
CAMERA_FORMAT_BLOB
而 V4L2 侧使用的是:
V4L2_PIX_FMT_NV12
V4L2_PIX_FMT_NV21
这两个体系要对上,不然上层想要 NV12,底层却不知道怎么给,结果就是预览黑屏或者 stream 创建失败。
因此在:
drivers/peripheral/camera/vdi_base/common/adapter/platform/v4l2/src/driver_adapter/include/v4l2_utils.h
中补充了映射:
CAMERA_FORMAT_YCBCR_420_SP -> V4L2_PIX_FMT_NV12
CAMERA_FORMAT_YCRCB_420_SP -> V4L2_PIX_FMT_NV21
CAMERA_FORMAT_BLOB -> V4L2_PIX_FMT_NV12
这里 BLOB 看起来有点奇怪,因为它一般用于 JPEG 拍照输出。但是当前这条 V4L2 路径还需要先保证 buffer 能够被正确申请和流转,所以这里先让它能够进入可用路径,后面再处理 JPEG 编码和保存。
这一步做完之后,摄像头从“看起来存在”变成了“真的能给我东西”。
第三阶段:补 metadata 和板级 hcs 能力
OpenHarmony 的 Camera App 在创建 preview/photo output 的时候,不是想要什么尺寸就直接创建,而是会先去读底层暴露出来的能力,也就是 metadata 和 hcs 中的配置。
如果底层没有告诉上层“我支持 1280x960 的 NV12 预览”,那么应用层就算硬写也没有用。
因此这里需要修改板级 camera host 配置:
vendor/spacemit/musepaper2/hdf_config/uhdf/camera/hdi_impl/camera_host_config.hcs
为两个摄像头都补充可用配置,包括:
640x480
1280x960
1280x720
720x720
1920x1080
并且同时暴露:
YCBCR_420_SP 预览
JPEG 拍照
除此之外,在:
drivers/peripheral/camera/vdi_base/v4l2/include/camera_host/metadata_enum_map.h
中补上:
OHOS_CAMERA_FORMAT_YCBCR_420_SP
这一步之后再看:
hidumper -s CameraService
可以看到两个摄像头都已经有了比较完整的 stream info。这里我还记了一下,当时两个摄像头的 Basic Stream Info Size 都能看到 10,也就是预览和拍照的组合已经能被系统读出来了。
第四阶段:Camera App 的前后摄切换
底层有两个摄像头是一回事,应用上能不能切换是另一回事。
开始的时候,Camera App 里切换按钮并不能稳定显示,或者显示了也切不过去。这里的问题主要在应用层:MUSEPaper2 是 RISC-V 板子,但是我们希望它走手机 Camera UI 路径,而不是 default 设备路径。
因此在产品参数里加入:
const.product.devicetype="phone"
位置:
vendor/spacemit/musepaper2/etc/param/product_musepaper2.para
这样 Camera App 会走 phone 端 UI。
然后在应用层做了几处适配:
applications/standard/camera/common/src/main/ets/default/camera/CameraService.ts
applications/standard/camera/common/src/main/ets/default/function/CameraBasicFunction.ts
applications/standard/camera/common/src/main/ets/default/featurecommon/cameraswitcher/CameraSwitchButton.ets
applications/standard/camera/product/phone/src/main/ets/pages/FootBar.ets
核心思路有三个:
- 初始化时用真实 camera count 更新
CameraPlatformCapability。 FootBar判断是否显示切换按钮时,不只依赖原本状态,也参考平台识别到的摄像头数量。- 把切换按钮的点击区域放到外层
Column上,不要让它看起来有按钮,实际上点起来像抽奖。
最后点击切换按钮后,日志能够看到:
createCameraInput id = lcam002
selected previewProfile = {"format":1004,"size":{"width":1280,"height":960}}
selected photoProfile = {"format":2000,"size":{"width":1280,"height":960}}
createSession end
这说明前摄切换已经真正走到了 lcam002,不是只改了 UI 状态。
至此,我终于可以认真说一句:前后摄切换成功。
第五阶段:预览画面和页面布局
摄像头能看到画面之后,新的问题出现了:Camera App 页面布局并不和谐,底部控制区有点和系统导航栏打架。
一开始看起来只是“按钮稍微靠下”,但是对于实际使用来说,这就是非常明显的问题。尤其是 MUSEPaper2 的屏幕尺寸和 phone UI 的默认布局并不完全一致。
在:
applications/standard/camera/product/phone/src/main/ets/pages/index.ets
中调整底部安全区域:
const ZOOM_HEIGHT = 140;
const BOTTOM_SAFE_AREA_HEIGHT = 240;
并在布局位置计算中使用:
.position({ y: Math.max(0, this.state.footBarHeight + ZOOM_HEIGHT - BOTTOM_SAFE_AREA_HEIGHT) })
最后用 uitest dumpLayout 看布局结果:
Camera app window: [0,32][1200,1800]
Preview/XComponent area: [0,73][1200,1672]
Shutter button: [543,1552][657,1666]
Camera switch button: [729,1576][795,1642]
System navigation bar: [0,1800][1200,1920]
这就比较清楚了,Camera 窗口底部到 y=1800,系统导航栏从 y=1800 开始,两个没有继续互相挤压。
UI 这种东西,代码改一行,观感差很多。虽然它不如驱动听起来“硬核”,但是用户第一眼看到的就是它,不能装作没看见。
第六阶段:拍照保存到相册
这部分是最折磨人的地方。
预览能看,前后摄能切,按下快门也有反应,但是照片没有进入相册。也就是说,用户看到的画面是活的,但按下快门之后,系统像是沉默了。
先看应用层日志:
ShutterButton
ACTION_CAPTURE
CameraService.takePicture
photoOutput captureStartWithInfo
说明应用层确实发起了拍照请求,而且 Framework 也给了 captureStart。
但是继续看 SaveCameraAsset,没有看到:
imageArrival
readNextImage
fileio write
这就说明 ImageReceiver 没收到真正的 JPEG 图像。
换成人话就是:
快门按下去了,系统也说“我开始拍了”,但是照片数据没有送到保存模块。
于是继续往 HDI 和 stream 方向看。这里发现 still capture 的 BLOB/JPEG stream 在申请 surface buffer 时出现了问题,日志里面能看到类似:
sfError = 40601000
继续顺藤摸瓜,在:
drivers/peripheral/camera/vdi_base/v4l2/src/stream_operator/stream_tunnel/standard/stream_tunnel.cpp
里面发现 BLOB buffer 原来有一个特殊处理,会把请求尺寸改成类似:
BLOB_MAX_SIZE x 1
这对于当前 MUSEPaper2 的 surface 请求路径并不合适,于是改成使用真实 photo size 申请 buffer。
同时在:
drivers/peripheral/camera/vdi_base/v4l2/src/stream_operator/stream_operator_vdi_impl.cpp
中补充 still capture / BLOB stream 的 JPEG encode type:
STILL_CAPTURE or BLOB -> ENCODE_TYPE_JPEG
这一步之后,底层 BLOB 请求失败的问题被缓解了。但是当前这条 VDI 路径还有一个现实问题:PhotoOutput.capture() 能触发 captureStartWithInfo,但在 app 侧依旧等不到 ImageReceiver imageArrival。
也就是说,标准 still JPEG 路径还没有完全打通。
这里我没有选择简单地在设备上手动推一个文件装作成功,因为那不是源码级适配,重新编译烧录后就没了。最后采用的方式是:
- 应用层仍然优先走标准
PhotoOutput.capture()。 SaveCameraAsset等待真实 JPEG 图像保存。- 如果在超时时间内没有收到
imageArrival,则截取当前预览区域。 - 将
PixelMap打包成 JPEG。 - 通过媒体库创建图片资产,写入相册。
关键代码位置:
applications/standard/camera/common/src/main/ets/default/camera/CameraService.ts
applications/standard/camera/common/src/main/ets/default/camera/SaveCameraAsset.ts
applications/standard/camera/product/phone/src/main/module.json5
在 CameraService.ts 中加入截图兜底区域:
const MUSE_PAPER2_CAPTURE_RECT = {
left: 0,
top: 72,
width: 1200,
height: 1480
};
拍照逻辑变成:
const imageSaved = this.mSaveCameraAsset.waitForNextImageSaved(CAPTURE_SAVE_TIMEOUT_MS);
await this.mPhotoOutPut.capture(this.mCaptureSetting);
if (!await imageSaved) {
const fallbackSaved = await this.savePreviewSnapshot();
if (fallbackSaved) {
return true;
}
return false;
}
在 SaveCameraAsset.ts 中增加:
savePixelMapAsImage(pixelMap, ...)
主要做的事情就是:
PixelMap
-> image.createImagePacker()
-> packToFile(..., image/jpeg)
-> createPhotoAsset
-> setPending(false)
-> onCaptureSuccess
由于这里使用了截图能力,还需要在 Camera HAP 的 module.json5 中加入权限:
{
"name": "ohos.permission.CAPTURE_SCREEN"
}
这一步严格来说不是最理想的终点,最理想的当然是底层 still JPEG 直接送到 ImageReceiver。
但是它是源码级适配,Camera App 重新编译安装后可以稳定工作;同时标准 still capture 路径也保留着,后续底层完全打通后可以自然切回真正的 JPEG 输出。
编译和部署验证
这部分延续上一篇的思路,不只是改代码,还要真的编译、推送、重启、验证。
HDI 和 pipeline:
ninja -C /home/dfq/repo/OpenHarmony/oh61/out/musepaper2 \
-f camera_only.ninja \
spacemit_products/spacemit_products/libcamera_host_vdi_impl_1.0.z.so
Camera Framework:
ninja -C /home/dfq/repo/OpenHarmony/oh61/out/musepaper2 \
-f camera_framework_only.ninja \
multimedia/camera_framework/libcamera_framework.z.so
ninja -C /home/dfq/repo/OpenHarmony/oh61/out/musepaper2 \
-f camera_service_only.ninja \
multimedia/camera_framework/libcamera_service.z.so
Camera HAP:
cd /home/dfq/repo/OpenHarmony/oh61/applications/standard/camera
OHOS_BASE_SDK_HOME=/home/dfq/repo/OpenHarmony/oh61/prebuilts/ohos-sdk/linux \
node /home/dfq/repo/OpenHarmony/oh61/prebuilts/tool/command-line-tools/6.x/hvigor/bin/hvigorw.js \
assembleApp --no-daemon --no-incremental
推送到设备:
hdc -t YLMPK100081280004 target mount
hdc -t YLMPK100081280004 file send \
out/musepaper2/spacemit_products/spacemit_products/libcamera_host_vdi_impl_1.0.z.so \
/vendor/lib64/libcamera_host_vdi_impl_1.0.z.so
hdc -t YLMPK100081280004 file send \
out/musepaper2/hdf/drivers_peripheral_camera/libperipheral_camera_pipeline_core.z.so \
/vendor/lib64/libperipheral_camera_pipeline_core.z.so
hdc -t YLMPK100081280004 file send \
out/musepaper2/multimedia/camera_framework/libcamera_framework.z.so \
/system/lib64/platformsdk/libcamera_framework.z.so
hdc -t YLMPK100081280004 file send \
out/musepaper2/multimedia/camera_framework/libcamera_service.z.so \
/system/lib64/libcamera_service.z.so
hdc -t YLMPK100081280004 install -r \
applications/standard/camera/product/phone/build/default/outputs/default/phone-default-signed.hap
这里还遇到了一个和摄像头无关的问题:完整编译时 IPC 单元测试里有重复 ninja rule:
multiple rules generate ... ipc_trace.o
所以摄像头验证阶段主要使用 camera 相关的小目标进行构建。这个问题先记录下来,后续单独处理。
最终验证结果
启动 Camera App:
aa start -a com.ohos.camera.MainAbility -b com.ohos.camera
后摄验证:
createCameraInput id = lcam001
selected previewProfile = {"format":1004,"size":{"width":1280,"height":960}}
selected photoProfile = {"format":2000,"size":{"width":1280,"height":960}}
拍照后日志:
photoOutput captureStartWithInfo: {"captureId":2,"time":-1}
waitForNextImageSaved timeout
savePreviewSnapshot
savePixelMapAsImage saved photoUri:
file://media/Photo/2/IMG_946889474_001/IMG_200013_164934.jpg
前摄验证:
createCameraInput id = lcam002
selected previewProfile = {"format":1004,"size":{"width":1280,"height":960}}
selected photoProfile = {"format":2000,"size":{"width":1280,"height":960}}
拍照后日志:
photoOutput captureStartWithInfo: {"captureId":4,"time":-1}
waitForNextImageSaved timeout
savePreviewSnapshot
savePixelMapAsImage saved photoUri:
file://media/Photo/3/IMG_946889508_002/IMG_200013_165008.jpg
也就是说,最终结果是:
后摄:可预览、可拍照、可保存到相册
前摄:可切换、可预览、可拍照、可保存到相册
页面:底栏不再压到系统导航栏
总结
这一次摄像头适配看起来只是一个外设驱动问题,但是实际做下来跨了很多层:
HCS 能力配置
Metadata 格式枚举
V4L2 设备匹配
Buffer 格式转换
Pipeline stream 分发
Camera Framework 输出
Camera App profile 选择
前后摄 UI 切换
相册保存逻辑
这也是我做完之后最大的感受:在 OpenHarmony 这种系统里,“一个外设能用”并不是某一个文件改对了就结束了,而是上层应用、系统服务、HDI、驱动和板级配置一起对齐。
目前仍然有一个遗留点:底层标准 still JPEG stream 还没有完全做到直接触发 ImageReceiver imageArrival,所以当前 Camera App 中保留了截图保存兜底。不过这个兜底是源码级修改,不是设备上的临时操作;重新编译烧录后依然可以使用。
后续如果继续优化,我认为可以从两个方向入手:
- 继续完善 VDI still capture,让 JPEG/BLOB stream 真正回到 ImageReceiver。
- 再检查视频录制、闪光灯、对焦、变焦等能力,让 Camera 不只是“能拍”,而是更接近完整手机相机能力。
做到这里,摄像头终于从“能看到一点东西”变成了“像一个相机应用了”。
虽然中间有很多地方看起来像在黑暗里摸索,但是摸着摸着,确实摸到了开关。
更多推荐

所有评论(0)