OpenHarmony - 相机(Camera)的调用

系统 OpenHarmony
本文主要介绍在OpenHarmony应用开发中ArkUI开发框架下相机应用的开发。


想了解更多关于开源的内容,请访问:

​51CTO 开源基础软件社区​

​https://ost.51cto.com​

一、前言

camera使用介绍

相机是一个系统的基础能力,能够通过调用相机进行拍照,在很多场景下都会使用到相机的调用,如人脸识别门禁,人脸解锁等操作。

本文主要介绍在OpenHarmony应用开发中ArkUI开发框架下相机应用的开发。

开发模式:Stage开发模式

SDK版本:3.2.2.5

开发环境:DevEco Studio 3.0 Release 3.0.0.993

效果展示

相机调用成功如下图:

OpenHarmony - 相机(Camera)的调用 -开源基础软件社区

二、实现步骤

1、声明权限

(1)在module.json5中配置权限

"reqPermissions": [   {
"name": "ohos.permission.LOCATION",
},
{
"name": "ohos.permission.CAMERA"
},
{
"name": "ohos.permission.MICROPHONE"
},
{
"name": "ohos.permission.MEDIA_LOCATION"
},
{
"name": "ohos.permission.WRITE_MEDIA"
},
{
"name": "ohos.permission.READ_MEDIA"
}]

(2)在MainAbility.ts中调用requestPermissionsFromUser方法申请权限

const PERMISSIONS: Array<string> = [
'ohos.permission.CAMERA',
'ohos.permission.MICROPHONE',
'ohos.permission.MEDIA_LOCATION',
'ohos.permission.READ_MEDIA',
'ohos.permission.WRITE_MEDIA',
'ohos.permission.GET_WIFI_INFO ',
'ohos.permission.GET_WIFI_PEERS_MAC ',
]

globalThis.abilityWant = want;
globalThis.context = this.context
globalThis.abilityContext = this.context;

globalThis.context.requestPermissionsFromUser(PERMISSIONS).then((message)=>{
console.log(JSON.stringify(message))
})

注意:权限需要在页面加载前提前申请,所以需要在调用相机的页面前添加一个过渡的页面。

2、准备工作

(1)导包

import camera from '@ohos.multimedia.camera';
import image from '@ohos.multimedia.image';
import fileio from '@ohos.fileio';
import mediaLibrary from '@ohos.multimedia.mediaLibrary'
const CameraSize = {
WIDTH: 640,
HEIGHT: 480
}

(2)定义变量

private mXComponentController = new XComponentController()
private cameraManager: camera.CameraManager = undefined
private cameras: Array<camera.Camera> = undefined
private cameraId: string = undefined
private mReceiver: image.ImageReceiver = undefined
private cameraInput: camera.CameraInput = undefined
private previewOutput: camera.PreviewOutput = undefined
private mSurfaceId: string = undefined
private photoOutput: camera.PhotoOutput = undefined
private captureSession: camera.CaptureSession = undefined
private mediaUtil: MediaUtil = undefined
@State desStr: string = ""
private fileAsset: mediaLibrary.FileAsset
private surfaceId: number
@State photoUriMedia: string = ""
private photoFlag: boolean = true
@State imgUrl: string = ""
@State isMediaUrl:boolean=true //判断保存路径为是沙箱路径或者媒体路径,默认媒体路径
aboutToAppear(){
this.mediaTest = mediaLibrary.getMediaLibrary(globalThis.context)
}

(3)工具方法

async createAndGetUri(mediaType: number) {
let info = this.getInfoFromType(mediaType)
let dateTimeUtil = new DateTimeUtil()
let name = `${dateTimeUtil.getDate()}_${dateTimeUtil.getTime()}`
let displayName = `${info.prefix}${name}${info.suffix}`
let publicPath = await this.mediaTest.getPublicDirectory(info.directory)
let dataUri = await this.mediaTest.createAsset(mediaType, displayName, publicPath)
return dataUri
}
async getFdPath(fileAsset: any) {
let fd = await fileAsset.open('Rw')
return fd
}
getInfoFromType(mediaType: number) {
let result = {
prefix: '', suffix: '', directory: 0
}
switch (mediaType) {
case mediaLibrary.MediaType.FILE:
result.prefix = 'FILE_'
result.suffix = '.txt'
result.directory = mediaLibrary.DirectoryType.DIR_DOCUMENTS
break
case mediaLibrary.MediaType.IMAGE:
result.prefix = 'IMG_'
result.suffix = '.jpg'
result.directory = mediaLibrary.DirectoryType.DIR_IMAGE
break
case mediaLibrary.MediaType.VIDEO:
result.prefix = 'VID_'
result.suffix = '.mp4'
result.directory = mediaLibrary.DirectoryType.DIR_VIDEO
break
case mediaLibrary.MediaType.AUDIO:
result.prefix = 'AUD_'
result.suffix = '.wav'
result.directory = mediaLibrary.DirectoryType.DIR_AUDIO
break
}
return result
}

(4)工具类

/**
* @file 日期工具
*/
export default class DateTimeUtil {

/**
* 时分秒
*/
getTime() {
const DATETIME = new Date()
return this.concatTime(DATETIME.getHours(), DATETIME.getMinutes(), DATETIME.getSeconds())
}

/**
* 年月日
*/
getDate() {
const DATETIME = new Date()
return this.concatDate(DATETIME.getFullYear(), DATETIME.getMonth() + 1, DATETIME.getDate())
}

/**
* 日期不足两位补充0
* @param value-数据值
*/
fill(value: number) {
return (value > 9 ? '' : '0') + value
}

/**
* 年月日格式修饰
* @param year
* @param month
* @param date
*/
concatDate(year: number, month: number, date: number) {
return `${year}${this.fill(month)}${this.fill(date)}`
}

/**
* 时分秒格式修饰
* @param hours
* @param minutes
* @param seconds
*/
concatTime(hours: number, minutes: number, seconds: number) {
return `${this.fill(hours)}${this.fill(minutes)}${this.fill(seconds)}`
}
}

这个工具类主要是用来进行获取时间对相片进行命名的工具类。

3、构建UI组件

页面主要分为2块,左边为相机的XComponent组件,右边为图片显示区域。拍完的照片能够显示在右边。XComponent组件作用于EGL/OpenGLES和媒体数据写入,并显示在XComponent组件。相关资料https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-basic-components-xcomponent-0000001333800561。

hml代码如下:

build() {
Flex() {
Flex() {
Stack() {
Flex() {
//相机显示的组件
XComponent({
id: 'componentId',
type: 'surface',
controller: this.mXComponentController
}).onLoad(() => {
this.mXComponentController.setXComponentSurfaceSize({ surfaceWidth: 640, surfaceHeight: 480 })
this.surfaceId = this.mXComponentController.getXComponentSurfaceId()
this.initCamera(this.surfaceId)
})
}.width(800).height(800)
//显示在相机上面的组件:拍照和摄像的图标,摄像的时间
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.End, alignItems: ItemAlign.Center }) {
if (this.photoFlag) { //拍照
Image($r("app.media.take_photo_normal")).width(50).height(50).onClick(() => {
this.desStr = "拍照完成"
this.takePicture()
})
}
Text(this.desStr).fontColor("red").height(30).fontSize(20)
}.width(480).height(480)
}.border({ width: 1, style: BorderStyle.Solid, color: "#000000" })
//右边的控制button和图片显示区域
Flex({
direction: FlexDirection.Column,
justifyContent: FlexAlign.SpaceBetween,
alignItems: ItemAlign.Center,
}) {
Button("选择沙箱路径存储").onClick(()=>{
this.isMediaUrl=false
}) .stateStyles({
normal: { // 设置默认情况下的显示样式
.backgroundColor(Color.Blue)
},
pressed: { // 设置手指摁下时的显示样式
.backgroundColor(Color.Pink)
}
})
Image(decodeURI("file://"+this.imgUrl)).width(480).height(350)//显示沙箱图片
Button("选择媒体路径存储").onClick(()=>{
this.isMediaUrl=true
}) .stateStyles({
normal: { // 设置默认情况下的显示样式
.backgroundColor(Color.Blue)
},
pressed: { // 设置手指摁下时的显示样式
.backgroundColor(Color.Pink)
}
})
Image(decodeURI(this.imgUrl)).width(480).height(350) //显示媒体图片
}.width(480).height("100%").border({ width: 1, style: BorderStyle.Solid, color: "#000000" })

}.border({ width: 1, style: BorderStyle.Solid, color: "red" })
.width("100%").height("100%")
}
.height('100%').width("100%")
}

UI实现了对存储路径的选择,需要存储到沙箱路径还是媒体路径。

注意:沙箱路径需要加上"file://",查看对应的存储路径步骤:

  1. 打开hdc命令窗口。
  2. cd /data/app/el2/100/base/com.chinasoft.photo/haps/entry/files进入。
  3. ls查看全部文件。

4、拍照流程

(1)初始化相机

这一步需要在拍照前就进行,一般是在XComponent组件的onLoad()中进行的。

//初始化相机和会话管理
async initCamera(surfaceId: number) {
this.cameraManager = await camera.getCameraManager(globalThis.context)//需要在Ability中定义globalThis.context=this.context
this.cameras = await this.cameraManager.getCameras()
this.cameraId = this.cameras[1].cameraId
await this.photoReceiver() //创建图片接收器并进行订阅
this.mSurfaceId = await this.mReceiver.getReceivingSurfaceId()
this.cameraInput = await this.cameraManager.createCameraInput(this.cameraId)
this.previewOutput = await camera.createPreviewOutput(surfaceId.toString())
this.photoOutput = await camera.createPhotoOutput(this.mSurfaceId)

this.captureSession = await camera.createCaptureSession(globalThis.context)
await this.captureSession.beginConfig()
await this.captureSession.addInput(this.cameraInput)
await this.captureSession.addOutput(this.previewOutput)
await this.captureSession.addOutput(this.photoOutput)
await this.captureSession.commitConfig()
await this.captureSession.start().then(() => {
console.log('zmw1--Promise returned to indicate the session start success.');
})
}
//创建图片接收器并进行订阅
async photoReceiver() {
this.mReceiver = image.createImageReceiver(CameraSize.WIDTH, CameraSize.HEIGHT, 4, 8)
let buffer = new ArrayBuffer(4096)
this.mReceiver.on('imageArrival', () => {
console.log("zmw -service-imageArrival")
this.mReceiver.readNextImage((err, image) => {
if (err || image === undefined) {
return
}
image.getComponent(4, (errMsg, img) => {
if (errMsg || img === undefined) {
return
}
if (img.byteBuffer) {
buffer = img.byteBuffer
}
if(this.isMediaUrl){
this.savePictureMedia(buffer, image)
}else{
this.savePictureSand(buffer, image)
}
})
})
return buffer
})
}
  1. 根据camera的getCameraManager方法获取CameraManager。
  2. 通过CameraManager获取所有的相机数组,找到可用的相机,并获取相机的cameraid。
  3. 创建图片接收器并进行订阅,获取receiver的surfaceId。
  4. 通过CameraManager的createCameraInput(cameraid)创建相机输入流。
  5. 通过camera的createPreviewOutput(sufaceId)创建相机预览输出流.这里sufaceId为XComponent的id。
  6. 通过camera的createPhotoOutput(sufaceId)创建相机拍照输出流.这里sufaceId为图片接收器的surfaceId。
  7. 会话管理:创建会话,并且配置会话的相机输入流,相机拍照输出流与相机预览流,提交配置,开始会话。

至此,相机就能正常的显示出图像了。

(2)用拍照方法拍摄照片

//拍摄照片
async takePicture() {
let photoSettings = {
rotation: camera.ImageRotation.ROTATION_0,
quality: camera.QualityLevel.QUALITY_LEVEL_LOW,
mirror: false
}
await this.photoOutput.capture(photoSettings)
}

调用相机的输出流的capture方法进行拍照操作,会触发图片接收器的监听,进行对字节流的写入操作,保存到沙箱或者媒体。

(3)保存图片

分为沙箱路径与媒体路径:

