HarmonyOS开发中的多摄像头:多摄像头切换、前后摄像头切换、双摄协同、多摄预览、摄像头能力查询

核心要点:掌握 HarmonyOS 多摄像头管理机制,实现前后摄像头无缝切换、双摄协同拍摄、多摄同时预览,以及摄像头能力动态查询,打造专业级多摄相机应用。

一、背景与动机

你有没有想过,为什么现在手机后面要放三颗、四颗甚至五颗摄像头?

想象一下这个场景:你正在旅行,面前是一片壮丽的雪山风景。你掏出手机想拍下来——如果只有一颗摄像头,要么拍不全(广角不够),要么拍不清晰(长焦拉胯)。但有了超广角+主摄+长焦的组合,你就能轻松应对各种拍摄场景。

再想想视频通话的场景。你正用前置摄像头和朋友聊天,突然想给对方看看你身边的小猫,这时候就需要前后摄像头无缝切换——画面不能中断,切换要丝滑。

还有更酷的:双摄协同。主摄拍高清画面的同时,副摄采集景深信息,实时虚化背景,让你用手机也能拍出单反级的虚化效果。

HarmonyOS 提供了完整的 Camera Kit 多摄管理能力,让我们可以灵活地查询、切换、协同多个摄像头。今天就来深入聊聊这些能力怎么用。


二、核心原理

2.1 多摄像头架构总览

HarmonyOS 的相机框架采用了分层架构,从底层硬件抽象到上层应用接口,层层封装:

flowchart TB
    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#fff
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000

    A[应用层 ArkTS]:::primary --> B[Camera Kit API]:::primary
    B --> C[Camera Manager]:::info
    C --> D[Camera Device]:::info
    D --> E[Camera Input]:::warning
    E --> F[Preview Output]:::warning
    E --> G[Photo Output]:::warning
    E --> H[Video Output]:::warning
    D --> I[多摄像头管理]:::purple
    I --> I1[前置摄像头]:::purple
    I --> I2[后置主摄]:::purple
    I --> I3[后置超广角]:::purple
    I --> I4[后置长焦]:::purple
    F --> J[XComponent 渲染]:::error

2.2 摄像头切换原理

摄像头切换的核心流程是:释放当前摄像头 → 打开目标摄像头 → 重新建立数据流。但这个过程如果处理不好,就会出现画面闪烁、黑屏等问题。HarmonyOS 提供了流复用机制,可以在切换时尽量复用已有的输出流,减少重建开销。

2.3 双摄协同原理

双摄协同是指同时打开两个摄像头,各自输出数据流,然后在应用层进行合成处理。典型场景包括:

  • 景深虚化:主摄 + 景深摄像头,主摄提供画面,景深摄像头提供深度图
  • 广角+长焦同框:超广角 + 长焦,实现画中画效果
  • 前后同拍:前置 + 后置同时预览,适合 Vlog 拍摄

2.4 摄像头能力查询

不同设备的摄像头配置差异很大——有的只有前后两颗,有的有四颗。在开发多摄应用时,必须先查询设备支持哪些摄像头及其能力,而不是硬编码。Camera Manager 提供了完整的查询接口。


三、代码实战

3.1 摄像头能力查询:摸清家底

在开始任何相机操作之前,第一步永远是查询设备有哪些摄像头、每个摄像头支持什么能力。

import { camera } from '@kit.CameraKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 摄像头能力查询工具类
 * 负责查询设备上所有摄像头的详细信息和支持的能力
 */
class CameraCapabilityQuery {
  private cameraManager: camera.CameraManager | null = null;

