HarmonyOS APP专属的录像开发小实践

举报
Jack20 发表于 2026/06/20 22:21:45 2026/06/20
【摘要】 核心要点:掌握 VideoSession 的创建与配置流程,学会录像参数的精细调控(分辨率、帧率、码率),实现录像防抖功能,完成录像的暂停与恢复控制。 一、背景与动机你有没有录过一段视频,回放的时候发现画面抖得像帕金森?或者录了半天,结果分辨率只有 480p,糊得没法看?又或者录到一半想暂停,结果发现 API 根本不支持?录像比拍照复杂得多。拍照是"咔嚓"一瞬间的静止,而录像是持续数分钟甚至...

核心要点:掌握 VideoSession 的创建与配置流程,学会录像参数的精细调控(分辨率、帧率、码率),实现录像防抖功能,完成录像的暂停与恢复控制。


一、背景与动机

你有没有录过一段视频,回放的时候发现画面抖得像帕金森?或者录了半天,结果分辨率只有 480p,糊得没法看?又或者录到一半想暂停,结果发现 API 根本不支持?

录像比拍照复杂得多。拍照是"咔嚓"一瞬间的静止,而录像是持续数分钟甚至数小时的动态数据流。这条数据流涉及视频编码、音频采集、文件写入三个并行通道的协同,任何一个环节出问题,录像就会失败。

而且,录像还有很多拍照不需要考虑的问题:防抖算法是否开启?分辨率和帧率怎么搭配才合理?码率设多少才能在清晰度和文件大小之间取得平衡?暂停后恢复录像,视频文件是合在一起还是分成两段?

这篇文章,我们就把录像这件事从头到尾讲明白。


二、核心原理

2.1 VideoSession 数据流架构

VideoSession 是录像场景的会话管理器,它比 PhotoSession 多了一个音频输入通道和一个视频输出通道:

flowchart TB
    classDef primary fill:#4A90D9,stroke:#2C5F8A,color:#fff,font-weight:bold
    classDef warning fill:#E8A838,stroke:#B87A1A,color:#fff,font-weight:bold
    classDef error fill:#D94A4A,stroke:#8A2C2C,color:#fff,font-weight:bold
    classDef info fill:#50B5A9,stroke:#2C7A6F,color:#fff,font-weight:bold
    classDef purple fill:#9B59B6,stroke:#6C3483,color:#fff,font-weight:bold

    A[CameraInput<br/>相机输入]:::primary --> B[VideoSession<br/>录像会话]:::error
    C[AudioInput<br/>音频输入]:::info --> B
    B --> D[PreviewOutput<br/>预览输出]:::info
    B --> E[VideoOutput<br/>视频输出]:::warning
    E --> F[AVRecorder<br/>媒体录制器]:::purple
    F --> G[视频文件.mp4]:::purple

    H[防抖控制]:::warning -.-> B
    I[分辨率/帧率]:::info -.-> B
    J[码率控制]:::purple -.-> F

    style B stroke-width:4px
    style F stroke-width:3px

关键区别(与 PhotoSession 对比):

维度 PhotoSession VideoSession
输出通道 PhotoOutput VideoOutput
音频 不需要 需要 AudioInput
数据流 单帧 持续流
录制器 需要 AVRecorder
暂停/恢复 不适用 支持

2.2 录像完整流程

flowchart TD
    classDef primary fill:#4A90D9,stroke:#2C5F8A,color:#fff,font-weight:bold
    classDef warning fill:#E8A838,stroke:#B87A1A,color:#fff,font-weight:bold
    classDef error fill:#D94A4A,stroke:#8A2C2C,color:#fff,font-weight:bold
    classDef info fill:#50B5A9,stroke:#2C7A6F,color:#fff,font-weight:bold
    classDef purple fill:#9B59B6,stroke:#6C3483,color:#fff,font-weight:bold

    A[创建VideoOutput]:::primary --> B[创建VideoSession]:::primary
    B --> C[配置录像参数]:::info
    C --> D[创建AVRecorder]:::purple
    D --> E[配置AVRecorder]:::purple
    E --> F[session.start 启动预览]:::warning
    F --> G[recorder.start 开始录制]:::error
    G --> H{录像中}:::purple
    H -->|暂停| I[recorder.pause]:::info
    I -->|恢复| J[recorder.resume]:::info
    J --> H
    H -->|停止| K[recorder.stop]:::error
    K --> L[recorder.release]:::info
    L --> M[保存视频文件]:::purple

    style G stroke-width:3px
    style H stroke-width:3px

