
??想了解更多關(guān)于開源的內(nèi)容,請?jiān)L問:??
??51CTO 開源基礎(chǔ)軟件社區(qū)??
??https://ost.51cto.com??
最近陸續(xù)看到各社區(qū)上有關(guān)OpenHarmony媒體相機(jī)的使用開發(fā)文檔,相機(jī)對于富設(shè)備來說必不可少,日常中我們經(jīng)常使用相機(jī)完成拍照、人臉驗(yàn)證等。
OpenHarmony系統(tǒng)一個(gè)重要的能力就是分布式,對于分布式相機(jī)我也倍感興趣,之前看到官方對分布式相機(jī)的一些說明,這里簡單介紹下,有興趣可以查看官方文檔:??分布式相機(jī)部件??。
分布式框架圖

分布式相機(jī)框架(Distributed Hardware)分為主控端和被控端。假設(shè):設(shè)備B擁有本地相機(jī)設(shè)備,分布式組網(wǎng)中的設(shè)備A可以分布式調(diào)用設(shè)備B的相機(jī)設(shè)備。這種場景下,設(shè)備A是主控端,設(shè)備B是被控端,兩個(gè)設(shè)備通過軟總線進(jìn)行交互。
- VirtualCameraHAL:作為硬件適配層(HAL)的一部分,負(fù)責(zé)和分布式相機(jī)框架中的主控端交互,將主控端CameraFramwork下發(fā)的指令傳輸給分布式相機(jī)框架的SourceMgr處理。
- SourceMgr:通過軟總線將控制信息傳遞給被控端的CameraClient。
- CameraClient:直接通過調(diào)用被控端CameraFramwork的接口來完成對設(shè)備B相機(jī)的控制。
最后,從設(shè)備B反饋的預(yù)覽圖像數(shù)據(jù)會(huì)通過分布式相機(jī)框架的ChannelSink回傳到設(shè)備A的HAL層,進(jìn)而反饋給應(yīng)用。通過這種方式,設(shè)備A的應(yīng)用就可以像使用本地設(shè)備一樣使用設(shè)備B的相機(jī)。
相關(guān)名詞介紹
- 主控端(source):控制端,通過調(diào)用分布式相機(jī)能力,使用被控端的攝像頭進(jìn)行預(yù)覽、拍照、錄像等功能。
- 被控端(sink):被控制端,通過分布式相機(jī)接收主控端的命令,使用本地?cái)z像頭為主控端提供圖像數(shù)據(jù)。
現(xiàn)在我們要實(shí)現(xiàn)分布式相機(jī),在主控端調(diào)用被控端相機(jī),實(shí)現(xiàn)遠(yuǎn)程操作相機(jī),開發(fā)此應(yīng)用的具體需求:
- 支持本地相機(jī)的預(yù)覽、拍照、保存相片、相片縮略圖、快速查看相片、切換攝像頭(如果一臺(tái)設(shè)備上存在多個(gè)攝像頭時(shí));
- 同一網(wǎng)絡(luò)下,支持分布式pin碼認(rèn)證,遠(yuǎn)程連接;
- 自由切換本地相機(jī)和遠(yuǎn)程相機(jī)。
UI草圖

從草圖上看,我們簡單的明應(yīng)用UI布局的整體內(nèi)容:
1、頂部右上角有個(gè)"切換設(shè)備"的按鈕,點(diǎn)擊 彈窗顯示設(shè)備列表,可以實(shí)現(xiàn)設(shè)備認(rèn)證與設(shè)備切換功能。
2、中間使用XComponent組件實(shí)現(xiàn)的相機(jī)預(yù)覽區(qū)域。
3、底部分為三個(gè)部分。
- 相機(jī)縮略圖:顯示當(dāng)前設(shè)備媒體庫中最新的圖片,點(diǎn)擊相機(jī)縮略圖按鈕可以查看相關(guān)的圖片。
- 拍照:點(diǎn)擊拍照按鈕,將相機(jī)當(dāng)前幀保存到本地媒體庫中。
- 切換攝像頭:如果一臺(tái)設(shè)備有多個(gè)攝像頭時(shí),例如相機(jī)有前后置攝像頭,點(diǎn)擊切換后會(huì)將當(dāng)前預(yù)覽的頁面切換到另外一個(gè)攝像頭的圖像。
實(shí)現(xiàn)效果
??演示視頻地址–待發(fā)布??https://ost.51cto.com/show/21218


開發(fā)環(huán)境
系統(tǒng):OpenHarmony 3.2 beta4/OpenHarmony 3.2 beta5
設(shè)備:DAYU200
IDE:DevEco Studio 3.0 Release ,Build Version: 3.0.0.993, built on September 4, 2022
SDK:Full_3.2.9.2
開發(fā)模式:Stage
開發(fā)語言:ets
開發(fā)實(shí)踐
本篇主要在應(yīng)用層的角度實(shí)現(xiàn)分布式相機(jī),實(shí)現(xiàn)遠(yuǎn)程相機(jī)與實(shí)現(xiàn)本地相機(jī)的流程相同,只是使用的相機(jī)對象不同,所以我們先完成本地相機(jī)的開發(fā),再通過參數(shù)修改相機(jī)對象來啟動(dòng)遠(yuǎn)程相機(jī)。
1、創(chuàng)建項(xiàng)目