  /**
   * 初始化相机管理器
   */
  async initCameraManager(): Promise<void> {
    try {
      this.cameraManager = camera.getCameraManager(getContext(this));
      console.info('[CameraQuery] 相机管理器初始化成功');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[CameraQuery] 初始化失败: ${err.code} - ${err.message}`);
    }
  }

  /**
   * 查询所有摄像头设备
   * 返回设备上可用的摄像头列表
   */
  async queryAllCameras(): Promise<camera.CameraDevice[]> {
    if (!this.cameraManager) {
      await this.initCameraManager();
    }

    try {
      const cameras = this.cameraManager!.getSupportedCameras();
      console.info(`[CameraQuery] 检测到 ${cameras.length} 个摄像头`);

      // 遍历打印每个摄像头的信息
      cameras.forEach((cam, index) => {
        console.info(`[CameraQuery] 摄像头 ${index}:`);
        console.info(`  - ID: ${cam.cameraId}`);
        console.info(`  - 位置: ${this.getCameraPositionLabel(cam.cameraPosition)}`);
        console.info(`  - 类型: ${this.getCameraTypeLabel(cam.cameraType)}`);
        console.info(`  - 连接类型: ${cam.connectionType}`);
      });

      return cameras;
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[CameraQuery] 查询摄像头失败: ${err.code} - ${err.message}`);
      return [];
    }
  }

  /**
   * 查询指定摄像头的输出能力
   * 包括支持的预览尺寸、拍照尺寸、视频尺寸等
   */
  queryCameraOutputCapability(cameraDevice: camera.CameraDevice): camera.CameraOutputCapability | null {
    if (!this.cameraManager) return null;

    try {
      const capability = this.cameraManager.getSupportedOutputCapability(cameraDevice);
      console.info(`[CameraQuery] 摄像头 ${cameraDevice.cameraId} 的输出能力:`);

      // 预览能力
      const previewProfiles = capability.previewProfiles;
      console.info(`  - 支持预览配置: ${previewProfiles.length} 种`);
      previewProfiles.forEach((profile, i) => {
        console.info(`    [${i}] ${profile.size.width}x${profile.size.height}, FPS: ${profile.frameRateRange.min}-${profile.frameRateRange.max}`);
      });

      // 拍照能力
      const photoProfiles = capability.photoProfiles;
      console.info(`  - 支持拍照配置: ${photoProfiles.length} 种`);

      // 视频能力
      const videoProfiles = capability.videoProfiles;
      console.info(`  - 支持视频配置: ${videoProfiles.length} 种`);

      return capability;
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[CameraQuery] 查询输出能力失败: ${err.code} - ${err.message}`);
      return null;
    }
  }

  /**
   * 判断是否支持双摄协同
   * 检查设备是否可以同时打开多个摄像头
   */
  async checkDualCameraSupport(): Promise<boolean> {
    if (!this.cameraManager) return false;

    try {
      const cameras = this.cameraManager.getSupportedCameras();
      // 至少需要两个摄像头才可能支持双摄
      if (cameras.length < 2) {
        console.info('[CameraQuery] 设备摄像头数量不足,不支持双摄协同');
        return false;
      }

      // 尝试同时创建两个 camera input 来验证
      // 实际支持情况需要通过 isCameraMuted 等接口或实际打开来确认
      const hasFront = cameras.some(c => c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_FRONT);
      const hasBack = cameras.some(c => c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK);

      if (hasFront && hasBack) {
        console.info('[CameraQuery] 设备同时拥有前后摄像头,可能支持双摄协同');
        return true;
      }

      // 检查是否有多个后置摄像头
      const backCameras = cameras.filter(c => c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK);
      if (backCameras.length >= 2) {
        console.info('[CameraQuery] 设备拥有多个后置摄像头,可能支持双摄协同');
        return true;
      }

      return false;
    } catch (error) {
      console.error('[CameraQuery] 双摄支持检查失败');
      return false;
    }
  }

  /** 获取摄像头位置标签 */
  private getCameraPositionLabel(position: camera.CameraPosition): string {
    const labels: Map<number, string> = new Map([
      [camera.CameraPosition.CAMERA_POSITION_FRONT, '前置'],
      [camera.CameraPosition.CAMERA_POSITION_BACK, '后置'],
      [camera.CameraPosition.CAMERA_POSITION_UNSPECIFIED, '未指定'],
    ]);
    return labels.get(position) ?? '未知';
  }

  /** 获取摄像头类型标签 */
  private getCameraTypeLabel(type: camera.CameraType): string {
    const labels: Map<number, string> = new Map([
      [camera.CameraType.CAMERA_TYPE_WIDE_ANGLE, '广角'],
      [camera.CameraType.CAMERA_TYPE_ULTRA_WIDE, '超广角'],
      [camera.CameraType.CAMERA_TYPE_TELEPHOTO, '长焦'],
      [camera.CameraType.CAMERA_TYPE_TRUE_DEPTH, '深感'],
      [camera.CameraType.CAMERA_TYPE_DEFAULT, '默认'],
    ]);
    return labels.get(type) ?? '未知';
  }
}

3.2 前后摄像头切换:丝滑不卡顿

这是最常见的需求——视频通话时切换前后摄像头,要求画面不中断、切换流畅。

import { camera } from '@kit.CameraKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 摄像头切换管理器
 * 实现前后摄像头无缝切换,支持切换动画和状态回调
 */
class CameraSwitchManager {
  private cameraManager: camera.CameraManager | null = null;
  private currentCameraInput: camera.CameraInput | null = null;
  private currentCameraDevice: camera.CameraDevice | null = null;
  private previewOutput: camera.PreviewOutput | null = null;
  private photoOutput: camera.PhotoOutput | null = null;
  private captureSession: camera.CaptureSession | null = null;
  private surfaceId: string = '';
  private isSwitching: boolean = false;

  /** 切换状态回调 */
  onSwitchStart?: () => void;
  onSwitchComplete?: (isFront: boolean) => void;
  onSwitchError?: (error: BusinessError) => void;

  /**
   * 初始化相机管理器和会话
   */
  async init(surfaceId: string): Promise<void> {
    this.surfaceId = surfaceId;

    try {
      // 获取相机管理器
      this.cameraManager = camera.getCameraManager(getContext(this));

      // 默认打开后置摄像头
      const cameras = this.cameraManager.getSupportedCameras();
      const backCamera = cameras.find(
        c => c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK
      );

      if (backCamera) {
        await this.openCamera(backCamera);
        console.info('[CameraSwitch] 初始化完成,默认使用后置摄像头');
      }
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[CameraSwitch] 初始化失败: ${err.code} - ${err.message}`);
    }
  }

  /**
   * 打开指定摄像头并建立预览流
   */
  private async openCamera(cameraDevice: camera.CameraDevice): Promise<void> {
    if (!this.cameraManager) return;

    // 创建 Camera Input
    this.currentCameraInput = this.cameraManager.createCameraInput(cameraDevice);
    await this.currentCameraInput.open();
    this.currentCameraDevice = cameraDevice;

    // 获取输出能力
    const outputCapability = this.cameraManager.getSupportedOutputCapability(cameraDevice);

    // 创建预览输出
    const previewProfile = outputCapability.previewProfiles[0];
    this.previewOutput = this.cameraManager.createPreviewOutput(previewProfile, this.surfaceId);

    // 创建拍照输出
    const photoProfile = outputCapability.photoProfiles[0];
    this.photoOutput = this.cameraManager.createPhotoOutput(photoProfile);

    // 创建捕获会话并配置
    this.captureSession = this.cameraManager.createCaptureSession();
    this.captureSession.beginConfig();
    this.captureSession.addInput(this.currentCameraInput);
    this.captureSession.addOutput(this.previewOutput);

    // 提交配置并启动
    await this.captureSession.commitConfig();
    await this.captureSession.start();

    console.info(`[CameraSwitch] 摄像头 ${cameraDevice.cameraId} 已打开并开始预览`);
  }

  /**
   * 切换前后摄像头
   * 核心流程:停止会话 → 释放旧输入 → 创建新输入 → 重新配置会话
   */
  async switchCamera(): Promise<void> {
    // 防止重复切换
    if (this.isSwitching) {
      console.warn('[CameraSwitch] 正在切换中,请勿重复操作');
      return;
    }

    if (!this.cameraManager || !this.currentCameraDevice || !this.captureSession) {
      console.error('[CameraSwitch] 相机未初始化');
      return;
    }

    this.isSwitching = true;
    this.onSwitchStart?.();

    try {
      // 确定目标摄像头
      const isCurrentFront = this.currentCameraDevice.cameraPosition === camera.CameraPosition.CAMERA_POSITION_FRONT;
      const targetPosition = isCurrentFront
        ? camera.CameraPosition.CAMERA_POSITION_BACK
        : camera.CameraPosition.CAMERA_POSITION_FRONT;

      const cameras = this.cameraManager.getSupportedCameras();
      const targetCamera = cameras.find(c => c.cameraPosition === targetPosition);

      if (!targetCamera) {
        console.error('[CameraSwitch] 找不到目标摄像头');
        this.isSwitching = false;
        return;
      }

      console.info(`[CameraSwitch] 开始切换: ${isCurrentFront ? '前置→后置' : '后置→前置'}`);

      // 第一步:停止当前会话
      await this.captureSession.stop();

      // 第二步:开始重新配置
      this.captureSession.beginConfig();

      // 第三步:移除旧输入
      if (this.currentCameraInput) {
        this.captureSession.removeInput(this.currentCameraInput);
        this.currentCameraInput.close();
      }

      // 第四步:创建新输入
      this.currentCameraInput = this.cameraManager.createCameraInput(targetCamera);
      await this.currentCameraInput.open();
      this.currentCameraDevice = targetCamera;
      this.captureSession.addInput(this.currentCameraInput);

      // 第五步:提交配置并启动
      await this.captureSession.commitConfig();
      await this.captureSession.start();

      const isNowFront = targetPosition === camera.CameraPosition.CAMERA_POSITION_FRONT;
      this.onSwitchComplete?.(isNowFront);
      console.info('[CameraSwitch] 切换完成');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[CameraSwitch] 切换失败: ${err.code} - ${err.message}`);
      this.onSwitchError?.(err);

      // 切换失败时尝试恢复
      try {
        await this.captureSession.start();
      } catch (e) {
        console.error('[CameraSwitch] 恢复预览失败');
      }
    } finally {
      this.isSwitching = false;
    }
  }

  /**
   * 获取当前是否为前置摄像头
   */
  get isFrontCamera(): boolean {
    return this.currentCameraDevice?.cameraPosition === camera.CameraPosition.CAMERA_POSITION_FRONT;
  }

  /**
   * 释放所有资源
   */
  async release(): Promise<void> {
    try {
      if (this.captureSession) {
        await this.captureSession.stop();
        this.captureSession.release();
      }
      if (this.currentCameraInput) {
        this.currentCameraInput.close();
      }
      if (this.previewOutput) {
        this.previewOutput.release();
      }
      if (this.photoOutput) {
        this.photoOutput.release();
      }
      console.info('[CameraSwitch] 资源已释放');
    } catch (error) {
      console.error('[CameraSwitch] 释放资源失败');
    }
  }
}

3.3 双摄协同:景深虚化效果

双摄同时工作,一个拍画面一个拍景深,实时合成虚化效果。这是手机人像模式的核心技术。

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

/**
 * 双摄协同管理器
 * 同时管理主摄和景深摄像头,实现实时景深虚化
 */
class DualCameraManager {
  private cameraManager: camera.CameraManager | null = null;
  private mainCameraInput: camera.CameraInput | null = null;
  private depthCameraInput: camera.CameraInput | null = null;
  private mainPreviewOutput: camera.PreviewOutput | null = null;
  private depthPreviewOutput: camera.PreviewOutput | null = null;
  private captureSession: camera.CaptureSession | null = null;

  /** 虚化强度 0-1 */
  private blurIntensity: number = 0.5;

  /**
   * 初始化双摄协同
   * @param mainSurfaceId 主摄预览 Surface ID
   * @param depthSurfaceId 景深摄预览 Surface ID
   */
  async init(mainSurfaceId: string, depthSurfaceId: string): Promise<boolean> {
    try {
      this.cameraManager = camera.getCameraManager(getContext(this));
      const cameras = this.cameraManager.getSupportedCameras();

      // 查找后置主摄
      const mainCamera = cameras.find(c =>
        c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK &&
        c.cameraType === camera.CameraType.CAMERA_TYPE_WIDE_ANGLE
      );

      // 查找景深摄像头
      const depthCamera = cameras.find(c =>
        c.cameraType === camera.CameraType.CAMERA_TYPE_TRUE_DEPTH
      );

      if (!mainCamera) {
        console.error('[DualCamera] 找不到后置主摄');
        return false;
      }

      // 创建主摄输入
      this.mainCameraInput = this.cameraManager.createCameraInput(mainCamera);
      await this.mainCameraInput.open();

      // 获取主摄输出能力并创建预览
      const mainCapability = this.cameraManager.getSupportedOutputCapability(mainCamera);
      const mainPreviewProfile = mainCapability.previewProfiles[0];
      this.mainPreviewOutput = this.cameraManager.createPreviewOutput(mainPreviewProfile, mainSurfaceId);

      // 创建捕获会话
      this.captureSession = this.cameraManager.createCaptureSession();
      this.captureSession.beginConfig();
      this.captureSession.addInput(this.mainCameraInput);
      this.captureSession.addOutput(this.mainPreviewOutput);

      // 如果有景深摄像头,添加到同一会话
      if (depthCamera) {
        this.depthCameraInput = this.cameraManager.createCameraInput(depthCamera);
        await this.depthCameraInput.open();

        const depthCapability = this.cameraManager.getSupportedOutputCapability(depthCamera);
        const depthPreviewProfile = depthCapability.previewProfiles[0];
        this.depthPreviewOutput = this.cameraManager.createPreviewOutput(depthPreviewProfile, depthSurfaceId);

        this.captureSession.addInput(this.depthCameraInput);
        this.captureSession.addOutput(this.depthPreviewOutput);

        console.info('[DualCamera] 双摄协同模式已启用');
      } else {
        console.warn('[DualCamera] 未找到景深摄像头,仅使用单摄模式');
      }

      await this.captureSession.commitConfig();
      await this.captureSession.start();

      return true;
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[DualCamera] 初始化失败: ${err.code} - ${err.message}`);
      return false;
    }
  }

  /**
   * 设置虚化强度
   * @param intensity 虚化强度 0.0 - 1.0
   */
  setBlurIntensity(intensity: number): void {
    this.blurIntensity = Math.max(0, Math.min(1, intensity));
    console.info(`[DualCamera] 虚化强度设置为: ${this.blurIntensity}`);
  }

  /**
   * 获取当前虚化强度
   */
  getBlurIntensity(): number {
    return this.blurIntensity;
  }

  /**
   * 拍照并合成虚化效果
   * 将主摄照片与景深信息合成,生成虚化照片
   */
  async captureWithBlur(): Promise<image.PixelMap | null> {
    if (!this.mainPreviewOutput) {
      console.error('[DualCamera] 预览未启动');
      return null;
    }

    try {
      // 这里简化处理,实际需要通过 PhotoOutput 获取照片
      // 然后结合景深数据进行虚化处理
      console.info('[DualCamera] 正在拍摄虚化照片...');

      // 实际项目中,虚化处理通常使用 NDK 或 GPU 算子
      // 此处仅展示流程框架

      return null; // 返回合成后的 PixelMap
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[DualCamera] 拍照失败: ${err.code} - ${err.message}`);
      return null;
    }
  }

  /**
   * 释放双摄资源
   */
  async release(): Promise<void> {
    try {
      if (this.captureSession) {
        await this.captureSession.stop();
        this.captureSession.release();
      }
      if (this.mainCameraInput) {
        this.mainCameraInput.close();
      }
      if (this.depthCameraInput) {
        this.depthCameraInput.close();
      }
      console.info('[DualCamera] 资源已释放');
    } catch (error) {
      console.error('[DualCamera] 释放资源失败');
    }
  }
}

3.4 多摄预览 UI 组件:画中画效果

在 Vlog 场景中,经常需要同时预览前后摄像头——大画面显示后置,小窗口显示前置。

import { camera } from '@kit.CameraKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct MultiCameraPreviewPage {
  // 主预览 Surface ID(后置)
  private mainSurfaceId: string = '';
  // 小窗预览 Surface ID(前置)
  private pipSurfaceId: string = '';
  private cameraManager: camera.CameraManager | null = null;
  private captureSession: camera.CaptureSession | null = null;
  private isRecording: boolean = false;
  private showPip: boolean = true;

  @State currentMode: string = '后置主摄';
  @State isFrontMain: boolean = false; // 前置是否为主画面

  aboutToAppear(): void {
    this.initCameras();
  }

  /**
   * 初始化双摄预览
   */
  async initCameras(): Promise<void> {
    try {
      this.cameraManager = camera.getCameraManager(getContext(this));
      const cameras = this.cameraManager.getSupportedCameras();

      // 后置主摄
      const backCamera = cameras.find(c =>
        c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK
      );
      // 前置摄像头
      const frontCamera = cameras.find(c =>
        c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_FRONT
      );

      if (!backCamera || !frontCamera) {
        console.error('[MultiPreview] 设备不支持前后双摄');
        return;
      }

      // 创建后置输入和预览
      const backInput = this.cameraManager.createCameraInput(backCamera);
      await backInput.open();

      const backCapability = this.cameraManager.getSupportedOutputCapability(backCamera);
      const backPreviewProfile = backCapability.previewProfiles[0];

      // 创建前置输入和预览
      const frontInput = this.cameraManager.createCameraInput(frontCamera);
      await frontInput.open();

      const frontCapability = this.cameraManager.getSupportedOutputCapability(frontCamera);
      const frontPreviewProfile = frontCapability.previewProfiles[0];

      // 创建双摄会话
      this.captureSession = this.cameraManager.createCaptureSession();
      this.captureSession.beginConfig();
      this.captureSession.addInput(backInput);
      this.captureSession.addInput(frontInput);

      // 等待 Surface ID 就绪后再创建输出
      // 注意:实际开发中需要确保 XComponent 的 surfaceId 已获取
      console.info('[MultiPreview] 双摄预览初始化完成');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[MultiPreview] 初始化失败: ${err.code} - ${err.message}`);
    }
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('多摄预览')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        Blank()
        Text(this.currentMode)
          .fontSize(14)
          .fontColor('#AAAAAA')
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 12 })

      // 主预览区域
      Stack() {
        // 后置主摄预览(大画面)
        XComponent({
          id: 'mainCameraPreview',
          type: XComponentType.SURFACE,
          libraryname: ''
        })
          .width('100%')
          .aspectRatio(3 / 4)
          .borderRadius(12)
          .onLoad(() => {
            // 获取 surfaceId 并启动预览
            console.info('[MultiPreview] 主预览 XComponent 加载完成');
          })

        // 前置小窗预览(画中画)
        if (this.showPip) {
          Column() {
            XComponent({
              id: 'pipCameraPreview',
              type: XComponentType.SURFACE,
              libraryname: ''
            })
              .width(120)
              .height(160)
              .borderRadius(8)
              .border({ width: 2, color: '#FFFFFF' })

            // 小窗切换按钮
            Button() {
              Text('切换')
                .fontSize(10)
                .fontColor('#FFFFFF')
            }
            .width(50)
            .height(24)
            .backgroundColor('#80000000')
            .borderRadius(12)
            .margin({ top: 4 })
            .onClick(() => {
              this.isFrontMain = !this.isFrontMain;
              this.currentMode = this.isFrontMain ? '前置主摄' : '后置主摄';
            })
          }
          .position({ x: '70%', y: 16 })
          .alignItems(HorizontalAlign.Center)
        }
      }
      .width('100%')
      .layoutWeight(1)

      // 底部控制栏
      Row() {
        // 画中画开关
        Button() {
          Text(this.showPip ? '关闭画中画' : '开启画中画')
            .fontSize(14)
            .fontColor('#FFFFFF')
        }
        .backgroundColor(this.showPip ? '#4FC3F7' : '#555555')
        .borderRadius(20)
        .onClick(() => {
          this.showPip = !this.showPip;
        })

        Blank()

        // 录制按钮
        Button() {
          Text(this.isRecording ? '停止' : '录制')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
        }
        .width(64)
        .height(64)
        .borderRadius(32)
        .backgroundColor(this.isRecording ? '#EF5350' : '#FFFFFF')
        .onClick(() => {
          this.isRecording = !this.isRecording;
        })

        Blank()

        // 前后切换
        Button() {
          Text('翻转')
            .fontSize(14)
            .fontColor('#FFFFFF')
        }
        .backgroundColor('#555555')
        .borderRadius(20)
        .onClick(() => {
          this.isFrontMain = !this.isFrontMain;
          this.currentMode = this.isFrontMain ? '前置主摄' : '后置主摄';
        })
      }
      .width('100%')
      .padding({ left: 24, right: 24, top: 16, bottom: 32 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000000')
  }
}

