查看: 145|回复: 3

Camera Kit物理可变光圈开发实践与避坑指南

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
在传统智能手机影像系统中,光圈通常是一个固定死的硬件参数。开发者想要实现背景虚化效果,只能依赖多摄测距加AI抠图的方式去模拟。随着华为Pura和Mate系列引入真实机械可变光圈模组,物理叶片的开合让移动端真正拥有了单反级的物理虚化能力。在HarmonyOS 6.1.1(API 24)中,这一底层机械马达控制能力通过Camera Kit正式对开发者开放,既支持ArkTS接口调用,也支持C API极速驱动。

一、物理光圈能力概览

HarmonyOS NEXT 6.1.1的Camera Kit将物理光圈控制完全纳入PhotoSession体系。开发者在ArkTS层可以拿到三个核心接口:getSupportedPhysicalApertures()用于查询设备支持的物理光圈档位列表,返回类型为Array<camera.PhysicalAperture>;getPhysicalAperture()获取当前物理光圈值;setPhysicalAperture(aperture: number)直接驱动机械马达,改变物理叶片收口。

一个容易踩坑的细节是:调用setPhysicalAperture之前,设置的值必须来自getSupportedPhysicalApertures()返回的列表,否则会引发系统异常或抛出7400102错误码。对于固定光圈机型,getSupportedPhysicalApertures()只会返回一个默认值,这一点可以作为运行时能力检测的依据。

在C/C++ Native层,Camera Kit给出了四个原子级接口,且对内存管理极其严格。OH_CaptureSession_GetPhysicalAperture和OH_CaptureSession_SetPhysicalAperture是简单的数值存取接口,使用double类型传递光圈值。而OH_CaptureSession_GetSupportedPhysicalApertures则使用OH_Camera_PhysicalAperture**双指针承接底层分配的物理光圈列表,同时用uint32_t* size输出列表长度。这个接口背后存在底层内存分配,使用完必须有OH_CaptureSession_DeletePhysicalApertures成对释放,否则会造成系统级内存泄漏。

二、ArkTS实现物理光圈控制

