HarmonyOS技术精讲-Camera Kit(相机服务)第12篇:自动切换摄像头实现

在这里插入图片描述

1. 开篇:从一次翻转说起

自动切换摄像头这个功能,在视频通话、直播、自拍等场景里几乎成了标配。用户把手机翻过来,摄像头就从前置切到后置;再翻回去,又切回前置。看起来很自然的一个操作,但很多人第一次尝试在HarmonyOS NEXT上实现时,会发现官方示例里只有手动切换的代码,根本没有自动切换的参考。哪怕你照着文档把cameraManager.getSupportedCameras()cameraManager.switchCamera()拼出来,也会遇到两个坑:

  1. 传感器信号来了,但相机还没准备好——直接调用switchCamera会抛异常。
  2. 频繁翻转导致反复切换——设备在翻转临界点时,加速度计数值抖动,一秒内触发多次切换,UI卡顿甚至黑屏。

这篇文章就专门解决这两个问题。我会从传感器监听、人脸检测的辅助判断、切换策略到平滑过渡,给出一套可直接运行的ArkTS代码。文末还会总结实际接入中踩过的几个怪毛病,帮助你避开同样的问题。

2. 它解决什么问题

2.1 场景与价值

自动切换摄像头适合以下场景:

场景 用户行为 自动切换需求
视频通话(微信/畅连) 手机翻过来看画面 从前置切到后置,让对方看到自己视角
自拍 手背对着自己时想拍风景 从后置切回前置(或反之)
直播带货 展示商品需要前后交替 跟随手机角度自动切换前后摄像头

不适合的场景:固定机位的监控、需要精确控制摄像头切换的专业录制(比如慢动作连拍)。自动切换在这些场景下反而会干扰操作。

2.2 为什么不用手动切换

手动切换简单,但用户操作成本高:必须在UI上放置切换按钮,而且需要额外的交互逻辑(比如切换时暂停预览)。自动切换可以让交互更自然,减少用户操作步骤。代价是增加了传感器监听和状态管理的复杂度。

3. 环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机(真机测试,模拟器传感器数据不准)

权限要求:需要在module.json5中声明以下权限:

  • ohos.permission.CAMERA
  • ohos.permission.ACCELEROMETER(加速度传感器)

4. 核心实现

4.1 总体设计

自动切换的核心流程:

  1. 初始化相机,获取前后摄像头列表。
  2. 注册加速度传感器监听,监听ACCELEROMETER数据,计算设备翻转方向。
  3. 当翻转角度超过阈值(比如60度)时,执行切换逻辑:
    • 先判断当前是否正在切换中(加锁)。
    • 调用cameraManager.switchCamera(targetCamera).
    • 切换完成后,更新UI显示当前摄像头方向。
  4. 可选:在切换前请求一帧预览图像,用人脸检测API确认是否存在人脸,如果人脸存在则不切换(防止自拍时误切)。

4.2 步骤一:权限声明与相机初始化

这部分代码是基础,但需要注意一件事:摄像头权限要用动态授权,否则cameraManager.switchCamera会直接返回错误。

// 申请相机权限
import { abilityAccessCtrl, PermissionRequestResult } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

async function requestCameraPermission(): Promise<boolean> {
  const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
  const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  try {
    const result: PermissionRequestResult = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']);
    return result.permissions[0].granted;
  } catch (error) {
    console.error('Camera permission request failed: ' + JSON.stringify(error));
    return false;
  }
}

相机初始化(前提是已经调用过requestCameraPermission):

import { camera } from '@kit.CameraKit';
import { image } from '@kit.ImageKit';

let cameraManager: camera.CameraManager;
let cameras: camera.CameraDevice[];
let currentCameraIndex: number = 0; // 0:后置, 1:前置