//保存沙箱路径
async savePictureSand(buffer: ArrayBuffer, img: image.Image) {
let info = this.mediaUtil.getInfoFromType(mediaLibrary.MediaType.IMAGE)
let dateTimeUtil = new DateTimeUtil()
let name = `${dateTimeUtil.getDate()}_${dateTimeUtil.getTime()}`
let displayName = `${info.prefix}${name}${info.suffix}`
let sandboxDirPath = globalThis.context.filesDir;
let path = sandboxDirPath + '/' + displayName
this.imgUrl=path
let fdSand = await fileio.open(path, 0o2 | 0o100, 0o666);
await fileio.write(fdSand, buffer)
await fileio.close(fdSand).then(()=>{
this.desStr=""
});
await img.release()
}
//保存媒体路径
async savePictureMedia(buffer: ArrayBuffer, img: image.Image) {
this.fileAsset = await this.mediaUtil.createAndGetUri(mediaLibrary.MediaType.IMAGE)
this.imgUrl = this.fileAsset.uri
let fd = await this.mediaUtil.getFdPath(this.fileAsset)
await fileio.write(fd, buffer)
await this.fileAsset.close(fd).then(()=>{
this.desStr=""
})
await img.release()
}

(4)释放相机

//结束释放相机资源
async releaseCamera() {
if (this.captureSession) {
await this.captureSession.stop().then(() => {
})
}
if (this.cameraInput) {
await this.cameraInput.release().then(() => {
})
}
if (this.previewOutput) {
await this.previewOutput.release().then(() => {
})
}
if (this.photoOutput) {
await this.photoOutput.release().then(() => {
})
}
// 释放会话
if (this.captureSession) {
await this.captureSession.release((err) => {
if (err) {
console.error('zmw Failed to release the CaptureSession instance ${err.message}');
return;
}
});
}
}

在完成了相机的调用后,需要对相机的资源进行释放,否则再次调用的时候会一直被占用而导致黑屏。

三、总结

openHarmony对于相机的官方使用文档不太清晰,有许多的坑,需要去趟,在这个过程中我遇到的问题:

  1. 在相机的使用时,由于开发板上的相机获取到了两个,一个是外接USB的相机,一个应该是系统的,在获取相机的id的时候需要注意。
  2. 在保存相机拍照的图片的时候,保存到沙箱路径时显示不到页面上,需要在保存的路径前加上"file://"。

需要扩展研究的是进行相机的摄像操作,以及相机拍照与摄像的切换操作。

参考资料:

https://gitee.com/openharmony/app_samples/tree/master/media/Scan。

https://gitee.com/openharmony/applications_camera。

https://gitee.com/openharmony/docs/blob/OpenHarmony-3.2-Beta3/zh-cn/application-dev/media/camera.md。

想了解更多关于开源的内容,请访问:

​51CTO 开源基础软件社区​

​https://ost.51cto.com​

责任编辑:jianghua 来源: 51CTO 开源基础软件社区
相关推荐

2023-11-27 08:21:49

Camera2API,

2022-01-06 16:16:21

鸿蒙HarmonyOS应用

2022-04-21 11:26:31

鸿蒙操作系统

2023-02-21 16:41:41

分布式相机鸿蒙

2023-02-20 15:38:38

2023-02-20 15:29:14

分布式相机鸿蒙

2021-11-01 17:31:21

Camera2 相机开发

2015-12-04 10:25:50

VR拍照谷歌

2013-06-13 17:30:16

Camera360拍照软件

2021-09-13 15:15:18

鸿蒙HarmonyOS应用

2013-01-09 17:22:38

Android开发Camera

2022-06-07 10:33:29

Camera组件鸿蒙

2022-02-24 16:00:59

Ability鸿蒙JS

2011-07-29 10:41:27

IPhone 应用开发 照相机

2022-12-15 17:35:37

2022-02-14 13:52:04

OpenHarmor系统鸿蒙

2022-02-08 15:07:45

OpenHarmor鸿蒙操作系统

2022-03-14 15:11:01

harmony鸿蒙操作系统

2022-05-20 10:56:54

AbilityeTS FA调用

2022-02-25 15:33:45

图像采集鸿蒙Camera相机
点赞
收藏

51CTO技术栈公众号