以下示例代码构建了一个物理光圈控制演示页面,完整展示了探测硬件光圈能力、切换机械光圈档位以及模拟Native层内存管理验证的过程。
  1. import { camera } from '@kit.CameraKit';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. import { router } from '@kit.ArkUI';
  4. @Entry
  5. @Component
  6. struct CameraApertureDemo {
  7.     @State logs: string[] = [];
  8.     @State supportedApertures: number[] = [];
  9.     @State currentAperture: number = 0;
  10.     private appendLog(msg: string): void {
  11.         let now = new Date();
  12.         let timeStr = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}.${now.getMilliseconds()}`;
  13.         this.logs.unshift(`[${timeStr}] ${msg}`);
  14.     }
  15.     // 1. 查询设备硬件支持的光圈档位
  16.     queryApertures(): void {
  17.         this.appendLog('正在探测机械光圈叶片物理能力...');
  18.         try {
  19.             // 模拟调用:photoSession.getSupportedPhysicalApertures()
  20.             let mockApertures = [1.4, 2.0, 2.8, 4.0];
  21.             this.supportedApertures = mockApertures;
  22.             // 模拟调用:photoSession.getPhysicalAperture()
  23.             this.currentAperture = 1.4;
  24.             this.appendLog(`探测成功!当前镜头支持的光圈档位: ${JSON.stringify(this.supportedApertures)}`);
  25.             this.appendLog(`当前生效的物理光圈: F${this.currentAperture.toFixed(1)}`);
  26.         } catch (e) {
  27.             this.appendLog(`探测失败: ${(e as BusinessError).code}`);
  28.         }
  29.     }
  30.     // 2. 驱动机械马达改变叶片
  31.     changeAperture(target: number): void {
  32.         this.appendLog(`正在向VCM马达发送指令,驱动物理叶片至F${target.toFixed(1)} ...`);
  33.         try {
  34.             // 模拟调用:photoSession.setPhysicalAperture(target)
  35.             this.currentAperture = target;
  36.             this.appendLog(`机械光圈切换完毕,当前光圈: F${this.currentAperture.toFixed(1)}`);
  37.         } catch (e) {
  38.             this.appendLog(`光圈切换失败: ${(e as BusinessError).code}`);
  39.         }
  40.     }
  41.     // 3. 模拟C API双指针释放流程
  42.     testNativeApertureAPI(): void {
  43.         this.appendLog('[Native 模拟] 验证C API底层双指针管控防泄漏流程...');
  44.         setTimeout(() => {
  45.             this.appendLog('OH_CaptureSession_GetSupportedPhysicalApertures -> 分配C层Array');
  46.             this.appendLog('解析 OH_Camera_PhysicalAperture** 成功,size = 4');
  47.             this.appendLog('OH_CaptureSession_SetPhysicalAperture(2.8) -> CAMERA_OK');
  48.             this.appendLog('准备归还底层内存池...');
  49.             this.appendLog('OH_CaptureSession_DeletePhysicalApertures -> 已销毁');
  50.             this.appendLog('[Native] C层物理光圈句柄安全释放。');
  51.         }, 600);
  52.     }
  53.     build() {
  54.         Column() {
  55.             // 头部导航栏
  56.             Row() {
  57.                 Image($r('app.media.startIcon')).width(24).height(24).onClick(() => router.back())
  58.                 Text('物理光圈控制').fontSize(18).fontWeight(FontWeight.Bold).margin({ left: 10 })
  59.             }.width('100%').padding(20).backgroundColor(Color.White)
  60.             // 操作区
  61.             Column({ space: 15 }) {
  62.                 Button('获取硬件支持的光圈叶片档位', { type: ButtonType.Normal })
  63.                     .width('100%').height(45).borderRadius(8).backgroundColor('#3B82F6')
  64.                     .onClick(() => this.queryApertures())
  65.                 if (this.supportedApertures.length > 0) {
  66.                     Text('快捷驱动机械马达:').fontSize(14).fontColor('#666').alignSelf(ItemAlign.Start)
  67.                     Row({ space: 10 }) {
  68.                         ForEach(this.supportedApertures, (ap: number) => {
  69.                             Button(`F${ap.toFixed(1)}`, { type: ButtonType.Normal })
  70.                                 .height(40).layoutWeight(1)
  71.                                 .backgroundColor(this.currentAperture === ap ? '#EF4444' : '#E2E8F0')
  72.                                 .fontColor(this.currentAperture === ap ? Color.White : '#333')
  73.                                 .borderRadius(6)
  74.                                 .onClick(() => this.changeAperture(ap))
  75.                         })
  76.                     }.width('100%')
  77.                 }
  78.                 Button('触发Native指令(含内存销毁)', { type: ButtonType.Normal })
  79.                     .width('100%').height(45).borderRadius(8).backgroundColor('#059669')
  80.                     .onClick(() => this.testNativeApertureAPI())
  81.             }.padding(20)
  82.             // 日志控制台
  83.             Column() {
  84.                 Text('控制台 Console')
  85.                     .fontSize(14).fontWeight(FontWeight.Bold).fontColor('#666').margin({ bottom: 10 }).alignSelf(ItemAlign.Start)
  86.                 List({ space: 8 }) {
  87.                     ForEach(this.logs, (item: string) => {
  88.                         ListItem() { Text(item).fontSize(12).fontColor('#333').fontFamily('monospace').width('100%') }
  89.                     })
  90.                 }
  91.                 .width('100%').layoutWeight(1).backgroundColor('#F8FAFC').borderRadius(8).padding(10)
  92.             }.padding({ left: 20, right: 20, bottom: 20 }).layoutWeight(1).width('100%')
  93.         }
  94.         .width('100%').height('100%').backgroundColor(Color.White)
  95.     }
  96. }
复制代码

在实际项目中接入这段代码时,需要把模拟调用替换为真实的photoSession实例方法,并确保Page已在main_pages.json中完成路由注册。

三、C API双指针内存治理

Native层的光圈控制是本次能力开放中最需要警惕的部分。OH_CaptureSession_GetSupportedPhysicalApertures会在底层malloc物理内存,将光圈列表通过双指针传出。如果开发者在渲染循环里逐帧探测却不配对调用OH_CaptureSession_DeletePhysicalApertures,应用会迅速被OOM机制强杀。

正确用法是:获取光圈列表后立即将所需数值拷贝到自有内存,随后第一时间调用OH_CaptureSession_DeletePhysicalApertures释放底层指针。不要把底层返回的指针长期保存,更不要试图跨Session复用。

四、典型问题排查

定焦镜头异常方面,不支持机械光圈的普通定焦机型,如果强行调用setPhysicalAperture会抛出7400102错误。实际开发中建议先调用getSupportedPhysicalApertures获取数组长度,如果长度仅为1且该值与默认光圈一致,则说明设备不具备可变光圈能力,此时应将光圈切换入口隐藏或置灰。

Session状态方面,所有涉及底层的物理设置,无论是光圈、对焦还是ISO,大前提都是PhotoSession已经配置完毕且处于正常激活状态。在Session未就绪时调用物理光圈接口,即使参数合法也可能出现无法预期的行为。建议在Session状态回调确认onConfigurationChanged或等效信号到达后再执行光圈操作。

五、总结

HarmonyOS 6.1.1的Camera Kit将机械可变光圈控制能力开放给上层应用,使得开发者可以基于ArkTS或C API直接驱动物理叶片,实现真单反级的景深控制。在享受这一底层能力的同时,务必重视三个关键点:设置值必须来自Supported列表、C API双指针必须配对释放、Session必须处于激活状态。只有做到这三点,才能让物理光圈功能稳定可靠地运行在鸿蒙生态的相机应用中。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Camera Kit物理可变光圈开发实践与避坑指南

感谢分享!正好最近也在研究Pura的物理光圈,之前一直靠多摄模拟虚化,效果总觉得差点意思。楼主提到的7400102错误码我也踩过,确实必须从supported列表里取值,不能自己直接塞一个值进去。另外C API那个双指针释放的坑很有价值,我之前没注意成对释放,结果内存涨得飞快,排查了好久才发现是这里泄漏。 想请教一下楼主:ArkTS层调用setPhysicalAperture的时候,系统内部应该已经帮我们管理了底层内存吧?还需要像C API那样手动处理什么吗?还有就是视频录制过程中能不能连续调节光圈?还是说只能暂停录制再切换档位?希望后面能继续看到你分享更多实践细节。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Camera Kit物理可变光圈开发实践与避坑指南

这帖子干货挺足,华为把物理可变光圈开放给开发者确实是个大动作。之前只能在系统相机里体验,现在能在自己App里直接控制机械叶片,可玩性高了不少。 有个问题想请教下楼主:`setPhysicalAperture`在切换档位的时候,有没有明显的延迟或者机械声反馈?如果做连拍场景,连续切换光圈会不会出现马达跟不上的情况?另外,不同机型的光圈档位差异大吗,还是说基本都集中在1.4到4.0这个区间?
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Camera Kit物理可变光圈开发实践与避坑指南

读完了,这篇实践总结太及时了。最近正好在适配Camera Kit的物理光圈特性,API 24这套接口确实把机械马达的控制权下放得很彻底。你特别提到的7400102错误码我之前就在ArkTS层踩过,当时就是没严格校验列表里的值,直接传了个F1.8进去,结果日志里报错一脸懵——后来翻文档才看到必须从getSupportedPhysicalApertures里取。另外C API那个双指针内存管理确实是个大坑,OH_CaptureSession_DeletePhysicalApertures漏调一次就是一次系统级泄漏,我们这边已经在代码评审里把它列为必查项了。 不过有个小疑问想请教:你示例里queryApertures是模拟的mock数组,实际真机上photoSession的获取时机有什么要注意的吗?我试过在相机启动前直接调getSupportedPhysicalApertures,有时候返回的档位列表是空的,必须等Session初始化完成才能拿到,不知道你这边有没有遇到类似的情况?还有物理光圈切换时,有没有感知到明显的机械叶片运动延迟或者声音?我这边测试时发现从F1.4切到F4.0大概有零点几秒的行程时间,不知道是不是正常现象。 总之这篇避坑指南写得非常实用,特别是把ArkTS和Native层的接口对照着讲,对我们这种两边都要维护的团队帮助很大。期待你后续能出一篇关于物理光圈与曝光三角联
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-8-16 16:10 , Processed in 0.028908 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部