2.3 录像参数详解

录像的质量由三个核心参数决定,它们之间相互制约:

分辨率 × 帧率 × 码率 = 视频质量

分辨率 推荐帧率 推荐码率 文件大小(1分钟) 适用场景
3840×2160 (4K) 30fps 20Mbps ~150MB 专业拍摄
1920×1080 (1080p) 30fps 8Mbps ~60MB 日常录像
1280×720 (720p) 30fps 4Mbps ~30MB 空间有限
640×480 (480p) 30fps 2Mbps ~15MB 低质量需求

2.4 防抖技术

录像防抖有两种实现方式:

  1. OIS(光学防抖):通过物理镜头位移补偿抖动,效果最好但硬件成本高
  2. EIS(电子防抖):通过算法裁剪画面边缘来补偿抖动,软件实现成本低

HarmonyOS 相机 API 中通过 VideoStabilizationMode 控制:

模式 说明
OFF 关闭防抖
LOW 低级防抖(轻度裁剪)
MIDDLE 中级防抖(中度裁剪)
HIGH 高级防抖(重度裁剪,画面最稳但视野最小)
AUTO 自动选择

三、代码实战

3.1 VideoSession 基础录像

这是最核心的示例——从创建 VideoSession 到完成一段录像的完整流程。

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

/**
 * 录像控制器
 * 封装完整的录像流程:初始化 → 预览 → 录制 → 暂停/恢复 → 停止
 */
export class VideoRecordController {
  private cameraManager: camera.CameraManager | null = null;
  private cameraInput: camera.CameraInput | null = null;
  private videoSession: camera.VideoSession | null = null;
  private previewOutput: camera.PreviewOutput | null = null;
  private videoOutput: camera.VideoOutput | null = null;
  private avRecorder: media.AVRecorder | null = null;

  // 录像状态
  private isRecording: boolean = false;
  private isPaused: boolean = false;
  private videoFilePath: string = '';

  // 回调
  private onRecordingStateChange?: (state: RecordingState) => void;
  private onError?: (error: BusinessError) => void;

  /**
   * 初始化录像会话
   */
  async init(context: Context, surfaceId: string): Promise<boolean> {
    try {
      // 1. 创建 CameraManager
      this.cameraManager = camera.getCameraManager(context);

      // 2. 获取后置摄像头
      const devices = this.cameraManager.getSupportedCameras();
      const backCamera = devices.find(
        d => d.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK
      ) || devices[0];

      // 3. 创建 CameraInput 并打开
      this.cameraInput = this.cameraManager.createCameraInput(backCamera);
      await this.cameraInput.open();

      // 4. 获取输出能力
      const capability = this.cameraManager.getSupportedOutputCapability(backCamera);

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

      // 6. 选择合适的录像配置(优先 1080p)
      const videoProfile = this.selectBestVideoProfile(capability.videoProfiles);

      // 7. 创建 VideoOutput
      this.videoOutput = this.cameraManager.createVideoOutput(videoProfile);

      // 8. 创建 VideoSession
      this.videoSession = this.cameraManager.createVideoSession(
        this.cameraInput,
        this.previewOutput,
        this.videoOutput
      );

      // 9. 启动会话(开始预览)
      await this.videoSession.start();

      console.info('[录像] 初始化完成,预览已启动');
      return true;

    } catch (error) {
      const err = error as BusinessError;
      console.error(`[录像初始化失败] code: ${err.code}, msg: ${err.message}`);
      this.onError?.(err);
      return false;
    }
  }

