
??想了解更多關(guān)于開源的內(nèi)容,請訪問:??
??51CTO 開源基礎(chǔ)軟件社區(qū)??
??https://ost.51cto.com??
一、前言
camera使用介紹
相機(jī)是一個系統(tǒng)的基礎(chǔ)能力,能夠通過調(diào)用相機(jī)進(jìn)行拍照,在很多場景下都會使用到相機(jī)的調(diào)用,如人臉識別門禁,人臉解鎖等操作。
本文主要介紹在OpenHarmony應(yīng)用開發(fā)中ArkUI開發(fā)框架下相機(jī)應(yīng)用的開發(fā)。
開發(fā)模式:Stage開發(fā)模式
SDK版本:3.2.2.5
開發(fā)環(huán)境:DevEco Studio 3.0 Release 3.0.0.993
效果展示
相機(jī)調(diào)用成功如下圖:

二、實現(xiàn)步驟
1、聲明權(quán)限
(1)在module.json5中配置權(quán)限
"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中調(diào)用requestPermissionsFromUser方法申請權(quán)限
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))
})
注意:權(quán)限需要在頁面加載前提前申請,所以需要在調(diào)用相機(jī)的頁面前添加一個過渡的頁面。
2、準(zhǔn)備工作
(1)導(dǎo)包
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 //判斷保存路徑為是沙箱路徑或者媒體路徑,默認(rèn)媒體路徑
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())
}
/**
* 日期不足兩位補(bǔ)充0
* @param value-數(shù)據(jù)值
*/
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)}`
}
}
這個工具類主要是用來進(jìn)行獲取時間對相片進(jìn)行命名的工具類。
3、構(gòu)建UI組件
頁面主要分為2塊,左邊為相機(jī)的XComponent組件,右邊為圖片顯示區(qū)域。拍完的照片能夠顯示在右邊。XComponent組件作用于EGL/OpenGLES和媒體數(shù)據(jù)寫入,并顯示在XComponent組件。相關(guān)資料https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-basic-components-xcomponent-0000001333800561。
hml代碼如下:
build() {
Flex() {
Flex() {
Stack() {
Flex() {
//相機(jī)顯示的組件
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)
//顯示在相機(jī)上面的組件:拍照和攝像的圖標(biāo),攝像的時間
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和圖片顯示區(qū)域
Flex({
direction: FlexDirection.Column,
justifyContent: FlexAlign.SpaceBetween,
alignItems: ItemAlign.Center,
}) {
Button("選擇沙箱路徑存儲").onClick(()=>{
this.isMediaUrl=false
}) .stateStyles({
normal: { // 設(shè)置默認(rèn)情況下的顯示樣式
.backgroundColor(Color.Blue)
},
pressed: { // 設(shè)置手指摁下時的顯示樣式
.backgroundColor(Color.Pink)
}
})
Image(decodeURI("file://"+this.imgUrl)).width(480).height(350)//顯示沙箱圖片
Button("選擇媒體路徑存儲").onClick(()=>{
this.isMediaUrl=true
}) .stateStyles({
normal: { // 設(shè)置默認(rèn)情況下的顯示樣式
.backgroundColor(Color.Blue)
},
pressed: { // 設(shè)置手指摁下時的顯示樣式
.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實現(xiàn)了對存儲路徑的選擇,需要存儲到沙箱路徑還是媒體路徑。
注意:沙箱路徑需要加上"file://",查看對應(yīng)的存儲路徑步驟:
- 打開hdc命令窗口。
- cd /data/app/el2/100/base/com.chinasoft.photo/haps/entry/files進(jìn)入。
- ls查看全部文件。
4、拍照流程
(1)初始化相機(jī)
這一步需要在拍照前就進(jìn)行,一般是在XComponent組件的onLoad()中進(jìn)行的。
//初始化相機(jī)和會話管理
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() //創(chuàng)建圖片接收器并進(jìn)行訂閱
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.');
})
}
//創(chuàng)建圖片接收器并進(jìn)行訂閱
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
})
}
- 根據(jù)camera的getCameraManager方法獲取CameraManager。
- 通過CameraManager獲取所有的相機(jī)數(shù)組,找到可用的相機(jī),并獲取相機(jī)的cameraid。
- 創(chuàng)建圖片接收器并進(jìn)行訂閱,獲取receiver的surfaceId。
- 通過CameraManager的createCameraInput(cameraid)創(chuàng)建相機(jī)輸入流。
- 通過camera的createPreviewOutput(sufaceId)創(chuàng)建相機(jī)預(yù)覽輸出流.這里sufaceId為XComponent的id。
- 通過camera的createPhotoOutput(sufaceId)創(chuàng)建相機(jī)拍照輸出流.這里sufaceId為圖片接收器的surfaceId。
- 會話管理:創(chuàng)建會話,并且配置會話的相機(jī)輸入流,相機(jī)拍照輸出流與相機(jī)預(yù)覽流,提交配置,開始會話。
至此,相機(jī)就能正常的顯示出圖像了。
(2)用拍照方法拍攝照片
//拍攝照片
async takePicture() {
let photoSettings = {
rotation: camera.ImageRotation.ROTATION_0,
quality: camera.QualityLevel.QUALITY_LEVEL_LOW,
mirror: false
}
await this.photoOutput.capture(photoSettings)
}
調(diào)用相機(jī)的輸出流的capture方法進(jìn)行拍照操作,會觸發(fā)圖片接收器的監(jiān)聽,進(jìn)行對字節(jié)流的寫入操作,保存到沙箱或者媒體。
(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)釋放相機(jī)
//結(jié)束釋放相機(jī)資源
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;
}
});
}
}
在完成了相機(jī)的調(diào)用后,需要對相機(jī)的資源進(jìn)行釋放,否則再次調(diào)用的時候會一直被占用而導(dǎo)致黑屏。
三、總結(jié)
openHarmony對于相機(jī)的官方使用文檔不太清晰,有許多的坑,需要去趟,在這個過程中我遇到的問題:
- 在相機(jī)的使用時,由于開發(fā)板上的相機(jī)獲取到了兩個,一個是外接USB的相機(jī),一個應(yīng)該是系統(tǒng)的,在獲取相機(jī)的id的時候需要注意。
- 在保存相機(jī)拍照的圖片的時候,保存到沙箱路徑時顯示不到頁面上,需要在保存的路徑前加上"file://"。
需要擴(kuò)展研究的是進(jìn)行相機(jī)的攝像操作,以及相機(jī)拍照與攝像的切換操作。
參考資料:
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。
??想了解更多關(guān)于開源的內(nèi)容,請訪問:??
??51CTO 開源基礎(chǔ)軟件社區(qū)??
??https://ost.51cto.com??