async function initCamera(): Promise<void> {
  const context = getContext(this) as common.UIAbilityContext;
  cameraManager = camera.getCameraManager(context);
  // 获取支持的所有摄像头
  cameras = cameraManager.getSupportedCameras();
  if (cameras.length < 2) {
    console.warn('Only one camera available, auto switch may not work.');
    return;
  }
  // 默认使用后置摄像头(索引0)
  currentCameraIndex = 0;
}

4.3 步骤二:加速度传感器监听

我们用@ohos.sensor模块的on方法订阅加速度数据。ACCELEROMETER返回xyz三个轴的值(单位为m/s²)。当设备平放时,z轴≈9.8;竖屏时,y≈9.8;横屏时,x≈9.8。我们主要关注z轴:正值表示屏幕朝上,负值表示屏幕朝下。

import { sensor } from '@kit.SensorServiceKit';

let flipThreshold: number = 6.0; // 加速度变化阈值
let lastFlipTime: number = 0;
const FLIP_COOLDOWN: number = 500; // 冷却时间500ms

function startSensorListener(): void {
  sensor.on(sensor.SensorType.ACCELEROMETER, (data: sensor.AccelerometerResponse) => {
    let currentTime = Date.now();
    if (currentTime - lastFlipTime < FLIP_COOLDOWN) {
      return; // 冷却期内忽略
    }
    let z = data.z;
    // 判断翻转:从屏幕朝上变为朝下(z从正变为负)
    // 实际可以用z的符号变化,但需要记住之前的状态
    // 简化:当z的绝对值小于某个值(表示侧放)或符号变化时触发
    // 这里用连续判断:若z < -4.0(朝下)且当前是前置,切后置;若z > 4.0(朝上)且当前是后置,切前置
    // 注意:相机可能还未初始化
    if (z < -4.0 && currentCameraIndex === 1) { // 前置 -> 后置
      switchCamera(0);
    } else if (z > 4.0 && currentCameraIndex === 0) { // 后置 -> 前置
      switchCamera(1);
    }
  });
}

function stopSensorListener(): void {
  sensor.off(sensor.SensorType.ACCELEROMETER);
}

说明:这里的阈值4.0是经验值,可以根据设备实际测试调整。更精确的做法是使用ROTATION_VECTOR传感器,但ACCELEROMETER实现简单,且对入门来说足够。

4.4 步骤三:切换摄像头(核心方法)

cameraManager.switchCamera的返回值是void,但实际切换需要等到CameraStatusChange回调才能确认完成。为了平滑切换,我们在调用前先暂停预览,切换后再恢复。

let previewOutput: camera.PreviewOutput;
let isSwitching: boolean = false;

async function switchCamera(targetIndex: number): Promise<void> {
  if (isSwitching) {
    console.warn('Camera switching in progress, ignored.');
    return;
  }
  if (targetIndex === currentCameraIndex) {
    return;
  }
  if (targetIndex >= cameras.length) {
    console.error('Target camera index out of range.');
    return;
  }
  isSwitching = true;
  try {
    // 先释放当前输出,再创建新的
    if (previewOutput) {
      await previewOutput.release();
    }
    // 创建新的预览输出
    const cameraInput: camera.CameraInput = cameraManager.createCameraInput(cameras[targetIndex]);
    const session: camera.CaptureSession = cameraManager.createCaptureSession();
    session.beginConfig();
    session.addInput(cameraInput);
    // 输出配置(假设surfaceId已获取)
    previewOutput = cameraManager.createPreviewOutput(surfaceId);
    session.addOutput(previewOutput);
    await session.commitConfig();
    await session.start();
    currentCameraIndex = targetIndex;
    // 切换成功
    console.info('Camera switched to index: ' + targetIndex);
  } catch (error) {
    console.error('Switch camera failed: ' + JSON.stringify(error));
  } finally {
    isSwitching = false;
  }
}

注意surfaceId需要从XComponentImageReceiver获取,这部分属于Camera基础能力,这里不展开。假设外部已经持有surfaceId

4.5 步骤四:集成人脸检测(可选)

