OpenHarmony上實現分布式相機

51cto 發佈 2024-05-08T17:34:47.469737+00:00

假設:設備 B 擁有本地相機設備,分布式組網中的設備 A 可以分布式調用設備 B 的相機設備。現在我們要實現分布式相機,在主控端調用被控端相機,實現遠程操作相機,開發此應用的具體需求:支持本地相機的預覽、拍照、保存相片、相片縮略圖、快速查看相片、切換攝像頭。

作者:徐金生

最近陸續看到各社區上有關 OpenHarmony 媒體相機的使用開發文檔,相機對於富設備來說必不可少,日常中我們經常使用相機完成拍照、人臉驗證等。

OpenHarmony 系統一個重要的能力就是分布式,對於分布式相機我也倍感興趣,之前看到官方對分布式相機的一些說明,這裡簡單介紹下。

有興趣可以查看官方文檔:分布式相機部件

https://gitee.com/openharmony/distributedhardware_distributed_camera

分布式框架圖



分布式相機框架(Distributed Hardware)分為主控端和被控端。假設:設備 B 擁有本地相機設備,分布式組網中的設備 A 可以分布式調用設備 B 的相機設備。

這種場景下,設備 A 是主控端,設備 B 是被控端,兩個設備通過軟總線進行交互。

  • VirtualCameraHAL:作為硬體適配層(HAL)的一部分,負責和分布式相機框架中的主控端交互,將主控端 CameraFramwork 下發的指令傳輸給分布式相機框架的 SourceMgr 處理。
  • SourceMgr:通過軟總線將控制信息傳遞給被控端的 CameraClient。
  • CameraClient:直接通過調用被控端 CameraFramwork 的接口來完成對設備 B 相機的控制。

最後,從設備 B 反饋的預覽圖像數據會通過分布式相機框架的 ChannelSink 回傳到設備 A 的 HAL 層,進而反饋給應用。通過這種方式,設備 A 的應用就可以像使用本地設備一樣使用設備 B 的相機。

相關名詞介紹:

  • 主控端(source):控制端,通過調用分布式相機能力,使用被控端的攝像頭進行預覽、拍照、錄像等功能。
  • 被控端(sink):被控制端,通過分布式相機接收主控端的命令,使用本地攝像頭為主控端提供圖像數據。

現在我們要實現分布式相機,在主控端調用被控端相機,實現遠程操作相機,開發此應用的具體需求:

  • 支持本地相機的預覽、拍照、保存相片、相片縮略圖、快速查看相片、切換攝像頭(如果一台設備上存在多個攝像頭時)。
  • 同一網絡下,支持分布式 pin 碼認證,遠程連接。
  • 自由切換本地相機和遠程相機。

UI 草圖

從草圖上看,我們簡單的明應用 UI 布局的整體內容:

  • 頂部右上角有個"切換設備"的按鈕,點擊彈窗顯示設備列表,可以實現設備認證與設備切換功能。
  • 中間使用 XComponent 組件實現的相機預覽區域。
  • 底部分為如下三個部分。

具體如下:

  • 相機縮略圖:顯示當前設備媒體庫中最新的圖片,點擊相機縮略圖按鈕可以查看相關的圖片。
  • 拍照:點擊拍照按鈕,將相機當前幀保存到本地媒體庫中。
  • 切換攝像頭:如果一台設備有多個攝像頭時,例如相機有前後置攝像頭,點擊切換後會將當前預覽的頁面切換到另外一個攝像頭的圖像。

實現效果

開發環境

如下:

  • 系統:OpenHarmony 3.2 beta4/OpenHarmony 3.2 beta5
  • 設備:DAYU200
  • IDE:DevEco Studio 3.0 Release ,Build Version: 3.0.0.993, built on September 4, 2022
  • SDK:Full_3.2.9.2
  • 開發模式:Stage
  • 開發語言:ets

開發實踐

本篇主要在應用層的角度實現分布式相機,實現遠程相機與實現本地相機的流程相同,只是使用的相機對象不同,所以我們先完成本地相機的開發,再通過參數修改相機對象來啟動遠程相機。

①創建項目

②權限聲明

(1)module.json 配置權限

說明: 在 module 模塊下添加權限聲明,權限的詳細說明

https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/security/permission-list.md
"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() 申請權限

