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

目录

大致流程

开发

权限处理

获取可用相机列表

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

创建并配置会话

拍照回调和启动会话

其他配置

闪光灯

连续自动对焦

缩放

释放资源

优化

设备旋转

[在 Worker 线程中使用相机](#在 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 线程中完成,通过宿主线程的即时消息通信完成线程间交互

相关推荐
小雨下雨的雨7 分钟前
HarmonyOS 应用开发实战:高精图像处理与头像裁剪持久化技术深度解析
图像处理·人工智能·华为·ai·交互·harmonyos·鸿蒙系统
讯方洋哥23 分钟前
HarmonyOS App开发——职前通应用App开发(上)
华为·harmonyos
江湖有缘36 分钟前
基于华为openEuler部署Sqliteviz轻量级SQLite可视化工具
jvm·华为·sqlite
洋九八1 小时前
Hi3861 OpenHarmony 多线程操作、Timer 定时器、点灯、 IO 相关设备控制
开发语言·华为·harmonyos
芒鸽1 小时前
基于 lycium 适配鸿蒙版 Ruby 的解决方案
华为·ruby·harmonyos
一起养小猫1 小时前
Flutter for OpenHarmony 实战:打造功能完整的记账助手应用
android·前端·flutter·游戏·harmonyos
一起养小猫1 小时前
Flutter for OpenHarmony 实战:打造功能完整的云笔记应用
网络·笔记·spring·flutter·json·harmonyos
一起养小猫1 小时前
Flutter for OpenHarmony 实战:笔记应用文件操作与数据管理详解
flutter·harmonyos
摘星编程1 小时前
React Native鸿蒙版:Calendar日历组件
react native·react.js·harmonyos
前端不太难1 小时前
HarmonyOS PC 焦点系统的正确建模方式
华为·状态模式·harmonyos