  /**
   * 选择最佳录像配置
   * 优先选择 1080p/30fps 的配置
   */
  private selectBestVideoProfile(
    profiles: camera.VideoProfile[]
  ): camera.VideoProfile {
    // 按优先级排序:1080p > 720p > 4K > 其他
    const preferred = profiles.find(p =>
      p.size.width === 1920 && p.size.height === 1080
    );
    if (preferred) return preferred;

    // 降级选择 720p
    const fallback = profiles.find(p =>
      p.size.width === 1280 && p.size.height === 720
    );
    if (fallback) return fallback;

    // 兜底:使用第一个配置
    return profiles[0];
  }

  /**
   * 开始录像
   * 这是录像的核心方法,涉及 AVRecorder 的完整配置
   */
  async startRecording(): Promise<void> {
    if (this.isRecording) {
      console.warn('[录像] 已在录制中');
      return;
    }

    try {
      // 1. 生成视频文件路径
      this.videoFilePath = `${getContext().filesDir}/VIDEO_${Date.now()}.mp4`;

      // 2. 创建 AVRecorder
      this.avRecorder = await media.createAVRecorder();

      // 3. 获取 VideoOutput 的 Surface
      const videoSurfaceId = await this.videoOutput!.getOutputSurface();

      // 4. 配置 AVRecorder 参数
      const config: media.AVRecorderConfig = {
        audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,  // 麦克风
        videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE, // Surface 模式
        surfaceId: videoSurfaceId,  // 视频数据来源
        url: `fd://0`,              // 文件描述符模式
        profile: {
          audio: {
            sampleRate: 48000,       // 音频采样率
            channels: 2,             // 双声道
            codec: media.CodecMimeType.AUDIO_AAC,    // AAC 编码
            bitrate: 128000,         // 音频码率 128kbps
          },
          video: {
            width: 1920,             // 视频宽度
            height: 1080,            // 视频高度
            frameRate: 30,           // 帧率
            codec: media.CodecMimeType.VIDEO_AVC,    // H.264 编码
            bitrate: 8000000,        // 视频码率 8Mbps
            isHdr: false,            // 非 HDR
          },
        } as media.AVRecorderProfile,
      };

      // 5. 准备 AVRecorder
      await this.avRecorder.prepare(config);

      // 6. 开始录制
      await this.avRecorder.start();

      this.isRecording = true;
      this.isPaused = false;
      this.onRecordingStateChange?.('recording');
      console.info('[录像] 开始录制');

    } catch (error) {
      const err = error as BusinessError;
      console.error(`[开始录像失败] code: ${err.code}, msg: ${err.message}`);
      this.onError?.(err);
    }
  }

  /**
   * 暂停录像
   */
  async pauseRecording(): Promise<void> {
    if (!this.isRecording || this.isPaused || !this.avRecorder) return;

    try {
      await this.avRecorder.pause();
      this.isPaused = true;
      this.onRecordingStateChange?.('paused');
      console.info('[录像] 已暂停');
    } catch (error) {
      console.error(`[暂停录像失败] ${error}`);
    }
  }

  /**
   * 恢复录像
   */
  async resumeRecording(): Promise<void> {
    if (!this.isRecording || !this.isPaused || !this.avRecorder) return;

    try {
      await this.avRecorder.resume();
      this.isPaused = false;
      this.onRecordingStateChange?.('recording');
      console.info('[录像] 已恢复');
    } catch (error) {
      console.error(`[恢复录像失败] ${error}`);
    }
  }

  /**
   * 停止录像
   */
  async stopRecording(): Promise<string> {
    if (!this.isRecording || !this.avRecorder) return '';

    try {
      // 1. 停止 AVRecorder
      await this.avRecorder.stop();

      // 2. 释放 AVRecorder
      await this.avRecorder.release();
      this.avRecorder = null;

      this.isRecording = false;
      this.isPaused = false;
      this.onRecordingStateChange?.('idle');
      console.info(`[录像] 已停止,文件: ${this.videoFilePath}`);

      return this.videoFilePath;

    } catch (error) {
      console.error(`[停止录像失败] ${error}`);
      return '';
    }
  }

  /**
   * 释放所有资源
   */
  async release(): Promise<void> {
    try {
      // 如果正在录制,先停止
      if (this.isRecording) {
        await this.stopRecording();
      }

      if (this.videoSession) {
        await this.videoSession.stop();
        await this.videoSession.release();
      }
      if (this.previewOutput) {
        await this.previewOutput.release();
      }
      if (this.videoOutput) {
        await this.videoOutput.release();
      }
      if (this.cameraInput) {
        await this.cameraInput.close();
        await this.cameraInput.release();
      }
      console.info('[录像] 资源已全部释放');
    } catch (error) {
      console.error(`[资源释放失败] ${error}`);
    }
  }

  // ===== 状态查询 =====

  getIsRecording(): boolean { return this.isRecording; }
  getIsPaused(): boolean { return this.isPaused; }
  getVideoFilePath(): string { return this.videoFilePath; }
}

/**
 * 录像状态枚举
 */
type RecordingState = 'idle' | 'recording' | 'paused';

3.2 录像防抖控制

防抖是录像中非常实用的功能,特别是手持拍摄时。这个示例展示了如何检测和设置防抖模式。

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

/**
 * 录像防抖控制器
 * 检测设备防抖能力,动态切换防抖模式
 */
export class VideoStabilizationController {
  private videoSession: camera.VideoSession | null = null;

  // 当前防抖模式
  private currentMode: camera.VideoStabilizationMode =
    camera.VideoStabilizationMode.OFF;

  /**
   * 绑定 VideoSession
   */
  bindSession(session: camera.VideoSession): void {
    this.videoSession = session;
    this.printStabilizationInfo();
  }

  /**
   * 打印防抖能力信息
   */
  private printStabilizationInfo(): void {
    if (!this.videoSession) return;

    const supportedModes = this.videoSession.getSupportedStabilizationModes();
    console.info(`[防抖] 支持的模式: ${supportedModes.map(m => this.getModeText(m)).join(', ')}`);

    const activeMode = this.videoSession.getActiveStabilizationMode();
    console.info(`[防抖] 当前模式: ${this.getModeText(activeMode)}`);
  }

  /**
   * 设置防抖模式
   * @param mode 目标防抖模式
   */
  setStabilizationMode(mode: camera.VideoStabilizationMode): boolean {
    if (!this.videoSession) {
      console.error('[防抖] 会话未绑定');
      return false;
    }

    try {
      // 检查是否支持
      const supportedModes = this.videoSession.getSupportedStabilizationModes();
      if (!supportedModes.includes(mode)) {
        console.warn(`[防抖] 不支持模式: ${this.getModeText(mode)}`);
        // 降级到 AUTO
        if (supportedModes.includes(camera.VideoStabilizationMode.AUTO)) {
          return this.setStabilizationMode(camera.VideoStabilizationMode.AUTO);
        }
        return false;
      }

      this.videoSession.setStabilizationMode(mode);
      this.currentMode = mode;
      console.info(`[防抖] 已切换到: ${this.getModeText(mode)}`);
      return true;

    } catch (error) {
      const err = error as BusinessError;
      console.error(`[防抖设置失败] code: ${err.code}, msg: ${err.message}`);
      return false;
    }
  }

  /**
   * 智能设置防抖
   * 根据设备能力自动选择最佳防抖模式
   */
  autoSetStabilization(): camera.VideoStabilizationMode {
    if (!this.videoSession) return camera.VideoStabilizationMode.OFF;

    const supportedModes = this.videoSession.getSupportedStabilizationModes();

    // 优先级:HIGH > MIDDLE > LOW > AUTO > OFF
    const priority: camera.VideoStabilizationMode[] = [
      camera.VideoStabilizationMode.HIGH,
      camera.VideoStabilizationMode.MIDDLE,
      camera.VideoStabilizationMode.LOW,
      camera.VideoStabilizationMode.AUTO,
      camera.VideoStabilizationMode.OFF,
    ];

    for (const mode of priority) {
      if (supportedModes.includes(mode)) {
        this.setStabilizationMode(mode);
        return mode;
      }
    }

    return camera.VideoStabilizationMode.OFF;
  }

