HarmonyOS鸿蒙Next中应用如何自定义相机实现录像功能?

HarmonyOS鸿蒙Next中应用如何自定义相机实现录像功能?

鸿蒙应用如何自定义相机实现录像功能?

4 回复

自定义相机录像 :

本文面向于相机应用开发场景,在相机应用中实现了基础视频录制功能。内容涵盖相机设备的创建与调用、录像的启动与停止、以及输出处理的完整流程,有效满足三方应用在不同硬件平台上对录像功能的开发需求。

基础录像

场景描述

录像功能是自定义相机应用的核心功能,提供实时预览和构图调整能力。通过点击界面上的录像按钮,用户即可开始视频录制。在录制过程中,相机应用会持续采集画面数据并将其保存为视频文件,用户可根据需要随时暂停或结束录制。

流程图:

cke_1063.png

1、申请相关权限 在开发相机应用时,需要先参考申请相关权限。

2、配置视频录制参数 通过相册管理模块 PhotoAccessHelper 创建一个视频资源,以便后续写入录像文件。 动态生成视频录制的配置profile。 音视频录制参数设置,包括采集源类型、编码方式、画质配置、保存路径等,为录制的启动和后续操作提供基础配置。

3、获取Surface 系统提供的media接口可以创建一个录像AVRecorder实例,通过该实例的getInputSurface()方法获取SurfaceId,用于后续录像输出流的关联,处理录像输出流的数据。

4、创建录像输出流 通过CameraOutputCapability模块中的videoProfiles属性,可获取当前设备支持的录像输出流配置,根据设备能力和目标配置,选择合适的视频配置。在当前示例中,演示了根据不同相机位置、图片质量去设置不同的分辨率和帧率,最后通过createVideoOutput()方法创建录像输出流。

5、启动录像 先通过videoOutput的start()方法启动录像输出流,再通过avRecorder的start()方法开始录像。如需实现前置摄像头录像功能,先通过isMirrorSupported()方法判断设备是否支持镜像功能;如果支持且当前为前置摄像头,则调用enableMirror()方法开启镜像效果。

6、暂停录像 先通过avRecorder的pause()方法暂停录像,再通过videoOutput的stop()方法停止录像输出流。

7、恢复录像 先通过videoOutput的start()方法启动录像输出流,再通过avRecorder的resume()方法恢复录像。

8、停止录像 先通过avRecorder的stop()方法停止录像,再通过videoOutput的stop()方法停止录像输出流。

9、释放资源 先通过avRecorder的release()方法释放录像资源,再通过videoOutput的release()方法释放输出流。

设置录像旋转角度 录像的旋转角度与重力方向(即设备旋转角度)相关。调用VideoOutput类中的getVideoRotation()可以获取到录像的旋转角度。详细请参见适配相机旋转角度(ArkTS) 。 deviceDegree:设备旋转角度。获取方式请见计算设备旋转角度。

cke_3214.png

项目地址:https://gitee.com/harmonyos_samples/CustomCamera

更多关于HarmonyOS鸿蒙Next中应用如何自定义相机实现录像功能?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


一、结论

大体开发思路与相机拍照思路相同,都是通过CameraKit进行相机设备调用,视频输入输出流和会话的绑定,在会话里设置相关参数。

唯一与拍照不同的区别是,通过avRecorder进行录像生命周期的管理,开始录像,暂停录像都是通过这个渲染播放对象进行管控。相对于拍照而言,录像的开发要麻烦一些。

自定义相机录像开发思路步骤为: 1.设置相机界面

2.选择相机摄像头实例 根据cameraKit提供的CameraManager获取相机管理实例,拿到设备的相机列表,一般分为前后两个。选择你要用的相机。

3.相机输出流 传入你选择的相机实例给cameraManager.createCameraInput创建相机输出流,开启会话后cameraInput.open,进行相机的参数配置(拍照还是摄像模式,闪光灯,焦距比)