四、踩坑与注意事项

4.1 切换时的黑屏闪烁

问题:前后摄像头切换时,画面会出现短暂黑屏或闪烁。

原因:切换过程需要释放旧摄像头、打开新摄像头、重建数据流,这个过程需要时间。

解决方案

  • 在切换开始时显示最后一帧的截图作为过渡
  • 使用 beginConfig()removeInput()addInput()commitConfig() 的方式复用会话,而非完全重建
  • 切换期间禁用按钮,防止用户连续点击

4.2 双摄同时打开的限制

问题:不是所有设备都支持同时打开两个摄像头。

原因:硬件 ISP 通道有限,低端设备可能只支持一个摄像头同时工作。

解决方案

  • 在打开第二个摄像头前,先通过 getSupportedCameras() 查询设备能力
  • 如果 createCameraInput() 抛出异常,说明不支持双摄,需要降级为单摄模式
  • 在 UI 上给出明确提示,不要让用户误以为功能坏了

4.3 Surface ID 时序问题

问题:创建 PreviewOutput 时需要 Surface ID,但 XComponent 的 onLoad 回调可能还没执行。

解决方案

  • XComponent.onLoad() 回调中获取 surfaceId 并保存
  • 确保在 surfaceId 获取后再创建 PreviewOutput
  • 使用 Promise 或回调机制保证时序正确