  /**
   * 获取防抖模式文本
   */
  getModeText(mode: camera.VideoStabilizationMode): string {
    const map: Record<number, string> = {
      [camera.VideoStabilizationMode.OFF]: '关闭',
      [camera.VideoStabilizationMode.LOW]: '低级',
      [camera.VideoStabilizationMode.MIDDLE]: '中级',
      [camera.VideoStabilizationMode.HIGH]: '高级',
      [camera.VideoStabilizationMode.AUTO]: '自动',
    };
    return map[mode] || '未知';
  }

  /**
   * 切换防抖模式(循环切换)
   */
  cycleStabilizationMode(): camera.VideoStabilizationMode {
    if (!this.videoSession) return this.currentMode;

    const supportedModes = this.videoSession.getSupportedStabilizationModes();
    const currentIndex = supportedModes.indexOf(this.currentMode);
    const nextIndex = (currentIndex + 1) % supportedModes.length;
    const nextMode = supportedModes[nextIndex];

    this.setStabilizationMode(nextMode);
    return nextMode;
  }
}

3.3 录像参数配置与分辨率选择

这个示例展示了如何根据用户需求选择不同的录像分辨率,并配置对应的码率和帧率。

import { camera } from '@kit.CameraKit';
import { media } from '@kit.MediaKit';

/**
 * 录像配置预设
 * 不同分辨率对应的推荐参数
 */
export interface VideoRecordPreset {
  name: string;           // 预设名称
  width: number;          // 宽度
  height: number;         // 高度
  frameRate: number;      // 帧率
  videoBitrate: number;   // 视频码率
  audioBitrate: number;   // 音频码率
  sampleRate: number;     // 音频采样率
  description: string;    // 描述
}

/**
 * 录像配置管理器
 * 提供预设配置和自定义配置能力
 */
export class VideoConfigManager {
  // 内置预设配置
  private static readonly PRESETS: VideoRecordPreset[] = [
    {
      name: '4K 超高清',
      width: 3840, height: 2160,
      frameRate: 30,
      videoBitrate: 20000000,
      audioBitrate: 128000,
      sampleRate: 48000,
      description: '最高画质,文件较大,适合专业拍摄'
    },
    {
      name: '1080p 高清',
      width: 1920, height: 1080,
      frameRate: 30,
      videoBitrate: 8000000,
      audioBitrate: 128000,
      sampleRate: 48000,
      description: '画质与体积平衡,日常录像首选'
    },
    {
      name: '720p 标清',
      width: 1280, height: 720,
      frameRate: 30,
      videoBitrate: 4000000,
      audioBitrate: 128000,
      sampleRate: 48000,
      description: '节省空间,适合长时间录制'
    },
    {
      name: '1080p 60fps',
      width: 1920, height: 1080,
      frameRate: 60,
      videoBitrate: 12000000,
      audioBitrate: 128000,
      sampleRate: 48000,
      description: '高帧率,适合运动场景'
    },
  ];

  /**
   * 获取所有预设配置
   */
  static getPresets(): VideoRecordPreset[] {
    return [...VideoConfigManager.PRESETS];
  }

  /**
   * 根据预设生成 AVRecorder 配置
   * @param preset 预设配置
   * @param surfaceId VideoOutput 的 Surface ID
   */
  static createRecorderConfig(
    preset: VideoRecordPreset,
    surfaceId: string
  ): media.AVRecorderConfig {
    return {
      audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
      videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE,
      surfaceId: surfaceId,
      url: 'fd://0',
      profile: {
        audio: {
          sampleRate: preset.sampleRate,
          channels: 2,
          codec: media.CodecMimeType.AUDIO_AAC,
          bitrate: preset.audioBitrate,
        },
        video: {
          width: preset.width,
          height: preset.height,
          frameRate: preset.frameRate,
          codec: media.CodecMimeType.VIDEO_AVC,
          bitrate: preset.videoBitrate,
          isHdr: false,
        },
      } as media.AVRecorderProfile,
    };
  }