代碼如下:

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)}`)
      // 判斷授權是否完成
      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)
    })
  }

這裡有個獲取縮略圖的功能,主要是獲取媒體庫中根據時間排序,獲取最新拍照的圖片作為當前需要顯示的縮略圖,實現此方法在後面說 CameraService 類的時候進行詳細介紹。

注意:如果首次啟動應用,在授權完成後需要加載相機,則建議授權放在啟動頁完成,或者在調用相機頁面之前添加一個過渡頁面,主要用於完成權限申請和啟動相機的入口,否則首次完成授權後無法顯示相機預覽,需要退出應用再重新進入才可以正常預覽,這裡先簡單說明下,文章後續會在問題環節詳細介紹。

③UI 布局

說明:UI 如前面截圖所示,實現整體頁面的布局。

頁面中主要使用到 XComponent 組件,用於 EGL/OpenGLES 和媒體數據寫入,並顯示在 XComponent 組件。

參看:XComponent 詳細介紹

https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-xcomponent.md

onLoad():XComponent 插件加載完成時的回調,在插件完成時可以獲取**ID並初始化相機。

XComponentController:XComponent 組件控制器,可以綁定至 XComponent 組件,通過 getXComponent/**aceId() 獲取 XComponent 對應的/**aceID。

代碼如下:

@State @Watch('selectedIndexChange') selectIndex: number = 0
  // 設備列表
  @State devices: Array<deviceManager.DeviceInfo> = []
  // 設備選擇彈窗
  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 // 是否正在切換相機
  private isInitCamera: boolean = false // 是否已初始化相機
  private isPermissions: boolean = false // 是否完成授權
  private componentController: XComponentController = new XComponentController()
  private mCurDeviceID: string = Constant.LOCAL_DEVICE_ID // 默認本地相機
  private mCurCameraIndex: number = 0 //  默認相機列表中首個相機
  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)啟動系統相冊

說明:用戶點擊圖片縮略圖時需要啟動圖片查看,這裡直接打開系統相冊,查看相關的圖片。

代碼如下:

await globalThis.cameraAbilityContext.startAbility({
                  parameters: { uri: 'photodetail' },
                  bundleName: 'com.ohos.photos',
                  abilityName: 'com.ohos.photos.MainAbility'
                })

④相機服務 CameraService.ts

(1)CameraService 單例模式,用於提供操作相機相關的業務

代碼如下:

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)初始化相機

說明:通過媒體相機提供的 API(@ohos.multimedia.camera)getCameraManager() 獲取相機管理對象 CameraManager,並註冊相機狀態變化監聽器,實時更新相機狀態。

同時通過 CameraManager…getSupportedCameras() 獲取前期支持的相機設備集合,這裡的相機設備包括當前設備上安裝的相機設備和遠程設備上的相機設備。

代碼如下:

/**
     * 初始化
     */
    public async initCamera(): Promise<number> {
        console.info(`${TAG} initCamera`)
        if (this.mCameraManager === null) {
            this.mCameraManager = await camera.getCameraManager(globalThis.cameraAbilityContext)
            // 註冊監聽相機狀態變化
            this.mCameraManager.on('cameraStatus', (cameraStatusInfo) => {
                console.info(`${TAG} camera Status: ${JSON.stringify(cameraStatusInfo)}`)
            })
            // 獲取相機列表
            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-內置相機 1-USB連接相機 2-遠程連接相機
                    // 判斷本地相機還是遠程相機
                    if (connectionType === camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN) {
                        // 本地相機
                        this.displayCameraDevice(Constant.LOCAL_DEVICE_ID, cameras[i])
                    } else if (connectionType === camera.ConnectionType.CAMERA_CONNECTION_REMOTE) {
                        // 遠程相機 相機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 選擇首個相機
                this.mCurCameraDevice = cameras[0]
                console.info(`${TAG} mCurCameraDevice: ${this.mCurCameraDevice.cameraId}`)
            }
        }
        return this.mCameraCount
    }


    /**
     * 處理相機設備
     * @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`)
            // 判斷相機列表中是否已經存在此相機
            let isExist: boolean = false
            for (let item of this.mCameraMap.get(key)) {
                if (item.cameraId === cameraDevice.cameraId) {
                    isExist = true
                    break
                }
            }
            // 添加列表中沒有的相機
            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)創建相機輸入流

說明:CameraManager.createCameraInput() 可以創建相機輸出流 CameraInput 實例,CameraInput 是在 CaptureSession 會話中使用的相機信息,支持打開相機、關閉相機等能力。

代碼如下:

/**
     * 創建相機輸入流
     * @param cameraIndex 相機下標
     * @param deviceId 設備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)相機預覽輸出流

說明:CameraManager.createPreviewOutput() 創建預覽輸出流對象 PreviewOutput,PreviewOutput 繼承 CameraOutput,在 CaptureSession 會話中使用的輸出信息,支持開始輸出預覽流、停止預覽輸出流、釋放預覽輸出流等能力。

/**
     * 創建相機預覽輸出流
     */
    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}}`)
        // 獲取當前相機設備支持的輸出能力
        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`)
            // 監聽預覽幀開始
            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() 可以創建拍照輸出對象 PhotoOutput,PhotoOutput 繼承 CameraOutput 在拍照會話中使用的輸出信息,支持拍照、判斷是否支持鏡像拍照、釋放資源、監聽拍照開始、拍照幀輸出捕獲、拍照結束等能力。