4.4 摄像头权限与隐私

问题:多摄应用需要 CAMERA 权限,但用户可能拒绝。

解决方案

  • module.json5 中声明 ohos.permission.CAMERA 权限
  • 运行时通过 abilityAccessCtrl 请求权限
  • 权限被拒绝时,展示友好的引导页面,说明为什么需要这个权限

4.5 后台切换摄像头

问题:应用切换到后台再回来,摄像头可能已经释放。

解决方案

  • onPageShow() 中重新检查摄像头状态
  • 如果摄像头已释放,重新初始化
  • onPageHide() 中主动释放资源,避免后台占用摄像头

五、HarmonyOS 6 适配

5.1 API 变更

功能HarmonyOS 5HarmonyOS 6
相机管理器获取camera.getCameraManager()同上,但增加了异步版本camera.getCameraManagerAsync()
多摄同时预览通过CaptureSession 添加多个 Input新增MultiCameraSession 专用类,更简洁
摄像头能力查询getSupportedOutputCapability()新增getSupportedOutputCapabilityV2(),返回更详细的能力信息
切换回调无专用回调新增onCameraSwitchComplete 回调

5.2 迁移指南

// HarmonyOS 5 写法
const cameraManager = camera.getCameraManager(context);

// HarmonyOS 6 推荐写法
const cameraManager = await camera.getCameraManagerAsync(context);
// 异步获取,避免阻塞主线程