人脸检测的作用是:当检测到人脸时,锁定当前摄像头,不执行自动切换。这样在自拍时,即使手机微微翻转,也不会误切到后置摄像头。

我们使用@kit.AIKit的人脸检测能力。注意:人脸检测需要传入image.PixelMap对象,可以从预览帧中获取。为了简化,我们只在前置摄像头状态下,每次传感器触发切换前,请求一帧预览图像进行检测。如果检测到人脸,则放弃本次切换。

import { faceDetection } from '@kit.AIKit';

async function hasFaceInPreview(): Promise<boolean> {
  // 从previewOutput中获取一帧图像(需要结合ImageReceiver)
  // 这里假设已经有一个getCurrentFrame(): Promise<image.PixelMap>的方法
  try {
    const pixelMap: image.PixelMap = await getCurrentFrame();
    if (!pixelMap) {
      return false;
    }
    const faceDetector = faceDetection.createFaceDetector();
    const faces = await faceDetector.detect(pixelMap);
    pixelMap.release();
    return faces.length > 0;
  } catch (e) {
    console.error('Face detection error: ' + e);
    return false; // 检测失败,允许切换
  }
}

然后在传感器回调中增加判断:

if (currentCameraIndex === 1 && z < -4.0) { // 前置状态,设备朝下
  const faceExists = await hasFaceInPreview();
  if (faceExists) {
    return; // 有人脸,不切换
  }
  switchCamera(0);
}

实际开发提示:每帧获取PixelMap性能开销大,建议只在前置状态下间隔2秒检测一次,或者利用已有预览Surface的ImageReceiver捕获帧。这里给出的是示意代码,完整实现需要配合image.ImageReceiver

4.6 步骤五:平滑过渡

切换摄像头时,预览会中断一小段时间(几帧)。为了让用户感觉“顺滑”,可以在切换前后加一个淡入淡出的动画:

// 假设你有一个xComponent组件用于预览
@State previewOpacity: number = 1.0;

async function smoothSwitch(targetIndex: number): Promise<void> {
  // 1. 渐隐
  animateTo({ duration: 150, curve: Curve.EaseInOut }, () => {
    this.previewOpacity = 0.0;
  });
  await new Promise(resolve => setTimeout(resolve, 170)); // 等待动画完成
  // 2. 切换相机
  await switchCamera(targetIndex);
  // 3. 渐显
  animateTo({ duration: 150, curve: Curve.EaseInOut }, () => {
    this.previewOpacity = 1.0;
  });
}

5. 完整代码整合

下面是一个完整的@Entry组件,包含上述所有步骤。使用时需要提前创建XComponent并获取surfaceId

import { camera } from '@kit.CameraKit';
import { sensor } from '@kit.SensorServiceKit';
import { faceDetection } from '@kit.AIKit';
import { image } from '@kit.ImageKit';
import { common, abilityAccessCtrl } from '@kit.AbilityKit';

@Entry
@Component
struct AutoSwitchCameraDemo {
  @State previewOpacity: number = 1.0;
  @State currentCameraName: string = '后置';

  private cameraManager: camera.CameraManager;
  private cameras: camera.CameraDevice[] = [];
  private currentCameraIndex: number = 0;
  private isSwitching: boolean = false;
  private surfaceId: string = ''; // 由XComponent提供

  aboutToAppear() {
    this.init();
  }

  async init() {
    const context = getContext(this) as common.UIAbilityContext;
    // 权限
    const atManager = abilityAccessCtrl.createAtManager();
    await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA', 'ohos.permission.ACCELEROMETER']);
    // 相机
    this.cameraManager = camera.getCameraManager(context);
    this.cameras = this.cameraManager.getSupportedCameras();
    if (this.cameras.length < 2) {
      console.warn('Only one camera device.');
    }
    this.currentCameraIndex = 0;
    await this.setupPreview(0);
    // 传感器监听
    this.startSensorListener();
  }

