HarmonyOS趣味相机实战第19篇:CameraKit输出Profile协商、宽高比评分与会话提交
HarmonyOS趣味相机实战第19篇CameraKit输出Profile协商、宽高比评分与会话提交摘要CameraKit 返回了多个previewProfiles和photoProfiles时直接取第一个元素虽然代码最短却把画面比例、性能和设备排序规则交给了底层实现。不同设备上的第一个 Profile 可能宽高比不同、分辨率过高甚至与页面316 x 390的竖屏 Cover 预览不匹配最终造成裁剪过多、识别坐标难对齐或会话配置失败。本文基于D:/APP/1quweixiangji趣味相机工程复盘CameraPreviewService.startPreview()的输出能力协商。我们把“取第一个”升级为可测试的候选过滤、宽高比评分、分辨率预算、预览与拍照组合校验并保留无 Profile 重载时的兼容降级。最后通过 PhotoSession 的 beginConfig/commitConfig 建立原子会话避免半配置状态。工程背景与源码定位文件当前责任本文关注点entry/src/main/ets/service/CameraPreviewService.ets创建 CameraInput、PreviewOutput、PhotoOutput 与 PhotoSessionProfile 选择和配置顺序entry/src/main/ets/service/CameraDeviceService.ets发现前后镜头并选择设备每个镜头独立协商能力entry/src/main/ets/pages/Index.ets316 x 390预览和三档照片质量目标比例与业务偏好entry/src/main/ets/service/CoreVisionHumanService.ets将图像坐标映射到预览Profile 尺寸和旋转输入entry/src/main/module.json5CAMERA 权限和手机设备声明运行边界build-profile.json5target/compatible SDK编译和 API 基线环境与能力边界项目当前值说明应用版本1.0.4AppScope/app.json5target SDK6.0.2(22)当前工程配置场景模式NORMAL_PHOTO普通拍照会话预览逻辑尺寸316 x 390ArkUI 工作台页面照片档位high/medium/low映射QualityLevel不是 Profile 尺寸当前设备类型phone模块配置当前选择策略数组第一个 Profile需要增强为确定性评分一、当前实现为什么能用项目获取设备与能力constmanager:camera.CameraManagercamera.getCameraManager(context);constcameraPosition:camera.CameraPositionpositionfront?camera.CameraPosition.CAMERA_POSITION_FRONT:camera.CameraPosition.CAMERA_POSITION_BACK;constcameraDevice:camera.CameraDevicemanager.getCameraDevice(cameraPosition,camera.CameraType.CAMERA_TYPE_DEFAULT);constcapability:camera.CameraOutputCapabilitymanager.getSupportedOutputCapability(cameraDevice,camera.SceneMode.NORMAL_PHOTO);随后选择第一个constprofile:camera.Profile|nullcapability.previewProfiles.length0?capability.previewProfiles[0]:null;constphotoProfile:camera.Profile|nullcapability.photoProfiles.length0?capability.photoProfiles[0]:null;并兼容不传 Profile 的重载constoutput:camera.PreviewOutputprofile?manager.createPreviewOutput(profile,surfaceId):manager.createPreviewOutput(surfaceId);constphotoOutput:camera.PhotoOutputphotoProfile?manager.createPhotoOutput(photoProfile):manager.createPhotoOutput();这段代码具备两个优点流程完整空数组时仍有降级路径。但“第一个”没有表达产品偏好无法保证跨设备一致。二、Profile选择需要回答四个问题对每个候选至少评估宽高比与预览容器是否接近。像素数量是否落在设备性能预算内。前置或后置镜头是否需要不同上限。预览 Profile 与拍照 Profile 是否能组成当前场景的有效会话。预览追求稳定帧率和低延迟拍照追求细节。两者不能用同一个“分辨率越高越好”规则。三、不要混淆Profile与照片质量档位页面提供privateresolutionLabels:string[][12MP (4:3),8MP (4:3),5MP (4:3)];privateresolutionQualities:CameraPhotoQualityPreference[][high,medium,low];拍照时映射constsetting:camera.PhotoCaptureSetting{quality:captureQualityLevel(qualityPreference),mirror:activeCameraPositionfront};QualityLevel是编码质量偏好不等于选择 12MP、8MP、5MP 的 Photo Profile。若 UI 明确展示像素级分辨率就必须让选项与实际photoProfile.size对应否则应把文案改为“高清/标准/省空间”。四、先把候选转换成可评分数据interfaceProfileCandidate{profile:camera.Profile;width:number;height:number;longEdge:number;shortEdge:number;pixels:number;aspect:number;}functiontoCandidate(profile:camera.Profile):ProfileCandidate{constwidthprofile.size.width;constheightprofile.size.height;return{profile,width,height,longEdge:Math.max(width,height),shortEdge:Math.min(width,height),pixels:width*height,aspect:Math.max(width,height)/Math.min(width,height)};}使用长短边计算比例可以避免候选以横向尺寸表示而页面实际竖屏显示时误判。五、目标比例来自产品容器页面容器为316 x 390consttargetAspect390/316;这个比例约为 1.234不是常见 4:3 的 1.333。相机图像通过 Cover 填充后一定会裁剪一部分。选择时既要尊重实际容器也要考虑常见相机输出比例。可把产品目标设为 4:3并让 UI 容器承担少量裁剪也可以严格按 390:316 评分。关键是产品、识别映射和测试使用同一规则。六、宽高比评分用相对误差functionaspectPenalty(candidateAspect:number,targetAspect:number):number{returnMath.abs(candidateAspect-targetAspect)/targetAspect;}相对误差比绝对差更容易跨比例比较。误差越小越好。可计算 Cover 裁剪比例functioncoverCropRatio(sourceAspect:number,targetAspect:number):number{if(sourceAspecttargetAspect){return1-targetAspect/sourceAspect;}return1-sourceAspect/targetAspect;}裁剪比例直接表达丢失的边缘区域更贴近用户感知和坐标对齐风险。七、预览分辨率要设置性能预算预览 Profile 过高会增加Surface 带宽。createPixelMapFromSurface()成本。CoreVision 采样分析成本。图像旋转和坐标映射压力。部分设备的会话启动时间。可以设置软目标与硬上限interfacePreviewBudget{preferredPixels:number;maxPixels:number;minShortEdge:number;}constphoneBudget:PreviewBudget{preferredPixels:1280*720,maxPixels:1920*1080,minShortEdge:640};这些数值是策略示例必须用项目目标设备测试确定不应写死为所有 HarmonyOS 设备的通用结论。八、组合一个确定性评分函数functionpreviewScore(candidate:ProfileCandidate,targetAspect:number,budget:PreviewBudget):number{if(candidate.shortEdgebudget.minShortEdge){returnNumber.POSITIVE_INFINITY;}if(candidate.pixelsbudget.maxPixels){returnNumber.POSITIVE_INFINITY;}constcropcoverCropRatio(candidate.aspect,targetAspect);constpixelDistanceMath.abs(candidate.pixels-budget.preferredPixels)/budget.preferredPixels;returncrop*1000pixelDistance*100;}权重明确后候选顺序变化也不会改变结果。分数相同时再按像素、宽度、格式等稳定字段排序。九、过滤失败时要有二级降级如果所有候选都超过硬上限不能直接返回 null。降级顺序满足比例和预算的候选 - 忽略 preferredPixels只保留 maxPixels - 选择像素最小的有效候选 - 使用 createPreviewOutput(surfaceId) 重载 - 返回明确启动错误实现functionchoosePreviewProfile(profiles:camera.Profile[],targetAspect:number,budget:PreviewBudget):camera.Profile|null{if(profiles.length0){returnnull;}constcandidatesprofiles.map(toCandidate);constrankedcandidates.map(candidate({candidate,score:previewScore(candidate,targetAspect,budget)})).filter(itemNumber.isFinite(item.score)).sort((a,b)a.score-b.score||a.candidate.pixels-b.candidate.pixels);if(ranked.length0){returnranked[0].candidate.profile;}candidates.sort((a,b)a.pixels-b.pixels);returncandidates[0].profile;}十、拍照Profile使用不同目标拍照输出可以更高但仍不应盲目选择最大值。需要考虑用户选择的业务档位。可接受的 JPEG 大小。ImageSource 与 PixelMap 解码内存。CoreVision 拍照后分析耗时。保存和转文档的处理时间。可以按设备候选动态生成三档high - 同比例候选中较高分辨率 medium - 同比例候选中位数 low - 满足最低清晰度的较低分辨率这样 UI 显示的是设备真实支持的尺寸而不是固定假设。十一、建立实际分辨率选项模型interfacePhotoResolutionOption{key:high|medium|low;label:string;profile:camera.Profile;megapixels:number;}functionprofileLabel(profile:camera.Profile):string{constpixelsprofile.size.width*profile.size.height;constmegapixelsMath.round(pixels/100000)/10;return${megapixels}MP (${profile.size.width}x${profile.size.height});}选项应在镜头切换后重新生成。前置镜头可能没有后置镜头的高分辨率档位页面不能继续展示旧选项。十二、预览与拍照Profile必须按同一设备重新获取切换镜头后当前流程会stopPreview - getCameraDevice(newPosition) - getSupportedOutputCapability(newDevice, NORMAL_PHOTO) - choose previewProfile - choose photoProfile - create outputs不要缓存后置镜头的 Profile 给前置镜头。Profile 的有效性属于设备、场景模式和输出类型组合。十三、PhotoSession配置保持原子顺序项目使用constsession:camera.PhotoSessionmanager.createSessioncamera.PhotoSession(camera.SceneMode.NORMAL_PHOTO);session.beginConfig();session.addInput(input);session.addOutput(output);session.addOutput(photoOutput);if(metadataOutput!null){session.addOutput(metadataOutput);}awaitsession.commitConfig();awaitsession.start();正确原则在beginConfig()与commitConfig()之间完成输入输出组合。commitConfig()成功后再start()。只有启动成功才把局部资源发布到静态字段。任一失败进入统一清理。十四、局部资源失败时的所有权风险当前资源在启动成功后才赋值CameraPreviewService.cameraInputinput;CameraPreviewService.previewOutputoutput;CameraPreviewService.photoOutputphotoOutput;CameraPreviewService.sessionsession;如果commitConfig()之前抛错stopPreview()看不到这些局部资源可能无法释放。可以使用启动上下文interfaceOpeningResources{input?:camera.CameraInput;preview?:camera.PreviewOutput;photo?:camera.PhotoOutput;metadata?:camera.MetadataOutput;session?:camera.PhotoSession;}在catch/finally中释放局部资源或创建后立即登记到可清理字段并用 generation 防止并发污染。十五、MetadataOutput是可选能力项目先检查constsupported:booleancapability.supportedMetadataObjectTypes.some(type{returntypecamera.MetadataObjectType.FACE_DETECTION;});不支持人脸 Metadata 时预览和拍照仍应启动只是识别链路降级到定时 Surface 采样。可选输出不能让基础会话失败。如果加入 MetadataOutput 后commitConfig()失败可以尝试一次不含 MetadataOutput 的组合而不是立即判定整个相机不可用。十六、记录选型结果而不是输出完整设备信息建议日志interfaceProfileDecisionLog{cameraPosition:front|back;previewWidth:number;previewHeight:number;photoWidth:number;photoHeight:number;previewCandidateCount:number;photoCandidateCount:number;usedFallback:boolean;}日志不需要包含用户图像、设备序列标识或照片内容。只记录能力数量、选中尺寸和降级原因即可。十七、纯函数测试构造 Profile 大小夹具constsizes[{width:640,height:480},{width:1280,height:720},{width:1440,height:1080},{width:1920,height:1080},{width:4000,height:3000}];断言候选数组顺序变化不影响最终选择。4:3 目标优先 4:3 候选。超过maxPixels的候选被过滤。全部超预算时选择最小可用候选。空数组返回 null 并进入重载降级。横向尺寸和竖向尺寸得到相同长短边比例。前后镜头分别生成自己的照片档位。十八、真机兼容矩阵场景操作验收单后摄设备启动预览正常选中 Profile前后摄设备连续切换 20 次每次重新协商且无黑屏低端设备开启实时识别预览帧率稳定高像素设备选择 high解码内存不过量不支持人脸 Metadata启动会话预览可用、识别降级候选顺序变化重启应用选型结果确定Surface 重建前后台切换新 Surface 使用新 Output十九、常见问题排查现象可能原因排查方式某些设备预览裁剪很多第一个 Profile 比例不匹配输出候选比例和评分识别耗时明显升高预览 Profile 像素过高设置预览像素预算UI 显示12MP但实际不是把 QualityLevel 当分辨率根据 Photo Profile 生成文案前置切换后档位错误复用后置候选每个设备重新获取 capability加 Metadata 后会话失败输出组合不兼容无 Metadata 重试一次commit失败后相机仍占用局部资源未登记引入 OpeningResources 清理同机重启选择结果变化依赖数组原始顺序使用确定性评分和稳定排序空 Profile 数组直接崩溃未检查长度使用重载或明确错误降级二十、上线前验收清单不再默认依赖 Profile 数组第一个元素。预览和拍照使用不同评分目标。宽高比按长短边和 Cover 裁剪计算。预览 Profile 有像素预算和最小清晰度。UI 分辨率标签来自真实 Photo Profile。QualityLevel 文案不会伪装成具体 MP。前后镜头切换后重新获取能力。候选排序具有确定性。空候选和全部超预算都有降级路径。MetadataOutput 失败不影响基础拍照会话。beginConfig/commitConfig/start 顺序正确。启动中途失败能释放所有局部资源。日志记录尺寸和降级不记录用户图像。多机型完成切镜头、前后台和识别压力测试。总结CameraKit 的输出能力协商不是“取一个能用的 Profile”而是把产品容器、设备性能、照片清晰度和会话兼容性统一到确定性策略中。预览优先稳定和低延迟拍照优先真实分辨率档位MetadataOutput 则作为可选增强能力。当 Profile 选择变成可测试的纯函数并在 PhotoSession 提交前完成组合验证趣味相机才能在候选顺序、镜头差异和设备能力变化下保持一致。页面显示的分辨率也会与真实输出对应不再把编码质量与像素尺寸混为一谈。