5.3 新增特性

  • 智能摄像头选择:HarmonyOS 6 新增 CameraSelector API,可以根据拍摄场景自动推荐最佳摄像头组合
  • 低延迟切换:切换延迟从 300ms 优化到 100ms 以内
  • 双摄同步增强:前后摄像头帧同步精度提升,适合双摄直播场景

六、总结

mindmap
  root((多摄像头开发))
    摄像头能力查询
      getSupportedCameras
      getSupportedOutputCapability
      摄像头位置与类型
      双摄支持检测
    前后摄像头切换
      释放旧输入
      创建新输入
      会话复用
      切换动画过渡
    双摄协同
      景深虚化
      前后同拍
      多输入多输出
      帧同步处理
    多摄预览
      画中画效果
      XComponent Surface
      主次画面切换
      录制控制
    踩坑要点
      黑屏闪烁
      双摄硬件限制
      Surface ID 时序
      权限与隐私
      后台状态处理

核心知识点回顾

  1. 能力查询是第一步:永远先查询设备有哪些摄像头、支持什么能力,不要硬编码摄像头 ID
  2. 切换要复用会话:通过 beginConfig()removeInput()addInput()commitConfig() 复用 CaptureSession,减少切换延迟
  3. 双摄需要硬件支持:不是所有设备都能同时开两个摄像头,要做好降级方案
  4. Surface ID 时序很重要:确保 XComponent 加载完成后再创建 PreviewOutput
  5. 权限和生命周期管理:正确处理权限请求、后台切换、资源释放

多摄像头开发是相机应用的核心能力之一。掌握了这些,你就能开发出像系统相机一样专业的多摄应用了!


蓝胖子样样好
79 声望702 粉丝

Never give up,and you will be successful