写点什么

【HarmonyOS】应用调用相机功能(扫码,自定义相机,人脸活体检测等)显示黑屏

作者:GeorgeGcs
  • 2025-08-19
    上海
  • 本文字数:9018 字

    阅读完需:约 30 分钟

【HarmonyOS】应用调用相机功能(扫码,自定义相机,人脸活体检测等)显示黑屏

@TOC

前言

鸿蒙应用开发中,当实现需要用到相机的功能时,例如扫一扫,自定义相机、人脸活体检测等,有可能遇到唤起的相机黑屏。


该问题主要是因为相机未释放导致。通常出现在前一个页面中使用了相机后,进入后一个页面中也在使用相机的情况下。

问题拆解

当我们开发定制扫一扫或者自定义相机时,需要操作 camera 对象,系统提供了单例对象来管控相机的生命周期。此时就很容易出现上述的释放问题。


按照开发习惯,我们经常会将资源释放逻辑,放在页面销毁函数中进行操作。但是该处理,在页面跳转时,会造成相机资源未释放,进入下个页面,也需要使用相机时,导致相机的启动操作会报错,XComponent 黑屏。



为了便于理解,以我们之前开发遇到的真实业务场景来举例,当扫码页面启动,相机资源正常按照步骤进行初始化后,此时扫码进入特殊场景,会跳转到人脸活体检测页面,导致进入页面后,人脸活体检测相机视频流是黑色。


该问题就是因为扫码页面没有释放相机资源导致。

解决方案

综上所述,我们在进行相机相关操作时,要注意在页面不显示时 onPageHide,也处理相机的资源释放。此方案处理,可规避进入下个页面也使用相机操作



async onPageHide() { // 页面消失或隐藏时,停止并释放相机流 this.userGrant = false; this.isFlashLightEnable = false; this.isSensorLight = false; try { customScan.off('lightingFlash'); } catch (error) { hilog.error(0x0001, this.TAG, `Failed to off lightingFlash. Code: ${error.code}, message: ${error.message}`); } await customScan.stop(); // 自定义相机流释放接口 customScan.release().then(() => { hilog.info(0x0001, this.TAG, 'Succeeded in releasing customScan by promise.'); }).catch((error: BusinessError) => { hilog.error(0x0001, this.TAG, `Failed to release customScan by promise. Code: ${error.code}, message: ${error.message}`); }) }
复制代码

源码示例



import { customScan, scanBarcode, scanCore, detectBarcode } from '@kit.ScanKit'import { hilog } from '@kit.PerformanceAnalysisKit'import { BusinessError } from '@kit.BasicServicesKit'import { abilityAccessCtrl, common } from '@kit.AbilityKit'import { display, promptAction } from '@kit.ArkUI'import { photoAccessHelper } from '@kit.MediaLibraryKit';
@Builderexport function ScanPageBuilder(name: string, param: object){ if(isLog(name, param)){ ScanPage() }}
function isLog(name: string, param: object){ console.log("ScanPageBuilder", " ScanPageBuilder init name: " + name); return true;}
@Entry@Componentexport struct ScanPage { private TAG: string = '[customScanPage]';
@State userGrant: boolean = false // 是否已申请相机权限 @State surfaceId: string = '' // xComponent组件生成id @State isShowBack: boolean = false // 是否已经返回扫码结果 @State isFlashLightEnable: boolean = false // 是否开启了闪光灯 @State isSensorLight: boolean = false // 记录当前环境亮暗状态 @State cameraHeight: number = 480 // 设置预览流高度,默认单位:vp @State cameraWidth: number = 300 // 设置预览流宽度,默认单位:vp @State cameraOffsetX: number = 0 // 设置预览流x轴方向偏移量,默认单位:vp @State cameraOffsetY: number = 0 // 设置预览流y轴方向偏移量,默认单位:vp @State zoomValue: number = 1 // 预览流缩放比例 @State setZoomValue: number = 1 // 已设置的预览流缩放比例 @State scaleValue: number = 1 // 屏幕缩放比 @State pinchValue: number = 1 // 双指缩放比例 @State displayHeight: number = 0 // 屏幕高度,单位vp @State displayWidth: number = 0 // 屏幕宽度,单位vp @State scanResult: Array<scanBarcode.ScanResult> = [] // 扫码结果 private mXComponentController: XComponentController = new XComponentController()
async onPageShow() { // 自定义启动第一步,用户申请权限 await this.requestCameraPermission(); // 自定义启动第二步:设置预览流布局尺寸 this.setDisplay(); // 自定义启动第三步,配置初始化接口 this.setScanConfig(); }
private setScanConfig(){ // 多码扫码识别,enableMultiMode: true 单码扫码识别enableMultiMode: false let options: scanBarcode.ScanOptions = { scanTypes: [scanCore.ScanType.ALL], enableMultiMode: true, enableAlbum: true } customScan.init(options); }
async onPageHide() { // 页面消失或隐藏时,停止并释放相机流 this.userGrant = false; this.isFlashLightEnable = false; this.isSensorLight = false; try { customScan.off('lightingFlash'); } catch (error) { hilog.error(0x0001, this.TAG, `Failed to off lightingFlash. Code: ${error.code}, message: ${error.message}`); } await customScan.stop(); // 自定义相机流释放接口 customScan.release().then(() => { hilog.info(0x0001, this.TAG, 'Succeeded in releasing customScan by promise.'); }).catch((error: BusinessError) => { hilog.error(0x0001, this.TAG, `Failed to release customScan by promise. Code: ${error.code}, message: ${error.message}`); }) }
/** * 去相册选择图片 */ onClickSelectPhoto = ()=>{ try { let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions(); // 设置筛选过滤条件 PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 选择用户选择数量 PhotoSelectOptions.maxSelectNumber = 1; // 实例化图片选择器 let photoPicker = new photoAccessHelper.PhotoViewPicker(); // 唤起安全相册组件 photoPicker.select(PhotoSelectOptions, (err: BusinessError, PhotoSelectResult: photoAccessHelper.PhotoSelectResult) => { if (err) { console.error(this.TAG, "onClickSelectPhoto photoPicker.select error:" + JSON.stringify(err)); return; } // 用户选择确认后,会回调到这里。 console.info(this.TAG, "onClickSelectPhoto photoPicker.select successfully:" + JSON.stringify(PhotoSelectResult)); // 因为设置的选择个数为1,所以直接取0位 this.detectPhoto(PhotoSelectResult.photoUris[0]); }); } catch (error) { let err: BusinessError = error as BusinessError; console.error(this.TAG, "onClickSelectPhoto photoPicker.select catch failed:" + JSON.stringify(err)); } }
/** * 解析图片码数据 */ private detectPhoto(uri: string){ if(uri){ let inputImg: detectBarcode.InputImage = { uri: uri, }; let setting: scanBarcode.ScanOptions = { scanTypes: [ scanCore.ScanType.ALL ], // 开启识别多码 enableMultiMode: true, enableAlbum: true, }; try { // 调用图片识码接口 detectBarcode.decode(inputImg, setting).then((result: Array<scanBarcode.ScanResult>) => { console.info(this.TAG, " decode res: " + JSON.stringify(result)) // 长度大于0说明有解析结果 if(result.length > 0){ // 长达大于1 多码 // 解析码值结果跳转应用服务页 this.scanResult = result; this.isShowBack = true;
if(result.length > 1){
}else{ this.showScanResult(result[0]); } }else{ promptAction.showToast({ message: "无二维码数据!" }); } }).catch((error: BusinessError) => { console.error(this.TAG, " decode error: " + JSON.stringify(error)) }); } catch (error) { console.error(this.TAG, " catch error: " + JSON.stringify(error)) } }else{ promptAction.showToast({ message: "图片数据异常!" + uri }); } }
/** * 用户申请权限 * @returns */ async reqPermissionsFromUser(): Promise<number[]> { hilog.info(0x0001, this.TAG, 'reqPermissionsFromUser start'); let context = getContext() as common.UIAbilityContext; let atManager = abilityAccessCtrl.createAtManager(); let grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']); return grantStatus.authResults; }
/** * 用户申请相机权限 */ async requestCameraPermission() { let grantStatus = await this.reqPermissionsFromUser(); for (let i = 0; i < grantStatus.length; i++) { if (grantStatus[i] === 0) { // 用户授权,可以继续访问目标操作 console.log(this.TAG, "Succeeded in getting permissions."); this.userGrant = true; } } }
// 竖屏时获取屏幕尺寸,设置预览流全屏示例 setDisplay() { // 折叠屏无 or 折叠 if(display.getFoldStatus() == display.FoldStatus.FOLD_STATUS_UNKNOWN || display.getFoldStatus() == display.FoldStatus.FOLD_STATUS_FOLDED){ // 默认竖屏 let displayClass = display.getDefaultDisplaySync(); this.displayHeight = px2vp(displayClass.height); this.displayWidth = px2vp(displayClass.width);
}else{ // 折叠屏展开 or 半展开 let displayClass = display.getDefaultDisplaySync(); let tempHeight = px2vp(displayClass.height); let tempWidth = px2vp(displayClass.width); console.info("debugDisplay", 'tempHeight: ' + tempHeight + " tempWidth: " + tempWidth); this.displayHeight = tempHeight + px2vp(8); this.displayWidth = ( tempWidth - px2vp(64) ) / 2;
} console.info("debugDisplay", 'final displayHeight: ' + this.displayHeight + " displayWidth: " + this.displayWidth);
let maxLen: number = Math.max(this.displayWidth, this.displayHeight); let minLen: number = Math.min(this.displayWidth, this.displayHeight); const RATIO: number = 16 / 9; this.cameraHeight = maxLen; this.cameraWidth = maxLen / RATIO; this.cameraOffsetX = (minLen - this.cameraWidth) / 2; }
// toast显示扫码结果 async showScanResult(result: scanBarcode.ScanResult) { // 使用toast显示出扫码结果 promptAction.showToast({ message: JSON.stringify(result), duration: 5000 }); }
/** * 启动相机 */ private startCamera() { this.isShowBack = false; this.scanResult = []; let viewControl: customScan.ViewControl = { width: this.cameraWidth, height: this.cameraHeight, surfaceId : this.surfaceId }; // 自定义启动第四步,请求扫码接口,通过Promise方式回调 try { customScan.start(viewControl) .then(async (result: Array<scanBarcode.ScanResult>) => { console.error(this.TAG, 'result: ' + JSON.stringify(result)); if (result.length) { // 解析码值结果跳转应用服务页 this.scanResult = result; this.isShowBack = true; // 获取到扫描结果后暂停相机流 try { customScan.stop().then(() => { console.info(this.TAG, 'Succeeded in stopping scan by promise '); }).catch((error: BusinessError) => { console.error(this.TAG, 'Failed to stop scan by promise err: ' + JSON.stringify(error)); }); } catch (error) { console.error(this.TAG, 'customScan.stop err: ' + JSON.stringify(error)); }
} }).catch((error: BusinessError) => { console.error(this.TAG, 'customScan.start err: ' + JSON.stringify(error)); }); } catch (err) { console.error(this.TAG, 'customScan.start err: ' + JSON.stringify(err)); } }
/** * 注册闪光灯监听接口 */ private setFlashLighting(){ customScan.on('lightingFlash', (error, isLightingFlash) => { if (error) { console.info(this.TAG, "customScan lightingFlash error: " + JSON.stringify(error)); return; } if (isLightingFlash) { this.isFlashLightEnable = true; } else { if (!customScan?.getFlashLightStatus()) { this.isFlashLightEnable = false; } } this.isSensorLight = isLightingFlash; }); }
// 自定义扫码界面的顶部返回按钮和扫码提示 @Builder TopTool() { Column() { Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { Text('返回') .onClick(async () => { // router.back(); this.mNavContext?.pathStack.removeByName("ScanPage"); }) }.padding({ left: 24, right: 24, top: 40 })
Column() { Text('扫描二维码/条形码') Text('对准二维码/条形码,即可自动扫描') }.margin({ left: 24, right: 24, top: 24 }) } .height(146) .width('100%') }
@Builder ScanKitView(){ XComponent({ id: 'componentId', type: XComponentType.SURFACE, controller: this.mXComponentController }) .onLoad(async () => {
// 获取XComponent组件的surfaceId this.surfaceId = this.mXComponentController.getXComponentSurfaceId(); console.info(this.TAG, "Succeeded in getting surfaceId: " + this.surfaceId);
this.startCamera(); this.setFlashLighting();
}) .width(this.cameraWidth) .height(this.cameraHeight) .position({ x: this.cameraOffsetX, y: this.cameraOffsetY }) }
@Builder ScanView(){ Stack() {
Column() { if (this.userGrant) { this.ScanKitView() } } .height('100%') .width('100%') .backgroundColor(Color.Red)
Column() { this.TopTool() Column() { } .layoutWeight(1) .width('100%')
Column() { Row() { // 闪光灯按钮,启动相机流后才能使用 Button('FlashLight') .onClick(() => { // 根据当前闪光灯状态,选择打开或关闭闪关灯 if (customScan.getFlashLightStatus()) { customScan.closeFlashLight(); setTimeout(() => { this.isFlashLightEnable = this.isSensorLight; }, 200); } else { customScan.openFlashLight(); } }) .visibility((this.userGrant && this.isFlashLightEnable) ? Visibility.Visible : Visibility.None)
// 扫码成功后,点击按钮后重新扫码 Button('ReScan') .onClick(() => { try { customScan.rescan(); } catch (error) { console.error(this.TAG, 'customScan.rescan err: ' + JSON.stringify(error)); }
// 点击按钮重启相机流,重新扫码 this.startCamera(); }) .visibility(this.isShowBack ? Visibility.Visible : Visibility.None)
// 选择相册图片解析二维码 Button('相册') .onClick(() => { this.onClickSelectPhoto(); }) }
Row() { // 预览流设置缩放比例 Button('缩放比例,当前比例:' + this.setZoomValue) .onClick(() => { // 设置相机缩放比例 if (!this.isShowBack) { if (!this.zoomValue || this.zoomValue === this.setZoomValue) { this.setZoomValue = customScan.getZoom(); } else { this.zoomValue = this.zoomValue; customScan.setZoom(this.zoomValue); setTimeout(() => { if (!this.isShowBack) { this.setZoomValue = customScan.getZoom(); } }, 1000); } } }) } .margin({ top: 10, bottom: 10 })
Row() { // 输入要设置的预览流缩放比例 TextInput({ placeholder: '输入缩放倍数' }) .type(InputType.Number) .borderWidth(1) .backgroundColor(Color.White) .onChange(value => { this.zoomValue = Number(value); }) } } .width('50%') .height(180) }
// 单码、多码扫描后,显示码图蓝点位置。点击toast码图信息 ForEach(this.scanResult, (item: scanBarcode.ScanResult, index: number) => { if (item.scanCodeRect) { Image($r("app.media.icon_select_dian")) .width(20) .height(20) .markAnchor({ x: 20, y: 20 }) .position({ x: (item.scanCodeRect.left + item?.scanCodeRect?.right) / 2 + this.cameraOffsetX, y: (item.scanCodeRect.top + item?.scanCodeRect?.bottom) / 2 + this.cameraOffsetY }) .onClick(() => { this.showScanResult(item); }) } }) } // 建议相机流设置为全屏 .width('100%') .height('100%') .onClick((event: ClickEvent) => { // 是否已扫描到结果 if (this.isShowBack) { return; } // 点击屏幕位置,获取点击位置(x,y),设置相机焦点 let x1 = vp2px(event.displayY) / (this.displayHeight + 0.0); let y1 = 1.0 - (vp2px(event.displayX) / (this.displayWidth + 0.0)); customScan.setFocusPoint({ x: x1, y: y1 }); hilog.info(0x0001, this.TAG, `Succeeded in setting focusPoint x1: ${x1}, y1: ${y1}`); // 设置连续自动对焦模式 setTimeout(() => { customScan.resetFocus(); }, 200); }).gesture(PinchGesture({ fingers: 2 }) .onActionStart((event: GestureEvent) => { hilog.info(0x0001, this.TAG, 'Pinch start'); }) .onActionUpdate((event: GestureEvent) => { if (event) { this.scaleValue = event.scale; } }) .onActionEnd((event: GestureEvent) => { // 是否已扫描到结果 if (this.isShowBack) { return; } // 获取双指缩放比例,设置变焦比 try { let zoom = customScan.getZoom(); this.pinchValue = this.scaleValue * zoom; customScan.setZoom(this.pinchValue); hilog.info(0x0001, this.TAG, 'Pinch end'); } catch (error) { hilog.error(0x0001, this.TAG, `Failed to setZoom. Code: ${error.code}, message: ${error.message}`); } })) }
private mNavContext: NavDestinationContext | null = null;
build() { NavDestination(){ this.ScanView() } .width("100%") .height("100%") .hideTitleBar(true) .onReady((navContext: NavDestinationContext)=>{ this.mNavContext = navContext; }) .onShown(()=>{ this.onPageShow(); }) .onHidden(()=>{ this.onPageHide(); }) }}
复制代码


发布于: 刚刚阅读数: 3
用户头像

GeorgeGcs

关注

路漫漫其修远兮,吾将上下而求索。 2024-12-24 加入

鸿蒙创作先锋,华为HDE专家,鸿蒙讲师,作者。 目前任职鸿蒙应用架构师。历经腾讯,宝马,研究所,金融。 待过私企,外企,央企。 深耕大应用开发领域十年。 OpenHarmony,HarmonyOS,Flutter,H5,Android,IOS。

评论

发布
暂无评论
【HarmonyOS】应用调用相机功能(扫码,自定义相机,人脸活体检测等)显示黑屏_GeorgeGcs_InfoQ写作社区