  async setupPreview(index: number): Promise<void> {
    const cameraInput = this.cameraManager.createCameraInput(this.cameras[index]);
    const session = this.cameraManager.createCaptureSession();
    session.beginConfig();
    session.addInput(cameraInput);
    // 假设surfaceId已通过XComponent的onAreaChange等回调赋值
    const previewOutput = this.cameraManager.createPreviewOutput(this.surfaceId);
    session.addOutput(previewOutput);
    await session.commitConfig();
    await session.start();
  }

  startSensorListener() {
    sensor.on(sensor.SensorType.ACCELEROMETER, (data: sensor.AccelerometerResponse) => {
      const z = data.z;
      const now = Date.now();
      if (now - this.lastFlipTime < 500) return;
      let targetIndex = -1;
      if (z < -4.0 && this.currentCameraIndex === 1) { // 朝下+前置→后置
        targetIndex = 0;
      } else if (z > 4.0 && this.currentCameraIndex === 0) { // 朝上+后置→前置
        targetIndex = 1;
      }
      if (targetIndex !== -1) {
        this.smoothSwitch(targetIndex);
        this.lastFlipTime = now;
      }
    });
  }

  lastFlipTime: number = 0;

  async smoothSwitch(targetIndex: number) {
    // 可增加人脸检测,这里省略以保持简洁
    animateTo({ duration: 150, curve: Curve.EaseInOut }, () => {
      this.previewOpacity = 0.0;
    });
    await new Promise(resolve => setTimeout(resolve, 170));
    await this.switchCamera(targetIndex);
    animateTo({ duration: 150, curve: Curve.EaseInOut }, () => {
      this.previewOpacity = 1.0;
    });
  }

  async switchCamera(targetIndex: number) {
    if (this.isSwitching || targetIndex === this.currentCameraIndex) return;
    this.isSwitching = true;
    try {
      // 释放当前会话,重新创建
      await this.setupPreview(targetIndex);
      this.currentCameraIndex = targetIndex;
      this.currentCameraName = targetIndex === 0 ? '后置' : '前置';
    } catch (e) {
      console.error('Switch failed: ' + e);
    } finally {
      this.isSwitching = false;
    }
  }

  build() {
    Column() {
      XComponent({
        id: 'cameraPreview',
        type: 'surface',
        controller: new XComponentController()
      })
        .onLoad((controller) => {
          this.surfaceId = controller.getXComponentSurfaceId() as string;
          // 此时可以开始预览
          this.setupPreview(this.currentCameraIndex);
        })
        .width('100%')
        .height('80%')
        .opacity(this.previewOpacity)

      Text('当前摄像头:' + this.currentCameraName)
        .fontSize(20)
        .margin(20)
    }
    .width('100%')
    .height('100%')
  }

  aboutToDisappear() {
    sensor.off(sensor.SensorType.ACCELEROMETER);
  }
}

6. 踩坑章节

坑1:switchCamera调用后预览黑屏或卡死

现象:调用cameraManager.switchCamera(newCamera)后,预览画面黑屏,或者回调一直不触发,最终超时。

原因switchCamera是一个异步操作,但它不会等待当前场景的输出释放。如果当前会话正在start状态,直接切换会导致资源冲突。官方文档建议:切换前必须先释放当前会话session.release()),然后重新创建新的会话和输出。上面代码中直接使用了setupPreview,内部每次都重新beginConfigaddInputcommitConfigstart,实际上相当于重新创建了会话,避免了这个问题。

解决方案:不要复用同一个CaptureSession对象,每次切换都新建会话(注意释放旧的输入和输出)。上面的setupPreview方法每调用一次都会创建新的CameraInputCaptureSession,旧的对象会被垃圾回收。如果希望更精细控制,可以维护一个currentSession变量,在切换前调用currentSession.release()

坑2:传感器临界抖动导致频繁切换

现象:手机平放或翻转过程中,z轴数值在阈值附近来回跳动,一秒内触发多次切换,UI闪烁。

原因:加速度传感器有噪声,在翻转的中间时刻数值不稳定

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