2、權(quán)限聲明
(1)module.json 配置權(quán)限
說明: 在module模塊下添加權(quán)限聲明,??權(quán)限的詳細(xì)說明??。
"requestPermissions": [
{
"name": "ohos.permission.REQUIRE_FORM"
},
{
"name": "ohos.permission.MEDIA_LOCATION"
},
{
"name": "ohos.permission.MODIFY_AUDIO_SETTINGS"
},
{
"name": "ohos.permission.READ_MEDIA"
},
{
"name": "ohos.permission.WRITE_MEDIA"
},
{
"name": "ohos.permission.GET_BUNDLE_INFO_PRIVILEGED"
},
{
"name": "ohos.permission.CAMERA"
},
{
"name": "ohos.permission.MICROPHONE"
},
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC"
}
]
(2)在index.ets頁面的初始化 aboutToAppear()申請權(quán)限
代碼如下:
let permissionList: Array<string> = [
"ohos.permission.MEDIA_LOCATION",
"ohos.permission.READ_MEDIA",
"ohos.permission.WRITE_MEDIA",
"ohos.permission.CAMERA",
"ohos.permission.MICROPHONE",
"ohos.permission.DISTRIBUTED_DATASYNC"
]
async aboutToAppear() {
console.info(`${TAG} aboutToAppear`)
globalThis.cameraAbilityContext.requestPermissionsFromUser(permissionList).then(async (data) => {
console.info(`${TAG} data permissions: ${JSON.stringify(data.permissions)}`)
console.info(`${TAG} data authResult: ${JSON.stringify(data.authResults)}`)
// 判斷授權(quán)是否完成
let resultCount: number = 0
for (let result of data.authResults) {
if (result === 0) {
resultCount += 1
}
}
if (resultCount === permissionList.length) {
this.isPermissions = true
}
await this.initCamera()
// 獲取縮略圖
this.mCameraService.getThumbnail(this.functionBackImpl)
})
}
這里有個(gè)獲取縮略圖的功能,主要是獲取媒體庫中根據(jù)時(shí)間排序,獲取最新拍照的圖片作為當(dāng)前需要顯示的縮略圖,實(shí)現(xiàn)此方法在后面說CameraService類的時(shí)候進(jìn)行詳細(xì)介紹。
注意: 如果首次啟動(dòng)應(yīng)用,在授權(quán)完成后需要加載相機(jī),則建議授權(quán)放在啟動(dòng)頁完成,或者在調(diào)用相機(jī)頁面之前添加一個(gè)過渡頁面,主要用于完成權(quán)限申請和啟動(dòng)相機(jī)的入口,否則首次完成授權(quán)后無法顯示相機(jī)預(yù)覽,需要退出應(yīng)用再重新進(jìn)入才可以正常預(yù)覽,這里先簡單說明下,文章后續(xù)會(huì)在問題環(huán)節(jié)詳細(xì)介紹。
3、UI布局
說明: UI如前面截圖所示,實(shí)現(xiàn)整體頁面的布局。頁面中主要使用到XComponent組件,用于EGL/OpenGLES和媒體數(shù)據(jù)寫入,并顯示在XComponent組件。參看:??XComponent詳細(xì)介紹??
- onLoad():XComponent插件加載完成時(shí)的回調(diào),在插件完成時(shí)可以獲取**ID并初始化相機(jī);
- XComponentController:XComponent組件控制器,可以綁定至XComponent組件,通過getXComponent/**aceId()獲取XComponent對應(yīng)的/**aceID。
代碼如下:
@State @Watch('selectedIndexChange') selectIndex: number = 0
// 設(shè)備列表
@State devices: Array<deviceManager.DeviceInfo> = []
// 設(shè)備選擇彈窗
private dialogController: CustomDialogController = new CustomDialogController({
builder: DeviceDialog({
deviceList: $devices,
selectIndex: $selectIndex,
}),
autoCancel: true,
alignment: DialogAlignment.Center
})
@State curPictureWidth: number = 70
@State curPictureHeight: number = 70
@State curThumbnailWidth: number = 70
@State curThumbnailHeight: number = 70
@State curSwitchAngle: number = 0
@State Id: string = ''
@State thumbnail: image.PixelMap = undefined
@State resourceUri: string = ''
@State isSwitchDeviceing: boolean = false // 是否正在切換相機(jī)
private isInitCamera: boolean = false // 是否已初始化相機(jī)
private isPermissions: boolean = false // 是否完成授權(quán)
private componentController: XComponentController = new XComponentController()
private mCurDeviceID: string = Constant.LOCAL_DEVICE_ID // 默認(rèn)本地相機(jī)
private mCurCameraIndex: number = 0 // 默認(rèn)相機(jī)列表中首個(gè)相機(jī)
private mCameraService = CameraService.getInstance()
build() {
Stack({ alignContent: Alignment.Center }) {
Column() {
Row({ space: 20 }) {
Image($r('app.media.ic_camera_public_setting'))
.width(40)
.height(40)
.margin({
right: 20
})
.objectFit(ImageFit.Contain)
.onClick(() => {
console.info(`${TAG} click distributed auth.`)
this.showDialog()
})
}
.width('100%')
.height('5%')
.margin({
top: 20,
bottom: 20
})
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.End)
Column() {
XComponent({
id: 'componentId',
type: 'xxxxace',
controller: this.componentController
}).onLoad(async () => {
console.info(`${TAG} XComponent onLoad is called`)
this.componentController.setXComponentxxxxaceSize({
xxxxWidth: Resolution.DEFAULT_WIDTH,
xxxxaceHeight: Resolution.DEFAULT_HEIGHT
})
this.id = this.componentController.getXComponentxxxxaceId()
console.info(`${TAG} id: ${this.id}`)
await this.initCamera()
}).height('100%')
.width('100%')
}
.width('100%')
.height('75%')
.margin({
bottom: 20
})
Row() {
Column() {
Image(this.thumbnail != undefined ? this.thumbnail : $r('app.media.screen_pic'))
.width(this.curThumbnailWidth)
.height(this.curThumbnailHeight)
.objectFit(ImageFit.Cover)
.onClick(async () => {
console.info(`${TAG} launch bundle com.ohos.photos`)
await globalThis.cameraAbilityContext.startAbility({
parameters: { uri: 'photodetail' },
bundleName: 'com.ohos.photos',
abilityName: 'com.ohos.photos.MainAbility'
})
animateTo({
duration: 200,
curve: Curve.EaseInOut,
delay: 0,
iterations: 1,
playMode: PlayMode.Reverse,
onFinish: () => {
animateTo({
duration: 100,
curve: Curve.EaseInOut,
delay: 0,
iterations: 1,
playMode: PlayMode.Reverse
}, () => {
this.curThumbnailWidth = 70
this.curThumbnailHeight = 70
})
}
}, () => {
this.curThumbnailWidth = 60
this.curThumbnailHeight = 60
})
})
}
.width('33%')
.alignItems(HorizontalAlign.Start)
Column() {
Image($r('app.media.icon_picture'))
.width(this.curPictureWidth)
.height(this.curPictureHeight)
.objectFit(ImageFit.Cover)
.alignRules({
center: {
align: VerticalAlign.Center,
anchor: 'center'
}
})
.onClick(() => {
this.takePicture()
animateTo({
duration: 200,
curve: Curve.EaseInOut,
delay: 0,
iterations: 1,
playMode: PlayMode.Reverse,
onFinish: () => {
animateTo({
duration: 100,
curve: Curve.EaseInOut,
delay: 0,
iterations: 1,
playMode: PlayMode.Reverse
}, () => {
this.curPictureWidth = 70
this.curPictureHeight = 70
})
}
}, () => {
this.curPictureWidth = 60
this.curPictureHeight = 60
})
})
}
.width('33%')
Column() {
Image($r('app.media.icon_switch'))
.width(50)
.height(50)
.objectFit(ImageFit.Cover)
.rotate({
x: 0,
y: 1,
z: 0,
angle: this.curSwitchAngle
})
.onClick(() => {
this.switchCamera()
animateTo({
duration: 500,
curve: Curve.EaseInOut,
delay: 0,
iterations: 1,
playMode: PlayMode.Reverse,
onFinish: () => {
animateTo({
duration: 500,
curve: Curve.EaseInOut,
delay: 0,
iterations: 1,
playMode: PlayMode.Reverse
}, () => {
this.curSwitchAngle = 0
})
}
}, () => {
this.curSwitchAngle = 180
})
})
}
.width('33%')
.alignItems(HorizontalAlign.End)
}
.width('100%')
.height('10%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.padding({
left: 40,
right: 40
})
}
.height('100%')
.width('100%')
.padding(10)
if (this.isSwitchDeviceing) {
Column() {
Image($r('app.media.load_switch_camera'))
.width(400)
.height(306)
.objectFit(ImageFit.Fill)
Text($r('app.string.switch_camera'))
.width('100%')
.height(50)
.fontSize(16)
.fontColor(Color.White)
.align(Alignment.Center)
}
.width('100%')
.height('100%')
.backgroundColor(Color.Black)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
})
}
}
.height('100%')
.backgroundColor(Color.Black)
}
(1)啟動(dòng)系統(tǒng)相冊
說明: 用戶點(diǎn)擊圖片縮略圖時(shí)需要啟動(dòng)圖片查看,這里直接打開系統(tǒng)相冊,查看相關(guān)的圖片。
代碼如下:
await globalThis.cameraAbilityContext.startAbility({
parameters: { uri: 'photodetail' },
bundleName: 'com.ohos.photos',
abilityName: 'com.ohos.photos.MainAbility'
})
4、相機(jī)服務(wù) CameraService.ts
(1)CameraService單例模式,用于提供操作相機(jī)相關(guān)的業(yè)務(wù)
代碼如下:
private static instance: CameraService = null
private constructor() {
this.mThumbnailGetter = new ThumbnailGetter()
}
/**
* 單例
*/
public static getInstance(): CameraService {
if (this.instance === null) {
this.instance = new CameraService()
}
return this.instance
}
(2)初始化相機(jī)
說明: 通過媒體相機(jī)提供的API(@ohos.multimedia.camera)getCameraManager()獲取相機(jī)管理對象CameraManager,并注冊相機(jī)狀態(tài)變化監(jiān)聽器,實(shí)時(shí)更新相機(jī)狀態(tài),同時(shí)通過CameraManager…getSupportedCameras() 獲取前期支持的相機(jī)設(shè)備集合,這里的相機(jī)設(shè)備包括當(dāng)前設(shè)備上安裝的相機(jī)設(shè)備和遠(yuǎn)程設(shè)備上的相機(jī)設(shè)備。
代碼如下:
/**
* 初始化
*/
public async initCamera(): Promise<number> {
console.info(`${TAG} initCamera`)
if (this.mCameraManager === null) {
this.mCameraManager = await camera.getCameraManager(globalThis.cameraAbilityContext)
// 注冊監(jiān)聽相機(jī)狀態(tài)變化
this.mCameraManager.on('cameraStatus', (cameraStatusInfo) => {
console.info(`${TAG} camera Status: ${JSON.stringify(cameraStatusInfo)}`)
})
// 獲取相機(jī)列表
let cameras: Array<camera.CameraDevice> = await this.mCameraManager.getSupportedCameras()
if (cameras) {
this.mCameraCount = cameras.length
console.info(`${TAG} mCameraCount: ${this.mCameraCount}`)
if (this.mCameraCount === 0) {
return this.mCameraCount
}
for (let i = 0; i < cameras.length; i++) {
console.info(`${TAG} --------------Camera Info-------------`)
const tempCameraId: string = cameras[i].cameraId
console.info(`${TAG} camera_id: ${tempCameraId}`)
console.info(`${TAG} cameraPosition: ${cameras[i].cameraPosition}`)
console.info(`${TAG} cameraType: ${cameras[i].cameraType}`)
const connectionType = cameras[i].connectionType
console.info(`${TAG} connectionType: ${connectionType}`)
// CameraPosition 0-未知未知 1-后置 2-前置
// CameraType 0-未知類型 1-廣角 2-超廣角 3長焦 4-帶景深信息
// connectionType 0-內(nèi)置相機(jī) 1-USB連接相機(jī) 2-遠(yuǎn)程連接相機(jī)
// 判斷本地相機(jī)還是遠(yuǎn)程相機(jī)
if (connectionType === camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN) {
// 本地相機(jī)
this.displayCameraDevice(Constant.LOCAL_DEVICE_ID, cameras[i])
} else if (connectionType === camera.ConnectionType.CAMERA_CONNECTION_REMOTE) {
// 遠(yuǎn)程相機(jī) 相機(jī)ID格式 : deviceID__Camera_cameraID 例如:3c8e510a1d0807ea51c2e893029a30816ed940bf848754749f427724e846fab7__Camera_lcam001
const cameraKey: string = tempCameraId.split('__Camera_')[0]
console.info(`${TAG} cameraKey: ${cameraKey}`)
this.displayCameraDevice(cameraKey, cameras[i])
}
}
// todo test 選擇首個(gè)相機(jī)
this.mCurCameraDevice = cameras[0]
console.info(`${TAG} mCurCameraDevice: ${this.mCurCameraDevice.cameraId}`)
}
}
return this.mCameraCount
}
/**
* 處理相機(jī)設(shè)備
* @param key
* @param cameraDevice
*/
private displayCameraDevice(key: string, cameraDevice: camera.CameraDevice) {
console.info(`${TAG} displayCameraDevice ${key}`)
if (this.mCameraMap.has(key) && this.mCameraMap.get(key)?.length > 0) {
console.info(`${TAG} displayCameraDevice has mCameraMap`)
// 判斷相機(jī)列表中是否已經(jīng)存在此相機(jī)
let isExist: boolean = false
for (let item of this.mCameraMap.get(key)) {
if (item.cameraId === cameraDevice.cameraId) {
isExist = true
break
}
}
// 添加列表中沒有的相機(jī)
if (!isExist) {
console.info(`${TAG} displayCameraDevice not exist , push ${cameraDevice.cameraId}`)
this.mCameraMap.get(key).push(cameraDevice)
} else {
console.info(`${TAG} displayCameraDevice has existed`)
}
} else {
let cameras: Array<camera.CameraDevice> = []
console.info(`${TAG} displayCameraDevice push ${cameraDevice.cameraId}`)
cameras.push(cameraDevice)
this.mCameraMap.set(key, cameras)
}
}
(3)創(chuàng)建相機(jī)輸入流
說明: CameraManager.createCameraInput()可以創(chuàng)建相機(jī)輸出流CameraInput實(shí)例,CameraInput是在CaptureSession會(huì)話中使用的相機(jī)信息,支持打開相機(jī)、關(guān)閉相機(jī)等能力。
代碼如下:
/**
* 創(chuàng)建相機(jī)輸入流
* @param cameraIndex 相機(jī)下標(biāo)
* @param deviceId 設(shè)備ID
*/
public async createCameraInput(cameraIndex?: number, deviceId?: string) {
console.info(`${TAG} createCameraInput`)
if (this.mCameraManager === null) {
console.error(`${TAG} mCameraManager is null`)
return
}
if (this.mCameraCount <= 0) {
console.error(`${TAG} not camera device`)
return
}
if (this.mCameraInput) {
this.mCameraInput.release()
}
if (deviceId && this.mCameraMap.has(deviceId)) {
if (cameraIndex < this.mCameraMap.get(deviceId)?.length) {
this.mCurCameraDevice = this.mCameraMap.get(deviceId)[cameraIndex]
} else {
this.mCurCameraDevice = this.mCameraMap.get(deviceId)[0]
}
}
console.info(`${TAG} mCurCameraDevice: ${this.mCurCameraDevice.cameraId}`)
try {
this.mCameraInput = await this.mCameraManager.createCameraInput(this.mCurCameraDevice)
console.info(`${TAG} mCameraInput: ${JSON.stringify(this.mCameraInput)}`)
this.mCameraInput.on('error', this.mCurCameraDevice, (error) => {
console.error(`${TAG} CameraInput error: ${JSON.stringify(error)}`)
})
await this.mCameraInput.open()
} catch (err) {
if (err) {
console.error(`${TAG} failed to createCameraInput`)
}
}
}
(4)相機(jī)預(yù)覽輸出流
說明: CameraManager.createPreviewOutput() 創(chuàng)建預(yù)覽輸出流對象PreviewOutput,PreviewOutput繼承CameraOutput,在CaptureSession會(huì)話中使用的輸出信息,支持開始輸出預(yù)覽流、停止預(yù)覽輸出流、釋放預(yù)覽輸出流等能力。
/**
* 創(chuàng)建相機(jī)預(yù)覽輸出流
*/
public async createPreviewOutput(Id: string, callback : PreviewCallBack) {
console.info(`${TAG} createPreviewOutput`)
if (this.mCameraManager === null) {
console.error(`${TAG} createPreviewOutput mCameraManager is null`)
return
}
this.Id = Id
console.info(`${TAG} Id ${Id}}`)
// 獲取當(dāng)前相機(jī)設(shè)備支持的輸出能力
let cameraOutputCap = await this.mCameraManager.getSupportedOutputCapability(this.mCurCameraDevice)
if (!cameraOutputCap) {
console.error(`${TAG} createPreviewOutput getSupportedOutputCapability error}`)
return
}
console.info(`${TAG} createPreviewOutput cameraOutputCap ${JSON.stringify(cameraOutputCap)}`)
let previewProfilesArray = cameraOutputCap.previewProfiles
let previewProfiles: camera.Profile
if (!previewProfilesArray || previewProfilesArray.length <= 0) {
console.error(`${TAG} createPreviewOutput previewProfilesArray error}`)
previewProfiles = {
format: 1,
size: {
width: 640,
height: 480
}
}
} else {
console.info(`${TAG} createPreviewOutput previewProfile length ${previewProfilesArray.length}`)
previewProfiles = previewProfilesArray[0]
}
console.info(`${TAG} createPreviewOutput previewProfile[0] ${JSON.stringify(previewProfiles)}`)
try {
this.mPreviewOutput = await this.mCameraManager.createPreviewOutput(previewProfiles, id
)
console.info(`${TAG} createPreviewOutput success`)
// 監(jiān)聽預(yù)覽幀開始
this.mPreviewOutput.on('frameStart', () => {
console.info(`${TAG} createPreviewOutput camera frame Start`)
callback.onFrameStart()
})
this.mPreviewOutput.on('frameEnd', () => {
console.info(`${TAG} createPreviewOutput camera frame End`)
callback.onFrameEnd()
})
this.mPreviewOutput.on('error', (error) => {
console.error(`${TAG} createPreviewOutput error: ${error}`)
})
} catch (err) {
console.error(`${TAG} failed to createPreviewOutput ${err}`)
}
}
(5)拍照輸出流
說明: CameraManager.createPhotoOutput()可以創(chuàng)建拍照輸出對象 PhotoOutput,PhotoOutput繼承CameraOutput 在拍照會(huì)話中使用的輸出信息,支持拍照、判斷是否支持鏡像拍照、釋放資源、監(jiān)聽拍照開始、拍照幀輸出捕獲、拍照結(jié)束等能力。
代碼如下:
/**
* 創(chuàng)建拍照輸出流
*/
public async createPhotoOutput(functionCallback: FunctionCallBack) {
console.info(`${TAG} createPhotoOutput`)
if (!this.mCameraManager) {
console.error(`${TAG} createPhotoOutput mCameraManager is null`)
return
}
// 通過寬、高、圖片格式、容量創(chuàng)建ImageReceiver實(shí)例
const receiver: image.ImageReceiver = image.createImageReceiver(Resolution.DEFAULT_WIDTH, Resolution.DEFAULT_HEIGHT, image.ImageFormat.JPEG, 8)
const imageId: string = await receiver.getReceivingxxxxaceId()
console.info(`${TAG} createPhotoOutput imageId: ${imageId}`)
let cameraOutputCap = await this.mCameraManager.getSupportedOutputCapability(this.mCurCameraDevice)
console.info(`${TAG} createPhotoOutput cameraOutputCap ${cameraOutputCap}`)
if (!cameraOutputCap) {
console.error(`${TAG} createPhotoOutput getSupportedOutputCapability error}`)
return
}
let photoProfilesArray = cameraOutputCap.photoProfiles
let photoProfiles: camera.Profile
if (!photoProfilesArray || photoProfilesArray.length <= 0) {
// 使用自定義的配置
photoProfiles = {
format: 2000,
size: {
width: 1280,
height: 960
}
}
} else {
console.info(`${TAG} createPhotoOutput photoProfile length ${photoProfilesArray.length}`)
photoProfiles = photoProfilesArray[0]
}
console.info(`${TAG} createPhotoOutput photoProfile ${JSON.stringify(photoProfiles)}`)
try {
this.mPhotoOutput = await this.mCameraManager.createPhotoOutput(photoProfiles, id)
console.info(`${TAG} createPhotoOutput mPhotoOutput success`)
// 保存圖片
this.mSaveCameraAsset.saveImage(receiver, Resolution.THUMBNAIL_WIDTH, Resolution.THUMBNAIL_HEIGHT, this.mThumbnailGetter, functionCallback)
} catch (err) {
console.error(`${TAG} createPhotoOutput failed to createPhotoOutput ${err}`)
}
}
- this.mSaveCameraAsset.saveImage(),這里將保存拍照的圖片進(jìn)行封裝—SaveCameraAsset.ts,后面會(huì)單獨(dú)介紹。
(6)會(huì)話管理
說明: 通過CameraManager.createCaptureSession()可以創(chuàng)建相機(jī)的會(huì)話類,保存相機(jī)運(yùn)行所需要的所有資源CameraInput、CameraOutput,并向相機(jī)設(shè)備申請完成相機(jī)拍照或錄像功能。CaptureSession對象提供了開始配置會(huì)話、添加CameraInput到會(huì)話、添加CameraOutput到會(huì)話、提交配置信息、開始會(huì)話、停止會(huì)話、釋放等能力。
代碼如下:
public async createSession(id: string) {
console.info(`${TAG} createSession`)
console.info(`${TAG} createSession id ${id}}`)
this.id= id
this.mCaptureSession = await this.mCameraManager.createCaptureSession()
console.info(`${TAG} createSession mCaptureSession ${this.mCaptureSession}`)
this.mCaptureSession.on('error', (error) => {
console.error(`${TAG} CaptureSession error ${JSON.stringify(error)}`)
})
try {
await this.mCaptureSession?.beginConfig()
await this.mCaptureSession?.addInput(this.mCameraInput)
if (this.mPhotoOutput != null) {
console.info(`${TAG} createSession addOutput PhotoOutput`)
await this.mCaptureSession?.addOutput(this.mPhotoOutput)
}
await this.mCaptureSession?.addOutput(this.mPreviewOutput)
} catch (err) {
if (err) {
console.error(`${TAG} createSession beginConfig fail err:${JSON.stringify(err)}`)
}
}
try {
await this.mCaptureSession?.commitConfig()
} catch (err) {
if (err) {
console.error(`${TAG} createSession commitConfig fail err:${JSON.stringify(err)}`)
}
}
try {
await this.mCaptureSession?.start()
} catch (err) {
if (err) {
console.error(`${TAG} createSession start fail err:${JSON.stringify(err)}`)
}
}
console.info(`${TAG} createSession mCaptureSession start`)
}
5、拍照
說明: 通過PhotoOutput.capture()可以實(shí)現(xiàn)拍照功能。
代碼如下:
/**
* 拍照
*/
public async takePicture() {
console.info(`${TAG} takePicture`)
if (!this.mCaptureSession) {
console.info(`${TAG} takePicture session is release`)
return
}
if (!this.mPhotoOutput) {
console.info(`${TAG} takePicture mPhotoOutput is null`)
return
}
try {
const photoCaptureSetting: camera.PhotoCaptureSetting = {
quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
rotation: camera.ImageRotation.ROTATION_0,
location: {
latitude: 0,
longitude: 0,
altitude: 0
},
mirror: false
}
await this.mPhotoOutput.capture(photoCaptureSetting)
} catch (err) {
console.error(`${TAG} takePicture err:${JSON.stringify(err)}`)
}
}
6、保存圖片 SaveCameraAsset
說明: SaveCameraAsset.ts主要用于保存拍攝的圖片,即是調(diào)用拍照操作后,會(huì)觸發(fā)圖片接收監(jiān)聽器,在將圖片的字節(jié)流進(jìn)行寫入本地文件操作。
代碼如下:
/**
* 保存相機(jī)拍照的資源
*/
import image from '@ohos.multimedia.image'
import mediaLibrary from '@ohos.multimedia.mediaLibrary'
import { FunctionCallBack } from '../model/CameraService'
import DateTimeUtil from '../utils/DateTimeUtil'
import fileIO from '@ohos.file.fs';
import ThumbnailGetter from '../model/ThumbnailGetter'
let photoUri: string // 圖片地址
const TAG: string = 'SaveCameraAsset'
export default class SaveCameraAsset {
private lastSaveTime: string = ''
private saveIndex: number = 0
constructor() {
}
public getPhotoUri(): string {
console.info(`${TAG} getPhotoUri = ${photoUri}`)
return photoUri
}
/**
* 保存拍照圖片
* @param imageReceiver 圖像接收對象
* @param thumbWidth 縮略圖寬度
* @param thumbHeight 縮略圖高度
* @param callback 回調(diào)
*/
public saveImage(imageReceiver: image.ImageReceiver, thumbWidth: number, thumbHeight: number, thumbnailGetter :ThumbnailGetter, callback: FunctionCallBack) {
console.info(`${TAG} saveImage`)
const mDateTimeUtil = new DateTimeUtil()
const fileKeyObj = mediaLibrary.FileKey
const mediaType = mediaLibrary.MediaType.IMAGE
let buffer = new ArrayBuffer(4096)
const media = mediaLibrary.getMediaLibrary(globalThis.cameraAbilityContext) // 獲取媒體庫實(shí)例
// 接收圖片回調(diào)
imageReceiver.on('imageArrival', async () => {
console.info(`${TAG} saveImage ImageArrival`)
// 使用當(dāng)前時(shí)間命名
const displayName = this.checkName(`IMG_${mDateTimeUtil.getDate()}_${mDateTimeUtil.getTime()}`) + '.jpg'
console.info(`${TAG} displayName = ${displayName}}`)
imageReceiver.readNextImage((err, imageObj: image.Image) => {
if (imageObj === undefined) {
console.error(`${TAG} saveImage failed to get valid image error = ${err}`)
return
}
// 根據(jù)圖像的組件類型從圖像中獲取組件緩存 4-JPEG類型
imageObj.getComponent(image.ComponentType.JPEG, async (errMsg, imgComponent) => {
if (imgComponent === undefined) {
console.error(`${TAG} getComponent failed to get valid buffer error = ${errMsg}`)
return
}
if (imgComponent.byteBuffer) {
console.info(`${TAG} getComponent imgComponent.byteBuffer ${imgComponent.byteBuffer}`)
buffer = imgComponent.byteBuffer
} else {
console.info(`${TAG} getComponent imgComponent.byteBuffer is undefined`)
}
await imageObj.release()
})
})
let publicPath:string = await media.getPublicDirectory(mediaLibrary.DirectoryType.DIR_CAMERA)
console.info(`${TAG} saveImage publicPath = ${publicPath}`)
// 創(chuàng)建媒體資源 返回提供封裝文件屬性
const dataUri : mediaLibrary.FileAsset = await media.createAsset(mediaType, displayName, publicPath)
// 媒體文件資源創(chuàng)建成功,將拍照的數(shù)據(jù)寫入到媒體資源
if (dataUri !== undefined) {
photoUri = dataUri.uri
console.info(`${TAG} saveImage photoUri: ${photoUri}`)
const args = dataUri.id.toString()
console.info(`${TAG} saveImage id: ${args}`)
// 通過ID查找媒體資源
const fetchOptions:mediaLibrary.MediaFetchOptions = {
selections : `${fileKeyObj.ID} = ?`,
selectionArgs : [args]
}
console.info(`${TAG} saveImage fetchOptions: ${JSON.stringify(fetchOptions)}`)
const fetchFileResult = await media.getFileAssets(fetchOptions)
const fileAsset = await fetchFileResult.getAllObject() // 獲取文件檢索結(jié)果中的所有文件資
if (fileAsset != undefined) {
fileAsset.forEach((dataInfo) => {
dataInfo.open('Rw').then((fd) => { // RW是讀寫方式打開文件 獲取fd
console.info(`${TAG} saveImage dataInfo.open called. fd: ${fd}`)
// 將緩存圖片流寫入資源
fileIO.write(fd, buffer).then(() => {
console.info(`${TAG} saveImage fileIO.write called`)
dataInfo.close(fd).then(() => {
console.info(`${TAG} saveImage dataInfo.close called`)
// 獲取資源縮略圖
thumbnailGetter.getThumbnailInfo(thumbWidth, thumbHeight, photoUri).then((thumbnail => {
if (thumbnail === undefined) {
console.error(`${TAG} saveImage getThumbnailInfo undefined`)
callback.onCaptureFailure()
} else {
console.info(`${TAG} photoUri: ${photoUri} PixelBytesNumber: ${thumbnail.getPixelBytesNumber()}`)
callback.onCaptureSuccess(thumbnail, photoUri)
}
}))
}).catch(error => {
console.error(`${TAG} saveImage close is error ${JSON.stringify(error)}`)
})
})
})
})
} else {
console.error(`${TAG} saveImage fileAsset: is null`)
}
} else {
console.error(`${TAG} saveImage photoUri is null`)
}
})
}
/**
* 檢測文件名稱
* @param fileName 文件名稱
* 如果同一時(shí)間有多張圖片,則使用時(shí)間_index命名
*/
private checkName(fileName: string): string {
if (this.lastSaveTime == fileName) {
this.saveIndex++
return `${fileName}_${this.saveIndex}`
}
this.lastSaveTime = fileName
this.saveIndex = 0
return fileName
}
}
7、獲取縮略圖
說明: 主要通過獲取當(dāng)前媒體庫中根據(jù)時(shí)間排序,獲取最新的圖片并縮放圖片大小后返回。
代碼如下:
/**
* 獲取縮略圖
* @param callback
*/
public getThumbnail(callback: FunctionCallBack) {
console.info(`${TAG} getThumbnail`)
this.mThumbnailGetter.getThumbnailInfo(Resolution.THUMBNAIL_WIDTH, Resolution.THUMBNAIL_HEIGHT).then((thumbnail) => {
console.info(`${TAG} getThumbnail thumbnail = ${thumbnail}`)
callback.thumbnail(thumbnail)
})
}
(1)ThumbnailGetter.ts
說明: 實(shí)現(xiàn)獲取縮略圖的對象。
代碼如下:
/**
* 縮略圖處理器
*/
import mediaLibrary from '@ohos.multimedia.mediaLibrary';
import image from '@ohos.multimedia.image';
const TAG: string = 'ThumbnailGetter'
export default class ThumbnailGetter {
public async getThumbnailInfo(width: number, height: number, uri?: string): Promise<image.PixelMap | undefined> {
console.info(`${TAG} getThumbnailInfo`)
// 文件關(guān)鍵信息
const fileKeyObj = mediaLibrary.FileKey
// 獲取媒體資源公共路徑
const media: mediaLibrary.MediaLibrary = mediaLibrary.getMediaLibrary(globalThis.cameraAbilityContext)
let publicPath: string = await media.getPublicDirectory(mediaLibrary.DirectoryType.DIR_CAMERA)
console.info(`${TAG} publicPath = ${publicPath}`)
let fetchOptions: mediaLibrary.MediaFetchOptions = {
selections: `${fileKeyObj.RELATIVE_PATH}=?`, // 檢索條件 RELATIVE_PATH-相對公共目錄的路徑
selectionArgs: [publicPath] // 檢索條件值
}
if (uri) {
fetchOptions.uri = uri // 文件的URI
} else {
fetchOptions.order = fileKeyObj.DATE_ADDED + ' DESC'
}
console.info(`${TAG} getThumbnailInfo fetchOptions : ${JSON.stringify(fetchOptions)}}`)
const fetchFileResult = await media.getFileAssets(fetchOptions) // 文件檢索結(jié)果集
const count = fetchFileResult.getCount()
console.info(`${TAG} count = ${count}`)
if (count == 0) {
return undefined
}
// 獲取結(jié)果集合中的最后一張圖片
const lastFileAsset = await fetchFileResult.getFirstObject()
if (lastFileAsset == null) {
console.error(`${TAG} getThumbnailInfo lastFileAsset is null`)
return undefined
}
const thumbnailPixelMap = lastFileAsset.getThumbnail({
width: width,
height: height
})
console.info(`${TAG} getThumbnailInfo thumbnailPixelMap ${JSON.stringify(thumbnailPixelMap)}}`)
return thumbnailPixelMap
}
}
8、釋放資源
說明: 在相機(jī)設(shè)備切換時(shí),如前后置攝像頭切換或者不同設(shè)備之間的攝像頭切換時(shí)都需要先釋放資源,再重新創(chuàng)建新的相機(jī)會(huì)話才可以正常運(yùn)行,釋放的資源包括:釋放相機(jī)輸入流、預(yù)覽輸出流、拍照輸出流、會(huì)話。
代碼如下:
/**
* 釋放相機(jī)輸入流
*/
public async releaseCameraInput() {
console.info(`${TAG} releaseCameraInput`)
if (this.mCameraInput) {
try {
await this.mCameraInput.release()
} catch (err) {
console.error(`${TAG} releaseCameraInput ${err}}`)
}
this.mCameraInput = null
}
}
/**
* 釋放預(yù)覽輸出流
*/
public async releasePreviewOutput() {
console.info(`${TAG} releasePreviewOutput`)
if (this.mPreviewOutput) {
await this.mPreviewOutput.release()
this.mPreviewOutput = null
}
}
/**
* 釋放拍照輸出流
*/
public async releasePhotoOutput() {
console.info(`${TAG} releasePhotoOutput`)
if (this.mPhotoOutput) {
await this.mPhotoOutput.release()
this.mPhotoOutput = null
}
}
public async releaseSession() {
console.info(`${TAG} releaseSession`)
if (this.mCaptureSession) {
await this.mCaptureSession.stop()
console.info(`${TAG} releaseSession stop`)
await this.mCaptureSession.release()
console.info(`${TAG} releaseSession release`)
this.mCaptureSession = null
console.info(`${TAG} releaseSession null`)
}
}
至此,總結(jié)下,需要實(shí)現(xiàn)相機(jī)預(yù)覽、拍照功能:1、通過camera媒體api提供的camera.getCameraManager()獲取CameraManager相機(jī)管理類;
2、通過相機(jī)管理類型創(chuàng)建相機(jī)預(yù)覽與拍照需要的輸入流(createCameraInput)和輸出流(createPreviewOutPut、createPhotoOutput),同時(shí)創(chuàng)建相關(guān)會(huì)話管理(createCaptureSession)
3、將輸入流、輸出流添加到會(huì)話中,并啟動(dòng)會(huì)話
4、拍照可以直接使用PhotoOutput.capture執(zhí)行拍照,并將拍照結(jié)果保存到媒體
5、在退出相機(jī)應(yīng)用時(shí),需要注意釋放相關(guān)的資源。因?yàn)榉植际较鄼C(jī)的應(yīng)用開發(fā)內(nèi)容比較長,這篇只說到主控端相機(jī)設(shè)備預(yù)覽與拍照功能,下一篇會(huì)將結(jié)合分布式相關(guān)內(nèi)容完成主控端設(shè)備調(diào)用遠(yuǎn)程相機(jī)進(jìn)行預(yù)覽的功能。