下面开始真正的适配阶段,工作目录很好在源码中发现,毕竟 \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

code
对应修改位置:

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 中可以看到:
code
为了让 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 创建失败。
因此在代码中补充了映射:

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

drivers/peripheral/camera/vdi_base/common/adapter/platform/v4l2/src/driver_adapter/include/v4l2_utils.h
这里 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"

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

核心思路有三个:

  1. 初始化时用真实 camera count 更新 CameraPlatformCapability
  2. FootBar 判断是否显示切换按钮时,不只依赖原本状态,也参考平台识别到的摄像头数量。
  3. 把切换按钮的点击区域放到外层 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 里,用来缓存最新的一帧真实摄像头画面。

最终采用的方式是:

  1. 应用层仍然优先走标准 PhotoOutput.capture()
  2. 在 MUSEPaper2 上额外创建隐藏预览帧输出。
  3. ImageReceiver 收到预览帧后缓存最新一帧。
  4. 按下快门时,如果 still JPEG 没有送到 app,就把缓存的真实预览帧转成 PixelMap
  5. PixelMap 打包成 JPEG。
  6. 通过媒体库创建图片资产,写入相册。

关键代码位置:

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 输出。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