  /**
   * 从设备支持的配置中选择最匹配的预设
   * @param videoProfiles 设备支持的录像配置列表
   * @param targetPreset 目标预设
   */
  static findMatchingProfile(
    videoProfiles: camera.VideoProfile[],
    targetPreset: VideoRecordPreset
  ): camera.VideoProfile | null {
    // 精确匹配
    const exactMatch = videoProfiles.find(p =>
      p.size.width === targetPreset.width &&
      p.size.height === targetPreset.height &&
      p.frameRateRange.min <= targetPreset.frameRate &&
      p.frameRateRange.max >= targetPreset.frameRate
    );
    if (exactMatch) return exactMatch;

    // 宽松匹配:只匹配分辨率
    const resolutionMatch = videoProfiles.find(p =>
      p.size.width === targetPreset.width &&
      p.size.height === targetPreset.height
    );
    if (resolutionMatch) return resolutionMatch;

    // 降级匹配:选择最接近的较低分辨率
    const sorted = [...videoProfiles].sort(
      (a, b) => (b.size.width * b.size.height) - (a.size.width * a.size.height)
    );
    const lowerMatch = sorted.find(p =>
      p.size.width * p.size.height <= targetPreset.width * targetPreset.height
    );
    return lowerMatch || sorted[sorted.length - 1];
  }
}

/**
 * UI 组件 - 录像页面
 * 集成录像、防抖、参数配置
 */
@Entry
@Component
struct VideoPage {
  private recordController: VideoRecordController = new VideoRecordController();
  private stabilizationController: VideoStabilizationController = new VideoStabilizationController();

  @State recordingState: string = 'idle';  // idle / recording / paused
  @State recordingTime: number = 0;        // 录像时长(秒)
  @State selectedPreset: number = 1;        // 默认 1080p
  @State stabilizationText: string = '自动'; // 防抖模式文本

  private timerHandle: number = -1;

  aboutToDisappear(): void {
    if (this.timerHandle !== -1) {
      clearInterval(this.timerHandle);
    }
    this.recordController.release();
  }

  build() {
    Column() {
      // 顶部状态栏
      Row() {
        // 分辨率选择
        Text(VideoConfigManager.getPresets()[this.selectedPreset].name)
          .fontSize(14)
          .fontColor('#4A90D9')
          .onClick(() => this.showPresetPicker())

        Blank()

        // 防抖模式
        Text(`防抖: ${this.stabilizationText}`)
          .fontSize(14)
          .fontColor('#50B5A9')
          .onClick(() => {
            const newMode = this.stabilizationController.cycleStabilizationMode();
            this.stabilizationText = this.stabilizationController.getModeText(newMode);
          })
      }
      .width('100%')
      .padding({ left: 20, right: 20, top: 10 })

      // 录像时长显示
      if (this.recordingState !== 'idle') {
        Row() {
          // 录制指示灯(红点闪烁)
          Circle()
            .width(10)
            .height(10)
            .fill(this.recordingState === 'recording' ? '#FF0000' : '#FF6B6B')
            .opacity(this.recordingState === 'paused' ? 0.5 : 1)

          Text(this.formatTime(this.recordingTime))
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
            .margin({ left: 8 })
        }
        .padding(10)
        .borderRadius(20)
        .backgroundColor('rgba(0,0,0,0.5)')
      }

      Blank()

      // 底部操作区
      Row() {
        // 暂停/恢复按钮
        if (this.recordingState === 'recording') {
          Button('暂停')
            .onClick(() => this.pauseRecording())
        } else if (this.recordingState === 'paused') {
          Button('恢复')
            .backgroundColor('#50B5A9')
            .onClick(() => this.resumeRecording())
        }

        // 录制/停止按钮
        Button(this.recordingState === 'idle' ? '录制' : '停止')
          .width(70)
          .height(70)
          .borderRadius(35)
          .backgroundColor(this.recordingState === 'idle' ? '#D94A4A' : '#999')
          .onClick(() => {
            if (this.recordingState === 'idle') {
              this.startRecording();
            } else {
              this.stopRecording();
            }
          })
      }
      .width('100%')
      .padding({ left: 30, right: 30, bottom: 30 })
      .justifyContent(FlexAlign.SpaceAround)
    }
    .width('100%')
    .height('100%')
  }

  /**
   * 开始录像
   */
  private async startRecording(): Promise<void> {
    await this.recordController.startRecording();
    this.recordingState = 'recording';

    // 启动计时器
    this.recordingTime = 0;
    this.timerHandle = setInterval(() => {
      this.recordingTime++;
    }, 1000) as unknown as number;
  }

  /**
   * 暂停录像
   */
  private async pauseRecording(): Promise<void> {
    await this.recordController.pauseRecording();
    this.recordingState = 'paused';

    // 暂停计时器
    if (this.timerHandle !== -1) {
      clearInterval(this.timerHandle);
      this.timerHandle = -1;
    }
  }

  /**
   * 恢复录像
   */
  private async resumeRecording(): Promise<void> {
    await this.recordController.resumeRecording();
    this.recordingState = 'recording';

    // 恢复计时器
    this.timerHandle = setInterval(() => {
      this.recordingTime++;
    }, 1000) as unknown as number;
  }

  /**
   * 停止录像
   */
  private async stopRecording(): Promise<void> {
    const filePath = await this.recordController.stopRecording();
    this.recordingState = 'idle';

    // 停止计时器
    if (this.timerHandle !== -1) {
      clearInterval(this.timerHandle);
      this.timerHandle = -1;
    }

    console.info(`[录像] 文件已保存: ${filePath}`);
  }

  /**
   * 格式化时间
   */
  private formatTime(seconds: number): string {
    const min = Math.floor(seconds / 60);
    const sec = seconds % 60;
    return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
  }

  /**
   * 显示预设选择器
   */
  private showPresetPicker(): void {
    // 实际开发中使用自定义弹窗或 ActionSheet
    const presets = VideoConfigManager.getPresets();
    this.selectedPreset = (this.selectedPreset + 1) % presets.length;
  }
}

四、踩坑与注意事项

4.1 AVRecorder 状态机

:AVRecorder 有严格的状态机,如果调用顺序不对会直接报错。

:必须按照 create → prepare → start → (pause → resume)* → stop → release 的顺序调用。

// ❌ 错误:跳过 prepare 直接 start
await avRecorder.start(); // 报错!必须先 prepare

// ✅ 正确:完整的状态流转
await avRecorder.prepare(config);
await avRecorder.start();
// ... 录制中 ...
await avRecorder.stop();
await avRecorder.release();

4.2 VideoOutput 的 Surface 获取时机

:在 session.start() 之前调用 videoOutput.getOutputSurface(),返回空值。

:必须在会话 start() 之后获取 Surface,因为 Surface 是在会话启动时才创建的。

// ❌ 错误:会话启动前获取 Surface
const surfaceId = await videoOutput.getOutputSurface(); // 可能为空
await session.start();

// ✅ 正确:会话启动后获取 Surface
await session.start();
const surfaceId = await videoOutput.getOutputSurface();

4.3 录像文件 fd://0 问题

:AVRecorder 的 url 配置为 fd://0,但实际需要传入真实的文件描述符。

:在 HarmonyOS 5.0 中,需要先打开文件获取 fd,然后将 url 设为 fd://${fd}

// 正确做法:使用文件描述符
const filePath = `${context.filesDir}/video_${Date.now()}.mp4`;
const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
const config: media.AVRecorderConfig = {
  url: `fd://${file.fd}`,
  // ... 其他配置
};

4.4 暂停恢复后音视频不同步

:录像暂停后恢复,播放时发现音视频不同步,声音比画面快了半秒。

:这是 AVRecorder 的已知问题。在 HarmonyOS 5.0 中,暂停恢复后可能出现短暂的不同步。建议:

  1. 尽量减少暂停/恢复的次数
  2. 暂停后等待 100ms 再恢复,给底层缓冲留出处理时间
  3. 如果对同步要求极高,考虑使用分段录制(每次暂停停止录制,恢复时重新开始)

4.5 高分辨率录像发热

:4K 30fps 录像 5 分钟后,手机烫得像暖手宝,系统开始降频,录像帧率骤降。

  1. 长时间录像建议使用 1080p 而非 4K
  2. 监听设备温度,温度过高时自动降低分辨率
  3. 开启防抖时注意,EIS 会额外消耗算力

4.6 防抖模式与分辨率冲突

:设置了 HIGH 级别防抖后,录像分辨率从 1080p 降到了 720p。

:EIS 高级防抖需要裁剪画面边缘,实际输出分辨率会降低。如果对分辨率有严格要求,建议使用 LOW 或 MIDDLE 级别的防抖。


五、HarmonyOS 6 适配

5.1 API 变更

变更项 HarmonyOS 5.0 HarmonyOS 6.0
VideoSession 创建 createVideoSession(input, preview, video) 保持不变
AVRecorder 配置 手动配置所有参数 新增 preset 预设模式
防抖模式 OFF/LOW/MIDDLE/HIGH/AUTO 新增 CINEMATIC 电影级防抖
HDR 录像 不支持 新增 HDR10+ 录像支持
暂停/恢复 可能出现音视频不同步 已修复同步问题

5.2 迁移要点

  1. HDR 录像:6.0 新增了 HDR 录像支持,在 AVRecorder 配置中设置 isHdr: true
// HarmonyOS 6.0 HDR 录像
const config: media.AVRecorderConfig = {
  profile: {
    video: {
      width: 1920,
      height: 1080,
      codec: media.CodecMimeType.VIDEO_HEVC,  // HDR 需要 HEVC 编码
      bitrate: 15000000,
      isHdr: true,  // 开启 HDR
    }
  }
};
  1. 预设模式:6.0 中 AVRecorder 新增了预设配置,简化参数设置:
// HarmonyOS 6.0 预设模式
const config: media.AVRecorderConfig = {
  preset: media.AVRecorderPreset.HIGH_QUALITY,  // 高质量预设
  url: `fd://${file.fd}`,
};
  1. 电影级防抖:6.0 新增了 CINEMATIC 防抖模式,结合 AI 算法实现更平滑的画面稳定效果。

六、总结

mindmap
  root((录像开发))
    VideoSession
      三要素 Input + Preview + VideoOutput
      需要 AudioInput 音频采集
      start 后才能获取 Surface
    AVRecorder
      严格状态机 create→prepare→start→stop→release
      配置视频编码 H.264/HEVC
      配置音频编码 AAC
      url 使用文件描述符
    录像参数
      分辨率 4K/1080p/720p
      帧率 30fps/60fps
      码率 与分辨率匹配
      预设配置简化开发
    防抖控制
      OIS 光学防抖 硬件
      EIS 电子防抖 软件
      HIGH 模式会降低分辨率
      AUTO 模式智能选择
    暂停恢复
      AVRecorder pause/resume
      注意音视频同步
      减少暂停次数
      高版本已修复同步问题
    踩坑要点
      Surface 获取时机
      AVRecorder 状态流转
      文件描述符配置
      高分辨率发热降频

核心记忆口诀

Session 先启动,Surface 后获取;Recorder 严状态,暂停慎恢复;防抖选 AUTO,码率配分辨。

下篇文章我们将深入「相机配置」,讲解预览分辨率、帧率、对焦、曝光补偿、白平衡等参数的精细调控。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。