4.配置相机会话信息 创建会话,将输入流,输出流,拍照,摄像,绑定到会话上。完事之后,开启会话,相机流就能正常输出。你去操作拍照摄像也可以正常操作。你可以理解会话为一个组合器。

5.录像操作 使用avRecorder进行录像的开启,暂停,释放的处理。

6.退出界面销毁相机 相机资源不释放,会导致再次初始化黑屏,甚至影响系统相机等异常问题。

二、代码实现和详细解释

/**
 * 相机管理类(单例模式)
 * 功能:封装视频相机的初始化、录像控制、资源释放等核心能力
 * 依赖:CameraKit、ImageKit、MediaKit、AbilityKit、MediaLibraryKit、CoreFileKit等系统套件
 */
import { camera } from '[@kit](/user/kit).CameraKit';
import { image } from '[@kit](/user/kit).ImageKit';
import { BusinessError } from '[@kit](/user/kit).BasicServicesKit';
import { media } from '[@kit](/user/kit).MediaKit';
import { common } from '[@kit](/user/kit).AbilityKit';
import { photoAccessHelper } from '[@kit](/user/kit).MediaLibraryKit';
import { fileIo as fs } from '[@kit](/user/kit).CoreFileKit';

export class CameraMgr {
  // 日志标签,用于控制台输出标识
  private TAG: string = "CameraMgr";

  // 单例实例,保证全局唯一的相机管理对象
  private static mCameraMgr: CameraMgr | null = null;

  // 相机输入流对象,用于获取相机硬件的输入数据
  private mCameraInput: camera.CameraInput | undefined = undefined;
  // 预览输出流对象,用于相机画面预览展示
  private mPreviewOutput: camera.PreviewOutput | undefined = undefined;

  // 视频会话对象,管理视频录制场景下的相机配置和数据流
  private mVideoSession: camera.VideoSession | undefined = undefined;
  // 视频输出流对象,用于将相机数据输出到录像编码器
  private mVideoOutput: camera.VideoOutput | undefined = undefined;
  // 音视频录制器对象,负责将视频/音频数据编码并写入文件
  private mAvRecorder: media.AVRecorder | undefined = undefined;

  // 录像文件句柄,用于操作录制后的视频文件
  private mFile: fs.File | null = null;

  /**
   * 获取单例实例(懒汉式)
   * [@returns](/user/returns) CameraMgr 单例对象
   */
  public static Ins(): CameraMgr {
    // 已存在实例则直接返回
    if (CameraMgr.mCameraMgr) {
      return CameraMgr.mCameraMgr
    }
    // 不存在则创建新实例
    CameraMgr.mCameraMgr = new CameraMgr();
    return CameraMgr.mCameraMgr
  }

  /**
   * 初始化视频相机(核心方法)
   * 流程:创建相机管理器→检查相机设备→配置录像参数→初始化录制器→创建会话/输入输出流→启动会话
   * [@param](/user/param) context 应用上下文,用于获取系统服务
   * [@param](/user/param) surfaceId 预览画面的Surface ID,绑定UI层的预览控件
   * [@returns](/user/returns) Promise<void> 异步初始化结果
   */
  public async initVideoCamera(context: common.Context, surfaceId: string): Promise<void> {
    // 1. 获取相机管理器实例,是操作相机的核心入口
    let cameraManager: camera.CameraManager = camera.getCameraManager(context);
    if (!cameraManager) {
      console.error("camera.getCameraManager error");
      return;
    }

    // 2. 监听相机状态变化(如打开/关闭/异常)
    cameraManager.on('cameraStatus', (err: BusinessError, cameraStatusInfo: camera.CameraStatusInfo) => {
      if (err !== undefined && err.code !== 0) {
        console.error('cameraStatus with errorCode = ' + err.code);
        return;
      }
      console.info(`camera : ${cameraStatusInfo.camera.cameraId}`); // 相机ID
      console.info(`status: ${cameraStatusInfo.status}`); // 相机状态(如ACTIVE/INACTIVE)
    });

    // 3. 获取设备支持的相机列表(前后置相机等)
    let cameraArray: Array<camera.CameraDevice> = [];
    try {
      cameraArray = cameraManager.getSupportedCameras();
    } catch (error) {
      let err = error as BusinessError;
      console.error(`getSupportedCameras call failed. error code: ${err.code}`);
    }

    // 无可用相机则终止初始化
    if (cameraArray.length <= 0) {
      console.error("cameraManager.getSupportedCameras error");
      return;
    }

    // 4. 检查相机是否支持视频录制模式
    let sceneModes: Array<camera.SceneMode> = cameraManager.getSupportedSceneModes(cameraArray[0]);
    let isSupportVideoMode: boolean = sceneModes.indexOf(camera.SceneMode.NORMAL_VIDEO) >= 0;
    if (!isSupportVideoMode) {
      console.error('video mode not support');
      return;
    }

    // 5. 获取相机输出能力(预览/拍照/录像的分辨率、格式等)
    let cameraOutputCap: camera.CameraOutputCapability =
      cameraManager.getSupportedOutputCapability(cameraArray[0], camera.SceneMode.NORMAL_VIDEO);
    if (!cameraOutputCap) {
      console.error("cameraManager.getSupportedOutputCapability error")
      return;
    }
    console.info("outputCapability: " + JSON.stringify(cameraOutputCap));

    // 提取预览/拍照/录像的配置文件列表
    let previewProfilesArray: Array<camera.Profile> = cameraOutputCap.previewProfiles;
    if (!previewProfilesArray) {
      console.error("createOutput previewProfilesArray == null || undefined");
    }

    let photoProfilesArray: Array<camera.Profile> = cameraOutputCap.photoProfiles;
    if (!photoProfilesArray) {
      console.error("createOutput photoProfilesArray == null || undefined");
    }

    let videoProfilesArray: Array<camera.VideoProfile> = cameraOutputCap.videoProfiles;
    if (!videoProfilesArray) {
      console.error("createOutput videoProfilesArray == null || undefined");
    }

    // 6. 配置录像分辨率(需与硬件支持的配置匹配)
    let videoSize: camera.Size = {
      width: 640,
      height: 480
    }
    // 从硬件支持的录像配置中匹配目标分辨率
    let videoProfile: undefined | camera.VideoProfile = videoProfilesArray.find((profile: camera.VideoProfile) => {
      return profile.size.width === videoSize.width && profile.size.height === videoSize.height;
    });
    if (!videoProfile) {
      console.error('videoProfile is not found');
      return;
    }

    // 7. 配置音视频录制参数(需与硬件编码能力匹配)
    let aVRecorderProfile: media.AVRecorderProfile = {
      audioBitrate: 48000,        // 音频比特率
      audioChannels: 2,           // 音频声道数(2=立体声)
      audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式(AAC)
      audioSampleRate: 48000,     // 音频采样率
      fileFormat: media.ContainerFormatType.CFT_MPEG_4, // 视频容器格式(MP4)
      videoBitrate: 2000000,      // 视频比特率(2Mbps)
      videoCodec: media.CodecMimeType.VIDEO_AVC, // 视频编码格式(H.264)
      videoFrameWidth: videoSize.width,  // 视频宽度
      videoFrameHeight: videoSize.height, // 视频高度
      videoFrameRate: 30          // 视频帧率(30fps)
    };

    // 8. 创建录像文件(存入系统相册)
    let options: photoAccessHelper.CreateOptions = {
      title: Date.now().toString() // 文件标题(用时间戳命名避免重复)
    };
    // 获取相册访问助手
    let accessHelper: photoAccessHelper.PhotoAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
    // 创建视频资产(返回文件URI)
    let videoUri: string = await accessHelper.createAsset(photoAccessHelper.PhotoType.VIDEO, 'mp4', options);
    // 打开文件(读写模式,不存在则创建)
    this.mFile = fs.openSync(videoUri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);

    // 9. 配置音视频录制器参数
    let aVRecorderConfig: media.AVRecorderConfig = {
      audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // 音频源(麦克风)
      videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, // 视频源(Surface YUV数据)
      profile: aVRecorderProfile, // 音视频编码配置
      url: `fd://${this.mFile.fd.toString()}`, // 文件句柄(fd://+文件描述符)
      rotation: 0, // 视频旋转角度(0/90/180/270)
      location: { latitude: 30, longitude: 130 } // 视频地理位置信息(可选)
    };

    // 10. 创建音视频录制器实例
    let avRecorder: media.AVRecorder | undefined = undefined;
    try {
      avRecorder = await media.createAVRecorder();
    } catch (error) {
      let err = error as BusinessError;
      console.error(`createAVRecorder call failed. error code: ${err.code}`);
    }
    if (avRecorder === undefined) {
      return;
    }
    this.mAvRecorder = avRecorder;

    // 初始化录制器(加载编码配置)
    try {
      await avRecorder.prepare(aVRecorderConfig);
    } catch (error) {
      let err = error as BusinessError;
      console.error(`prepare call failed. error code: ${err.code}`);
    }

    // 11. 获取录制器的输入Surface ID(用于绑定相机视频输出)
    let videoSurfaceId: string | undefined = undefined;
    try {
      videoSurfaceId = await avRecorder.getInputSurface();
    } catch (error) {
      let err = error as BusinessError;
      console.error(`getInputSurface call failed. error code: ${err.code}`);
    }
    if (videoSurfaceId === undefined) {
      return;
    }

    // 12. 创建视频输出流(绑定录制器Surface)
    let videoOutput: camera.VideoOutput | undefined = undefined;
    try {
      videoOutput = cameraManager.createVideoOutput(videoProfile, videoSurfaceId);
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to create the videoOutput instance. error: ${JSON.stringify(err)}`);
    }
    if (videoOutput === undefined) {
      return;
    }
    this.mVideoOutput = videoOutput;

    // 监听视频输出流错误
    videoOutput.on('error', (error: BusinessError) => {
      console.error(`Preview output error code: ${error.code}`);
    });

    // 13. 创建视频会话(管理视频录制的数据流)
    let videoSession: camera.VideoSession | undefined = undefined;
    try {
      videoSession = cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO) as camera.VideoSession;
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to create the session instance. error: ${JSON.stringify(err)}`);
    }
    if (videoSession === undefined) {
      return;
    }
    this.mVideoSession = videoSession;