代碼如下:

/**
     * 創建拍照輸出流
     */
    public async createPhotoOutput(functionCallback: FunctionCallBack) {
        console.info(`${TAG} createPhotoOutput`)
        if (!this.mCameraManager) {
            console.error(`${TAG} createPhotoOutput mCameraManager is null`)
            return
        }
        // 通過寬、高、圖片格式、容量創建ImageReceiver實例
        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(),這裡將保存拍照的圖片進行封裝—SaveCameraAsset.ts,後面會單獨介紹。

(6)會話管理

說明:通過 CameraManager.createCaptureSession() 可以創建相機的會話類,保存相機運行所需要的所有資源 CameraInput、CameraOutput,並向相機設備申請完成相機拍照或錄像功能。

CaptureSession 對象提供了開始配置會話、添加 CameraInput 到會話、添加 CameraOutput 到會話、提交配置信息、開始會話、停止會話、釋放等能力。

代碼如下:

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`)
    }

⑤拍照

說明:通過 PhotoOutput.capture() 可以實現拍照功能。

代碼如下:

/**
     * 拍照
     */
    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)}`)
        }
    }

⑥保存圖片 SaveCameraAsset

說明:SaveCameraAsset.ts 主要用於保存拍攝的圖片,即是調用拍照操作後,會觸發圖片接收監聽器,在將圖片的字節流進行寫入本地文件操作。

代碼如下:

/**
 * 保存相機拍照的資源
 */
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 回調
     */
    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) // 獲取媒體庫實例
        // 接收圖片回調
        imageReceiver.on('imageArrival', async () => {
            console.info(`${TAG} saveImage ImageArrival`)
            // 使用當前時間命名
            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
                }
                // 根據圖像的組件類型從圖像中獲取組件緩存 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}`)
            //  創建媒體資源 返回提供封裝文件屬性
            const dataUri : mediaLibrary.FileAsset = await media.createAsset(mediaType, displayName, publicPath)
            // 媒體文件資源創建成功,將拍照的數據寫入到媒體資源
            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() // 獲取文件檢索結果中的所有文件資
                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 文件名稱
     * 如果同一時間有多張圖片,則使用時間_index命名
     */
    private checkName(fileName: string): string {
        if (this.lastSaveTime == fileName) {
            this.saveIndex++
            return `${fileName}_${this.saveIndex}`
        }
        this.lastSaveTime = fileName
        this.saveIndex = 0
        return fileName
    }
}

⑦獲取縮略圖

說明:主要通過獲取當前媒體庫中根據時間排序,獲取最新的圖片並縮放圖片大小後返回。

代碼如下:

/**
     * 獲取縮略圖
     * @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

說明: 實現獲取縮略圖的對象。

代碼如下:

/**
 * 縮略圖處理器
 */
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`)
        // 文件關鍵信息
        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) // 文件檢索結果集
        const count = fetchFileResult.getCount()
        console.info(`${TAG} count = ${count}`)
        if (count == 0) {
            return undefined
        }
        // 獲取結果集合中的最後一張圖片
        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
    }
}

⑧釋放資源

說明:在相機設備切換時,如前後置攝像頭切換或者不同設備之間的攝像頭切換時都需要先釋放資源,再重新創建新的相機會話才可以正常運行,釋放的資源包括:釋放相機輸入流、預覽輸出流、拍照輸出流、會話。

代碼如下:

/**
     * 釋放相機輸入流
     */
    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
        }
    }



/**
     * 釋放預覽輸出流
     */
    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`)
        }
    }

至此,總結下,需要實現相機預覽、拍照功能:

  • 通過 camera 媒體 api 提供的 camera.getCameraManager() 獲取 CameraManager 相機管理類。
  • 通過相機管理類型創建相機預覽與拍照需要的輸入流(createCameraInput)和輸出流(createPreviewOutPut、createPhotoOutput),同時創建相關會話管理(createCaptureSession)
  • 將輸入流、輸出流添加到會話中,並啟動會話
  • 拍照可以直接使用 PhotoOutput.capture 執行拍照,並將拍照結果保存到媒體
  • 在退出相機應用時,需要注意釋放相關的資源。

因為分布式相機的應用開發內容比較長,這篇只說到主控端相機設備預覽與拍照功能,下一篇會將結合分布式相關內容完成主控端設備調用遠程相機進行預覽的功能。

關鍵字: