04_摄像头适配(二)——主要工作
下面开始真正的适配阶段,工作目录很好在源码中发现,毕竟
\driver\目录里面非常清晰。
总览
找到 camera 的目录下,.md 文件交代的非常清楚关于目录结构的内容:
/drivers/peripheral/camera
├── hal # camera模块的hal层代码
│ ├── adapter # camera hal平台适配层的实现
│ ├── buffer_manager # camera hal统一的Buffer管理
│ ├── device_manager # 提供camera hal层设备管理能力,包括设备枚举、设备能力查询等
│ ├── hdi_impl # camera hal HDI的具体实现
│ ├── include # camera hal层内部的头文件
│ ├── init # camera hal层HDI接口使用样例实现
│ ├── pipeline_core # camera hal层pipeline核心代码
│ ├── test # camera hal层测试代码实现
│ └── utils # camera hal层工具类代码,目前提供的是watchdog
├── hal_c # 提供C实现的HAL接口
│ ├── hdi_cif # C实现的HDI接口适配代码
│ └── include # C形式的HDI接口
└── interfaces # camera hal对上层服务提供的驱动能力接口
├── hdi_ipc # IPC模式的HDI实现
├── hdi_passthrough # 直通模式的HDI实现
└── include # camera hal对外提供的HDI定义
第一阶段:先让系统真正识别两个摄像头
刚开始打开 Camera App,一片漆黑,我就知道这里偷不了懒了,而且后来发现,最大的问题不是拍照,而是它甚至没有完整地把两个摄像头作为前后摄来认识。于是第一步就是确认底层到底暴露了几个 cameraId。
再 OH 系统中可以使用鸿蒙的指令工具 hidumper,这个是用于统一系统信息导出的命令行工具,支持分析CPU、内存、存储等系统资源使用情况,查询系统服务运行情况,定位资源使用异常、通信等相关问题。
使用:
hidumper -s CameraService
看下输出:
# hidumper -s CameraService
-------------------------------[ability]-------------------------------
----------------------------------CameraService----------------------------------
--------Dump Summary Begin-------
# Number of Cameras:[2]
# Number of Active Cameras:[0]
# Current session summary:
Number of Camera sessions:[0]
--------Dump CameraDevice Begin-------
# Camera ID:[lcam001]:
## Camera Position:[Back]
## Camera Type:[Wide-Angle]
## Camera Connection Type:[Builtin]
## Camera Available stream configuration List:
### Basic Stream Info Size: 10
Format:[YCBCR_420_SP] Size:[Width:640 Height:480]
Format:[JPEG] Size:[Width:640 Height:480]
Format:[YCBCR_420_SP] Size:[Width:1280 Height:960]
Format:[JPEG] Size:[Width:1280 Height:960]
Format:[YCBCR_420_SP] Size:[Width:1280 Height:720]
Format:[JPEG] Size:[Width:1280 Height:720]
Format:[YCBCR_420_SP] Size:[Width:720 Height:720]
Format:[JPEG] Size:[Width:720 Height:720]
Format:[YCBCR_420_SP] Size:[Width:1920 Height:1080]
Format:[JPEG] Size:[Width:1920 Height:1080]
## Zoom Related Info:
OHOS_ABILITY_ZOOM_RATIO_RANGE data size:2
Available Zoom Ratio Range:[1.0000001.000000]
## Flash Related Info:
Available Flash Modes:[ Close ]
## Compensation Related Info:
Available Compensation Modes:[ 00]
## Color Space Related Info:
## AF Related Info:
Available Focus Modes:[ Manual Continuous-Auto Auto Locked ]
## AE Related Info:
Available Exposure Modes:[ Manual Continuous-Auto Locked Auto ]
## Sensor Related Info:
## Video Stabilization Related Info:
Available Video Stabilization Modes:[ Off ]:
## Video FrameRateRange Related Info:
Available FrameRateRange:
[ 15, 30 ]
## Camera Prelaunch Related Info:
## Camera Thumbnail Related Info:
# Camera ID:[lcam002]:
## Camera Position:[Front]
## Camera Type:[Wide-Angle]
## Camera Connection Type:[Builtin]
## Camera Available stream configuration List:
### Basic Stream Info Size: 10
Format:[YCBCR_420_SP] Size:[Width:640 Height:480]
Format:[JPEG] Size:[Width:640 Height:480]
Format:[YCBCR_420_SP] Size:[Width:1280 Height:960]
Format:[JPEG] Size:[Width:1280 Height:960]
Format:[YCBCR_420_SP] Size:[Width:1280 Height:720]
Format:[JPEG] Size:[Width:1280 Height:720]
Format:[YCBCR_420_SP] Size:[Width:720 Height:720]
Format:[JPEG] Size:[Width:720 Height:720]
Format:[YCBCR_420_SP] Size:[Width:1920 Height:1080]
Format:[JPEG] Size:[Width:1920 Height:1080]
## Zoom Related Info:
OHOS_ABILITY_ZOOM_RATIO_RANGE data size:2
Available Zoom Ratio Range:[1.0000001.000000]
## Flash Related Info:
Available Flash Modes:[ Close ]
## Compensation Related Info:
Available Compensation Modes:[ 00]
## Color Space Related Info:
## AF Related Info:
Available Focus Modes:[ Manual Continuous-Auto Auto Locked ]
## AE Related Info:
Available Exposure Modes:[ Manual Continuous-Auto Locked Auto ]
## Sensor Related Info:
## Video Stabilization Related Info:
Available Video Stabilization Modes:[ Off ]:
## Video FrameRateRange Related Info:
Available FrameRateRange:
[ 15, 30 ]
## Camera Prelaunch Related Info:
## Camera Thumbnail Related Info:
可以看到系统里面有两个逻辑摄像头:
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 的格式对上
改完发现还是漆黑一片,所以只能顺着结构层次看看还有什么问题,AI 说摄像头这部分特别容易出现一个现象:明明设备存在,日志也说打开了,但是就是没有图像。这个时候大概率要看格式。
MUSEPaper2 上对应的 V4L2 设备名称中有 Spacemit 的虚拟视频设备,例如:
spacemit vivi
spacemit vivi2
在文件 /device/board/spacemit/musepaper2/camera/vdi_impl/v4l2/device_manager/include/project_hardware.h 中可以看到:

为了让 Camera Host 能够正确找到它们,我在 V4L2 file format 逻辑中加入了 fallback 匹配:
spacemit vivi -> svivi-vid-cap-00
spacemit vivi2 -> svivi-vid-cap-01

接下来是格式映射。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 创建失败。
因此在代码中补充了映射:
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 预览”,那么应用层就算硬写也没有用。
因此这里需要在 /vendor/spacemit/musepaper2/hdf_config/uhdf/camera/hdi_impl/camera_host_config.hcs 修改板级 camera host 配置,为两个摄像头都补充可用配置,包括:
640x480
1280x960
1280x720
720x720
1920x1080
数据由 AI 提供。。
并且同时暴露:
YCBCR_420_SP 预览
JPEG 拍照
除此之外,在 /drivers/peripheral/camera/vdi_base/v4l2/include/camera_host/metadata_enum_map.h 中补上:

这一步之后再看:
hidumper -s CameraService
可以看到两个摄像头都已经有了比较完整的 stream info。这里我还记了一下,当时两个摄像头的 Basic Stream Info Size 都能看到 10,也就是预览和拍照的组合已经能被系统读出来了。
第四阶段:Camera App 的前后摄切换
底层有两个摄像头是一回事,应用上能不能切换是另一回事。
开始的时候,Camera App 里切换按钮并不能稳定显示,或者显示了也切不过去。这里的问题主要在应用层:MUSEPaper2 是 RISC-V 板子,但是我们希望它走手机 Camera UI 路径,而不是 default 设备路径。
因此在产品参数里加入:
const.product.devicetype="phone"

这样 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 这种东西,代码改一行,观感差很多。虽然它不如驱动听起来“硬核”,但是用户第一眼看到的就是它,不能装作没看见。但是这些东西又很浪费时间去一点点调整,所以这些工作是使用 AI 更多。
第六阶段:拍照保存到相册
这部分是最折磨人的地方。
预览能看,前后摄能切,按下快门也有反应,但是照片没有进入相册。也就是说,用户看到的画面是活的,但按下快门之后,系统像是沉默了。头疼。
先看应用层日志:
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 原来有一个特殊处理,会把请求尺寸修改:

这对于当前 MUSEPaper2 的 surface 请求路径并不合适,于是改成使用真实 photo size 申请 buffer。
原代码主要是直接复制上层传入值:
scg.encodeType = info.encodeType_;
但 MUSEPaper2 当前这条链路中,上层传下来的 info.encodeType_ 并不总是正确标记为 JPEG,有时仍是:
ENCODE_TYPE_NULL = 0
于是可能出现这样的不一致状态:
intent = STILL_CAPTURE
format = CAMERA_FORMAT_BLOB
encodeType = ENCODE_TYPE_NULL
这对人来说显然是一条 JPEG 拍照流,但 pipeline 看到的是:这是静态拍照,Buffer 也是 BLOB,可是又说不需要编码。
后续节点只能依赖 encodeType 判断怎样处理 Buffer,就可能:
- 不进入 JPEG 编码路径;
- 把 BLOB 当普通 Buffer 流转;
- 不写入正确的 JPEG 有效长度;
- 不触发对应的编码后处理;
最终无法向上层交付可解析的 JPEG。
所以需要在 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 路径还没有完全打通。
这里其实可以选择简单地在设备上手动推一个文件装作成功,但是因为那不是源码级适配,重新编译烧录后就没了。
最开始我尝试过用预览区域截图作为兜底,但是后面重新烧录测试时发现这个方案不够干净:有时候保存出来的图片会带上 Camera App 的按钮、切换图标等 UI 组件,甚至在某些时机拿到黑图。这个现象说明它截到的是屏幕合成结果,而不是真正的摄像头帧。
所以最后把方案改成了更接近摄像头链路本身的方式:在正常预览之外,再创建一个隐藏的 PreviewOutput,它不显示在页面上,只接到一个 ImageReceiver 里,用来缓存最新的一帧真实摄像头画面。
最终采用的方式是:
- 应用层仍然优先走标准
PhotoOutput.capture()。 - 在 MUSEPaper2 上额外创建隐藏预览帧输出。
ImageReceiver收到预览帧后缓存最新一帧。- 按下快门时,如果 still JPEG 没有送到 app,就把缓存的真实预览帧转成
PixelMap。 - 将
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 中加入隐藏预览帧缓存:

拍照逻辑变成:
if (!this.mPhotoOutPut) {
return await this.savePreviewFrameAsPhoto();
}
const imageSaved = this.mSaveCameraAsset.waitForNextImageSaved(CAPTURE_SAVE_TIMEOUT_MS);
await this.mPhotoOutPut.capture(this.mCaptureSetting);
if (!await imageSaved && !await this.savePreviewFrameAsPhoto()) {
return false;
}
这里还有一个细节:缓存到的预览帧可能本身就是 JPEG,也可能是 NV12/NV21 这类 YUV 数据,所以转换时做了两条路径:
JPEG frame -> image.createImageSource -> PixelMap
NV12/NV21 frame -> image.createPixelMap(...RGBA_8888)
如果有 stride 和 width 不一致的情况,还要重新拷贝每一行,不然保存出来的图像会错位。
在 SaveCameraAsset.ts 中增加:
savePixelMapAsImage(pixelMap, ...)
主要做的事情就是:
PixelMap
-> image.createImagePacker()
-> packToFile(..., image/jpeg)
-> createPhotoAsset
-> setPending(false)
-> onCaptureSuccess
这一步严格来说仍然不是最理想的终点,最理想的当然是底层 still JPEG 直接送到 ImageReceiver。
但是它已经适配,Camera App 重新编译安装后可以稳定工作;同时标准 still capture 路径也保留着,后续底层完全打通后可以自然切回真正的 JPEG 输出。
更多推荐



所有评论(0)