    // 监听视频会话错误
    videoSession.on('error', (error: BusinessError) => {
      console.error(`Video session error code: ${error.code}`);
    });

    // 14. 开始配置视频会话(添加输入/输出流前必须调用)
    try {
      videoSession.beginConfig();
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to beginConfig. error: ${JSON.stringify(err)}`);
    }

    // 15. 创建相机输入流(绑定物理相机设备)
    let cameraInput: camera.CameraInput | undefined = undefined;
    try {
      cameraInput = cameraManager.createCameraInput(cameraArray[0]);
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to createCameraInput. error: ${JSON.stringify(err)}`);
    }
    if (cameraInput === undefined) {
      return;
    }
    this.mCameraInput = cameraInput;

    // 监听相机输入流错误
    let cameraDevice: camera.CameraDevice = cameraArray[0];
    cameraInput.on('error', cameraDevice, (error: BusinessError) => {
      console.error(`Camera input error code: ${error.code}`);
    });

    // 打开相机(启动硬件相机)
    try {
      await cameraInput.open();
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to open cameraInput. error: ${JSON.stringify(err)}`);
    }

    // 16. 向会话添加相机输入流(会话获取相机数据)
    try {
      videoSession.addInput(cameraInput);
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to add cameraInput. error: ${JSON.stringify(err)}`);
    }

    // 17. 创建预览输出流(绑定UI预览Surface)
    let previewOutput: camera.PreviewOutput | undefined = undefined;
    try {
      previewOutput = cameraManager.createPreviewOutput(previewProfilesArray[0], surfaceId);
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to create the PreviewOutput instance. error: ${JSON.stringify(err)}`);
    }
    if (previewOutput === undefined) {
      return;
    }
    this.mPreviewOutput = previewOutput;

    // 向会话添加预览输出流(会话输出预览画面)
    try {
      videoSession.addOutput(previewOutput);
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to add previewOutput. error: ${JSON.stringify(err)}`);
    }

    // 18. 向会话添加视频输出流(会话输出录像数据)
    try {
      videoSession.addOutput(videoOutput);
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to add videoOutput. error: ${JSON.stringify(err)}`);
    }

    // 19. 提交会话配置(使配置生效)
    try {
      await videoSession.commitConfig();
    } catch (error) {
      let err = error as BusinessError;
      console.error(`videoSession commitConfig error: ${JSON.stringify(err)}`);
    }

    // 20. 启动视频会话(开始预览和录像准备)
    try {
      await videoSession.start();
    } catch (error) {
      let err = error as BusinessError;
      console.error(`videoSession start error: ${JSON.stringify(err)}`);
    }
  }

  /**
   * 开始录像
   * 流程:启动视频输出流→启动音视频录制器
   */
  public async startRecord() {
    // 启动视频输出流(开始向录制器传输视频数据)
    this.mVideoOutput?.start((err: BusinessError) => {
      if (err) {
        console.error(`Failed to start the video output. error: ${JSON.stringify(err)}`);
        return;
      }
      console.info('Callback invoked to indicate the video output start success.');
    });

    // 启动音视频录制器(开始编码并写入文件)
    try {
      await this.mAvRecorder?.start();
    } catch (error) {
      let err = error as BusinessError;
      console.error(`avRecorder start error: ${JSON.stringify(err)}`);
    }
  }

  /**
   * 停止录像
   * 流程:停止视频输出流→停止音视频录制器
   */
  public async stopRecorder() {
    // 停止视频输出流(停止向录制器传输视频数据)
    this.mVideoOutput?.stop((err: BusinessError) => {
      if (err) {
        console.error(`Failed to stop the video output. error: ${JSON.stringify(err)}`);
        return;
      }
      console.info('Callback invoked to indicate the video output stop success.');
    });

    // 停止音视频录制器(停止编码并完成文件写入)
    try {
      await this.mAvRecorder?.stop();
    } catch (error) {
      let err = error as BusinessError;
      console.error(`avRecorder stop error: ${JSON.stringify(err)}`);
    }
  }

  /**
   * 销毁视频相机(释放所有资源)
   * 流程:停止会话→关闭文件→释放相机/预览/录像流→释放会话→置空对象
   */
  public async destroyVideoCamera() {
    // 停止当前视频会话(停止预览和录像)
    await this.mVideoSession?.stop();

    // 关闭录像文件(确保数据写入完成)
    if (this.mFile) {
      fs.closeSync(this.mFile);
      this.mFile = null;
    }

    // 关闭相机输入流(释放物理相机)
    await this.mCameraInput?.close();

    // 释放预览输出流(释放UI预览资源)
    await this.mPreviewOutput?.release();

    // 释放录像输出流(释放录制器绑定的资源)
    await this.mVideoOutput?.release();

    // 释放视频会话(释放会话管理的所有资源)
    await

在HarmonyOS Next中,使用CameraKit API实现自定义相机录像。首先,通过cameraManager.getSupportedCameras()获取相机列表并创建CameraInput。然后,使用captureSession配置视频输出,通过VideoOutput类设置录像参数,如分辨率与帧率。调用captureSession.start()启动预览,使用VideoOutput.start()开始录制。录像数据可通过文件路径或回调接口获取。整个过程需在UI主线程外处理,并注意权限声明与资源释放。

在HarmonyOS Next中,应用可以通过CameraKitAVRecorder等核心接口,自定义相机并实现录像功能。以下是关键步骤和代码示例:

1. 权限与配置

  • 声明权限:在module.json5中申请相机和麦克风权限。
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA"
      },
      {
        "name": "ohos.permission.MICROPHONE"
      }
    ]
  }
}
  • 动态权限申请:在应用启动时通过abilityAccessCtrl动态申请权限。

2. 初始化相机

使用CameraKit创建相机实例,并配置输出流。

import camera from '@ohos.multimedia.camera';
import image from '@ohos.multimedia.image';
import media from '@ohos.multimedia.media';

// 获取CameraManager实例
let cameraManager = camera.getCameraManager(context);

// 获取相机设备列表并选择后置摄像头
let cameraDevices = cameraManager.getSupportedCameras();
let cameraDevice = cameraDevices.find(device => device.lensFacing === camera.LensFacing.LENS_FACING_BACK);

// 创建相机输入流
let cameraInput = cameraManager.createCameraInput(cameraDevice);

// 创建预览输出流(用于画面预览)
let previewOutput = cameraManager.createPreviewOutput(previewSurfaceId);

// 创建视频输出流(用于录像)
let videoProfile = {
  audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
  videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
  profile: {
    audioBitrate: 48000,
    audioChannels: 2,
    audioCodec: media.CodecMimeType.AUDIO_AAC,
    audioSampleRate: 48000,
    fileFormat: media.ContainerFormatType.CFT_MPEG_4,
    videoBitrate: 2000000,
    videoCodec: media.CodecMimeType.VIDEO_AVC,
    videoFrameWidth: 640,
    videoFrameHeight: 480,
    videoFrameRate: 30
  }
};
let videoOutput = cameraManager.createVideoOutput(videoProfile);

3. 会话管理与录像控制

  • 创建相机会话:绑定输入输出流,启动预览。
let captureSession = cameraManager.createCaptureSession();
await captureSession.beginConfig();
await captureSession.addInput(cameraInput);
await captureSession.addOutput(previewOutput);
await captureSession.addOutput(videoOutput);
await captureSession.commitConfig();
await captureSession.start();
  • 开始/停止录像:通过AVRecorder控制录像过程。
import media from '@ohos.multimedia.media';

// 创建AVRecorder实例
let avRecorder = await media.createAVRecorder();

// 准备录像配置
let avConfig = {
  audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
  videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
  profile: {
    audioBitrate: 48000,
    audioChannels: 2,
    audioCodec: media.CodecMimeType.AUDIO_AAC,
    audioSampleRate: 48000,
    fileFormat: media.ContainerFormatType.CFT_MPEG_4,
    videoBitrate: 2000000,
    videoCodec: media.CodecMimeType.VIDEO_AVC,
    videoFrameWidth: 640,
    videoFrameHeight: 480,
    videoFrameRate: 30
  },
  url: 'file:///data/storage/el2/base/video.mp4' // 录像文件路径
};
await avRecorder.prepare(avConfig);

// 开始录像
await avRecorder.start();

// 停止录像
await avRecorder.stop();
await avRecorder.release();

4. 资源释放

在应用退出或相机不可用时,及时释放资源:

await captureSession.stop();
await cameraInput.release();

注意事项

  • 帧率与分辨率:需根据设备支持的规格动态配置。
  • 异步操作:所有相机操作均为异步,需使用async/await或Promise处理。
  • 兼容性:不同设备对编解码器和格式的支持可能存在差异,建议通过CameraKit查询设备能力。

通过以上步骤,即可在HarmonyOS Next中实现自定义相机的录像功能。

回到顶部