鸿蒙NEXT开发-自定义相机拍照

发布于:2025-03-12 ⋅ 阅读:(11) ⋅ 点赞:(0)

目录

大致流程

开发

权限处理

获取可用相机列表

创建相机输入流并打开相机

创建并配置会话

拍照回调和启动会话

其他配置

闪光灯

连续自动对焦

缩放

释放资源

优化

设备旋转

在 Worker 线程中使用相机


大致流程

相机开发需要使用真机,模拟器目前还是不支持的。这就劝退了一部分开发者。
所需要的调用的接口大部分集中在@kit.CameraKit@kit.AbilityKit中。保存图片时需要用到@kit.ImageKit@kit.CoreFileKit@kit.MediaLibraryKit
接下来看下需要做哪些工作:

  1. 获取相机权限
  2. 获取可用相机列表
    1. 可以在这里监听相机状态(USB相机连接、断开连接、关闭、被占用等)
    2. 选择当前使用的相机
  3. 创建相机输入流并打开相机
    1. 可以创建相机输入流
    2. 可以监听预览输出流状态,包括预览流启动、预览流结束、预览流输出错误
    3. 可以获取相机支持的模式列表(NORMAL_PHOTO,NORMAL_VIDEO,SECURE_PHOTO)
    4. 可以获取当前相机设备支持的所有输出流,如预览流、拍照流、录像流等
  4. 会话(Session)管理
    1. 配置相机的输入流和输出流(分辨路等配置)
    2. 添加闪光灯、调整焦距等配置
    3. 会话切换控制:切换拍照或者录像
    4. 交和开启会话,可以开始调用相机相关功能
  5. 预览
    1. 创建Surface用于预览
    2. 将预览输出流通过SurfaceID与Surface关联
    3. 调用Session.start方法开始预览
  6. 拍照
    1. 创建拍照输出流
    2. 设置拍照photoAvailable的回调,并将拍照的buffer保存为图片。
    3. 参数配置(闪光灯、变焦、焦距等)
    4. 触发拍照

开发

权限处理

在进入拍照页面之前先申请权限,具体的流程看这里申请应用权限,本文不再赘述。

获取可用相机列表

首先要获取相机管理实例,这里为了代码看起来清晰,将各个步骤写到了单独的方法中。
另外多出使用camera.CameraManager实例,因此定义为了全局变量


@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;
      }
      // 如果当通过USB连接相机设备时,回调函数会返回新的相机出现状态
      if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_APPEAR) {
        hilog.info(0x01,TAG,`New Camera device appear.`);
      }
      // 如果当断开相机设备USB连接时,回调函数会返回相机被移除状态
      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中预览做准备。

    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错误信息
    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)来获取相机支持的模式,一般情况下都会支持拍照和录像。
      之后我们获取设备支持的输出流能力

      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之后,可以从该对象的previewProfilesphotoProfiles属性中获取到设备支持的分辨率大小。这里我们直接使用1920*1080的分辨率。
      需要注意的是 previewProfilesphotoProfiles所支持的分辨率不一定是一致的。预览的话只要宽高比一致,分辨率别差的太离谱就可以。

      之后我们使用选择好的Profile对象来创建拍照输出流和预览输出流

          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

        private mXComponentController: XComponentController = new XComponentController;
        XComponent({
        id: 'componentId',
        type: XComponentType.SURFACE,
        controller: this.mXComponentController,
        })
        .onLoad(async () => {
            this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
        })
        

          所以这里我们需要注意一下创建预览输出流的时机

          创建并配置会话

          创建会话也只是一行的就可以搞定,但可能会有异常出现

            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;
              }
              // 监听session错误信息
              this.photoSession.on('error', (error: BusinessError) => {
                console.error(`Capture session error code: ${error.code}`);
              });
            }
          

            配置会话主要是添加相机输入流、预览输出流和拍照输出流,最后提交配置

            
              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();
              }
            

              拍照回调和启动会话

              我们先启动会话

              await this.photoSession.start().then(() => {
                      console.info('Promise returned to indicate the session start success.');
                    });
              

                会话启动之后我们就可以进行拍照了。拍照的话需要调用拍照输出流的capture方法

                takePhoto() {
                if (!this.photoOutput) {
                    return
                }
                let photoCaptureSetting: camera.PhotoCaptureSetting = {
                    quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, // 设置图片质量高
                    rotation: camera.ImageRotation.ROTATION_0 // 设置图片旋转角度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事件监听,该监听可以在创建拍照输出流之后就添加

                  setPhotoOutputCb(): void {
                      if (!this.photoOutput) {
                        return
                      }
                      //设置回调之后,调用photoOutput的capture方法,就会将拍照的buffer回传到回调中
                      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)之后,可以通过该对象配置闪光灯模式、对焦模式、缩放等

                    闪光灯

                    首先判断设备是否支持闪光灯,然后再判断支持的闪光灯模式。

                     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);
                          }
                        }
                      }
                    
                      连续自动对焦

                      也是需要先判断是否支持自动连续对焦,不支持的话只能手动对焦

                        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)方法进行对焦

                        缩放

                        同样的,需要先获取到支持的缩放范围

                          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);设置缩放比

                          释放资源

                          在拍照结束后需要释放相应的资源

                          await photoSession.stop();
                          
                          // 释放相机输入流
                          await cameraInput.close();
                          
                          // 释放预览输出流
                          await previewOutput.release();
                          
                          // 释放拍照输出流
                          await photoOutput.release();
                          
                          // 释放会话
                          await photoSession.release();
                          
                          // 会话置空
                          photoSession = undefined;
                          

                            优化

                            设备旋转

                            上面的代码中我们并没有考虑设备旋转问题

                            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 线程中完成,通过宿主线程的即时消息通信完成线程间交互