前言
这个就没啥好说的,有需求就要搞定需求,搞不定需求就搞定提出需求的人嘛
大致流程
相机开发需要使用真机,模拟器目前还是不支持的。这就劝退了一部分开发者。
所需要的调用的接口大部分集中在@kit.CameraKit
、@kit.AbilityKit
中。保存图片时需要用到@kit.ImageKit
、@kit.CoreFileKit
、@kit.MediaLibraryKit
等
接下来看下需要做哪些工作:
- 获取相机权限
- 获取可用相机列表
- 可以在这里监听相机状态(USB相机连接、断开连接、关闭、被占用等)
- 选择当前使用的相机
- 创建相机输入流并打开相机
- 可以创建相机输入流
- 可以监听预览输出流状态,包括预览流启动、预览流结束、预览流输出错误
- 可以获取相机支持的模式列表(NORMAL_PHOTO,NORMAL_VIDEO,SECURE_PHOTO)
- 可以获取当前相机设备支持的所有输出流,如预览流、拍照流、录像流等
- 会话(Session)管理
- 配置相机的输入流和输出流(分辨路等配置)
- 添加闪光灯、调整焦距等配置
- 会话切换控制:切换拍照或者录像
- 交和开启会话,可以开始调用相机相关功能
- 预览
- 创建Surface用于预览
- 将预览输出流通过SurfaceID与Surface关联
- 调用Session.start方法开始预览
- 拍照
- 创建拍照输出流
- 设置拍照photoAvailable的回调,并将拍照的buffer保存为图片。
- 参数配置(闪光灯、变焦、焦距等)
- 触发拍照
开发
权限处理
在进入拍照页面之前先申请权限,具体的流程看这里申请应用权限,本文不再赘述。
获取可用相机列表
首先要获取相机管理实例,这里为了代码看起来清晰,将各个步骤写到了单独的方法中。
另外多出使用camera.CameraManager
实例,因此定义为了全局变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @Entry @Component struct TakePhotoPage {
getCameraManager(context: common.BaseContext): camera.CameraManager { let cameraManager: camera.CameraManager = camera.getCameraManager(context); return cameraManager; }
getCameraDevices(cameraManager: camera.CameraManager): Array<camera.CameraDevice> { let cameraArray: Array<camera.CameraDevice> = cameraManager.getSupportedCameras(); if (cameraArray != undefined && cameraArray.length > 0) { return cameraArray; } else { hilog.error(0x01,TAG,"cameraManager.getSupportedCameras error"); return []; } }
onCameraStatusChange(cameraManager: camera.CameraManager): void { cameraManager.on('cameraStatus', (err: BusinessError, cameraStatusInfo: camera.CameraStatusInfo) => { if (err !== undefined && err.code !== 0) { hilog.error(0x01,TAG,`Callback Error, errorCode: ${err.code}`); return; } if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_APPEAR) { hilog.info(0x01,TAG,`New Camera device appear.`); } if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_DISAPPEAR) { hilog.info(0x01,TAG,`Camera device has been removed.`); } if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_AVAILABLE) { hilog.info(0x01,TAG,`Current Camera is available.`); } if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_UNAVAILABLE) { hilog.info(0x01,TAG,`Current Camera has been occupied.`); } hilog.info(0x01,TAG,`camera: ${cameraStatusInfo.camera.cameraId}`); hilog.info(0x01,TAG,`status: ${cameraStatusInfo.status}`); }); } }
|
这样我们可以获取到所有可用的相机列表,并且可以根据相机类型、连接类型等过滤掉不适用的相机。
在获取到相机列表后,我们默认使用返回列表的第一个相机。
创建相机输入流并打开相机
在这一步我们主要是创建相机的输入流,为后面在XComponent
中预览做准备。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| createInput(): camera.CameraInput | undefined {
let cameraInput: camera.CameraInput | undefined = undefined; try { cameraInput = this.cameraManager.createCameraInput(this.currentCamera); } catch (error) { let err = error as BusinessError; console.error('Failed to createCameraInput errorCode = ' + err.code); } if (cameraInput === undefined) { return undefined; }
cameraInput.on('error', this.currentCamera, (error: BusinessError) => { console.error(`Camera input error code: ${error.code}`); });
return cameraInput; }
|
最重要的就一行代码:调用cameraManager.createCameraInput(camera: CameraDevice)
创建一个输入流并返回,之后调用返回的输入流的open()
方法打开相机,注意该方法是异步的。
同样的,我们可以调用cameraManager.getSupportedSceneModes(camera: CameraDevice)
来获取相机支持的模式,一般情况下都会支持拍照和录像。
之后我们获取设备支持的输出流能力
1 2 3 4 5 6 7 8 9 10 11
| getSupportedOutputCapability(): camera.CameraOutputCapability | undefined {
let cameraOutputCapability: camera.CameraOutputCapability = this.cameraManager.getSupportedOutputCapability(this.currentCamera, camera.SceneMode.NORMAL_PHOTO)
if (!cameraOutputCapability) { console.error("cameraManager.getSupportedOutputCapability error"); return undefined; } return cameraOutputCapability; }
|
我们拿到cameraOutputCapability
之后,可以从该对象的previewProfiles
、photoProfiles
属性中获取到设备支持的分辨率大小。这里我们直接使用1920*1080
的分辨率。
需要注意的是 previewProfiles
和photoProfiles
所支持的分辨率不一定是一致的。预览的话只要宽高比一致,分辨率别差的太离谱就可以。
之后我们使用选择好的Profile
对象来创建拍照输出流和预览输出流
1 2 3 4 5 6 7 8 9 10
| try { this.photoOutput = this.cameraManager.createPhotoOutput(this.currentPhotoProfile); this.previewOutput = this.cameraManager.createPreviewOutput(previewProfile, this.surfaceId); } catch (error) { let err = error as BusinessError; console.error('Failed to createPhotoOutput errorCode = ' + err.code); } if (this.photoOutput === undefined) { return; }
|
这里需要注意的是,创建预览输出流的时候需要传入 surfaceID,该值来源于组件XComponent
1 2 3 4 5 6 7 8 9
| private mXComponentController: XComponentController = new XComponentController; XComponent({ id: 'componentId', type: XComponentType.SURFACE, controller: this.mXComponentController, }) .onLoad(async () => { this.surfaceId = this.mXComponentController.getXComponentSurfaceId(); })
|
所以这里我们需要注意一下创建预览输出流的时机
创建并配置会话
创建会话也只是一行的就可以搞定,但可能会有异常出现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| createSession() {
try { this.photoSession = this.cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession; } catch (error) { let err = error as BusinessError; console.error('Failed to create the session instance. errorCode = ' + err.code); } if (this.photoSession === undefined) { return; } this.photoSession.on('error', (error: BusinessError) => { console.error(`Capture session error code: ${error.code}`); }); }
|
配置会话主要是添加相机输入流、预览输出流和拍照输出流,最后提交配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| async configSession(cameraInput: camera.CameraInput, previewOutput: camera.PreviewOutput) { if (!this.photoSession) { return }
try { this.photoSession.beginConfig(); } catch (error) { let err = error as BusinessError; console.error('Failed to beginConfig. errorCode = ' + err.code); }
try { this.photoSession.addInput(cameraInput); } catch (error) { let err = error as BusinessError; console.error('Failed to addInput. errorCode = ' + err.code); }
try { this.photoSession.addOutput(previewOutput); } catch (error) { let err = error as BusinessError; console.error('Failed to addOutput(previewOutput). errorCode = ' + err.code); }
try { this.photoSession.addOutput(this.photoOutput); } catch (error) { let err = error as BusinessError; console.error('Failed to addOutput(photoOutput). errorCode = ' + err.code); }
await this.photoSession.commitConfig(); }
|
拍照回调和启动会话
我们先启动会话
1 2 3
| await this.photoSession.start().then(() => { console.info('Promise returned to indicate the session start success.'); });
|
会话启动之后我们就可以进行拍照了。拍照的话需要调用拍照输出流的capture
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| takePhoto() { if (!this.photoOutput) { return } let photoCaptureSetting: camera.PhotoCaptureSetting = { quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, rotation: camera.ImageRotation.ROTATION_0 }
this.photoOutput.capture(photoCaptureSetting, (err: BusinessError) => { if (err) { console.error(`Failed to capture the photo ${err.message}`); return; } console.info('Callback invoked to indicate the photo capture request success.'); }); }
|
但照片内容确不是在该方法中返回,而是需要我们在拍照输出流中添加photoAvailable
事件监听,该监听可以在创建拍照输出流之后就添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| setPhotoOutputCb(): void { if (!this.photoOutput) { return } this.photoOutput.on('photoAvailable', (errCode: BusinessError, photo: camera.Photo): void => { console.info('getPhoto start'); console.info(`err: ${JSON.stringify(errCode)}`); if (errCode || photo === undefined) { console.error('getPhoto failed'); return; } let imageObj = photo.main; imageObj.getComponent(image.ComponentType.JPEG, (errCode: BusinessError, component: image.Component): void => { console.info('getComponent start'); if (errCode || component === undefined) { console.error('getComponent failed'); return; } let buffer: ArrayBuffer; if (component.byteBuffer) { buffer = component.byteBuffer; let filePath = getContext().cacheDir + '/' + systemDateTime.getTime() + '.jpg' let file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE) fileIo.writeSync(file.fd, buffer) fileIo.closeSync(file)
let fileUrl = fileUri.getUriFromPath(filePath) promptAction.showToast({message:fileUrl})
let id: number = 0 promptAction.openCustomDialog({ builder: () => { this.saveImageToAlbumDialog(fileUrl, () => { promptAction.closeCustomDialog(id) }) } }).then((dialogID) => { id = dialogID })
} else { console.error('byteBuffer is null'); return; } imageObj.release(); }); }); }
|
这里就简单写了一下处理:拿到 ArrayBuffer 之后写入沙箱文件,然后在弹窗中展示
其他配置
我们创建会话(camera.PhotoSession)之后,可以通过该对象配置闪光灯模式、对焦模式、缩放等
闪光灯
首先判断设备是否支持闪光灯,然后再判断支持的闪光灯模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| getSupportFlashMode() { this.supportFlashMode = [] if (!this.photoSession) { return } let flashStatus: boolean = false; try { flashStatus = this.photoSession.hasFlash(); } catch (error) { let err = error as BusinessError; console.error('Failed to hasFlash. errorCode = ' + err.code); } console.info('Returned with the flash light support status:' + flashStatus);
if (flashStatus) { try { let status: boolean = this.photoSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_CLOSE); if (status) { this.supportFlashMode.push(camera.FlashMode.FLASH_MODE_CLOSE) } } catch (error) { let err = error as BusinessError; console.error('Failed to check whether the flash mode is supported. errorCode = ' + err.code); }
try { let status: boolean = this.photoSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_OPEN); if (status) { this.supportFlashMode.push(camera.FlashMode.FLASH_MODE_OPEN) } } catch (error) { let err = error as BusinessError; console.error('Failed to check whether the flash mode is supported. errorCode = ' + err.code); }
try { let status: boolean = this.photoSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_AUTO); if (status) { this.supportFlashMode.push(camera.FlashMode.FLASH_MODE_AUTO) } } catch (error) { let err = error as BusinessError; console.error('Failed to check whether the flash mode is supported. errorCode = ' + err.code); }
try { let status: boolean = this.photoSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_ALWAYS_OPEN); if (status) { this.supportFlashMode.push(camera.FlashMode.FLASH_MODE_ALWAYS_OPEN) } } catch (error) { let err = error as BusinessError; console.error('Failed to check whether the flash mode is supported. errorCode = ' + err.code); } } }
|
连续自动对焦
也是需要先判断是否支持自动连续对焦,不支持的话只能手动对焦
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| setAutoContinuousFocus() { if (!this.photoSession) { return } let focusModeStatus: boolean = false; try { let status: boolean = this.photoSession.isFocusModeSupported(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO); focusModeStatus = status; } catch (error) { let err = error as BusinessError; console.error('Failed to check whether the focus mode is supported. errorCode = ' + err.code); }
if (focusModeStatus) { try { this.photoSession.setFocusMode(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO); } catch (error) { let err = error as BusinessError; console.error('Failed to set the focus mode. errorCode = ' + err.code); } }
}
|
手动对焦则是获取到用户点击的位置,然后调用this.photoSession.setFocusPoint(point: camera.Point)
方法进行对焦
缩放
同样的,需要先获取到支持的缩放范围
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| getZoomRatioRange() { if (!this.photoSession) { return } let zoomRatioRange: Array<number> = []; try { zoomRatioRange = this.photoSession.getZoomRatioRange(); } catch (error) { let err = error as BusinessError; console.error('Failed to get the zoom ratio range. errorCode = ' + err.code); } if (zoomRatioRange.length <= 0) { return; } this.zoomRatioRangeStart = zoomRatioRange[0] this.zoomRatioRangeEnd = zoomRatioRange[1]
}
|
然后调用this.photoSession.setZoomRatio(zoom);
设置缩放比
释放资源
在拍照结束后需要释放相应的资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| await photoSession.stop();
await cameraInput.close();
await previewOutput.release();
await photoOutput.release();
await photoSession.release();
photoSession = undefined;
|
优化
设备旋转
上面的代码中我们并没有考虑设备旋转问题
1 2 3 4 5 6 7 8 9 10 11 12
| import { display } from '@kit.ArkUI';
let initDisplayRotation = display.getDefaultDisplaySync().rotation; let initPreviewRotation = previewOutput.getPreviewRotation(initDisplayRotation * camera.ImageRotation.ROTATION_90); previewOutput.setPreviewRotation(initPreviewRotation, false); display.off('change'); display.on('change', () => { initDisplayRotation = display.getDefaultDisplaySync().rotation; let imageRotation = initDisplayRotation * camera.ImageRotation.ROTATION_90; let previewRotation = previewOutput.getPreviewRotation(imageRotation); previewOutput.setPreviewRotation(previewRotation, false); });
|
在 Worker 线程中使用相机
一般情况下,设备的性能足以支持我们直接使用相机,但如果要追求极致性能,可以将拍照的一系列流程都放在 Worker 线程中完成,通过宿主线程的即时消息通信完成线程间交互
具体的代码在https://github.com/huangyuanlove/HelloArkUI/blob/main/entry/src/main/ets/pages/playground/take_photo/TakePhotoPage.ets
就不再贴一遍了。
上面代码中并没有实现切换摄像头、切换闪光灯、切换分辨率功能,只是做了展示。