HarmonyOS 视频录制暂停与恢复功能实现:让用户录制体验更灵活

hm688c71eace447
发布于 2025-9-24 11:42
浏览
0收藏

在移动应用开发中,视频录制功能的灵活性直接影响用户体验。比如用户录制 Vlog 时需要暂停调整镜头,或是录制产品演示视频时需暂停补充讲解,此时 “单次录制支持暂停 / 恢复” 就成了关键需求 —— 它能避免用户因中断录制生成多个视频文件,还能减少后期拼接的繁琐操作。在 HarmonyOS 系统中,借助 AVRecorder 多媒体接口,我们可以轻松实现这一功能,让视频录制过程更符合用户直觉。下面正文开始

一、核心技术基础:认识 AVRecorder 接口

要实现视频录制的暂停与恢复,首先需要了解 HarmonyOS 提供的 AVRecorder 类 —— 它是系统级多媒体录制接口,封装了视频源(如相机)、音频源(如麦克风)的采集、编码与文件输出逻辑,且从 API 9 开始原生支持 pause()(暂停)和 resume()(恢复)方法,这为功能实现提供了底层支持。

1.1 AVRecorder 的核心能力

  • 多源适配:支持从相机(VIDEO_SOURCE_CAMERA)获取视频流,从麦克风(AUDIO_SOURCE_MIC)获取音频流,满足音视频同步录制需求;​
  • 灵活控制:提供 prepare()(准备)、start()(开始)、pause()(暂停)、resume()(恢复)、stop()(停止)、release()(释放)等方法,覆盖录制全生命周期;​
  • 文件完整性保障:暂停 / 恢复过程中,系统会自动处理音视频帧的拼接逻辑,最终生成单个连续的视频文件,无需开发者手动合并片段。​

1.2 关键状态流转

AVRecorder 的状态管理是功能稳定的核心,必须严格遵循以下流转顺序,避免跨状态调用导致异常:

idle(初始) → prepared(准备完成) → recording(录制中) → paused(暂停) → recording(恢复录制) → stopped(停止) → released(资源释放)

例如:未调用 start() 直接调用 pause(),或在 paused 状态下调用 start(),都会触发 BusinessError 异常,因此需在代码中做好状态判断。

二、完整实现流程:从权限配置到功能落地

下面以 ArkTS 声明式开发为例,完整实现 “视频录制 + 暂停 / 恢复 + 文件保存” 功能,涵盖权限申请、录制器初始化、控制逻辑与 UI 交互。

2.1 第一步:配置必要权限

视频录制需访问相机、麦克风与媒体存储,需在 module.json5 的 requestPermissions 中声明以下权限(HarmonyOS 权限管理遵循 “动态申请 + 用户授权” 原则):

{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.CAMERA", "reason": "需要访问相机进行视频采集", "usedScene": { "ability": ["com.example.videorecorder.MainAbility"], "when": "always" } },
      { "name": "ohos.permission.MICROPHONE", "reason": "需要访问麦克风进行音频采集", "usedScene": { "ability": ["com.example.videorecorder.MainAbility"], "when": "always" } },
      { "name": "ohos.permission.WRITE_MEDIA", "reason": "需要写入媒体文件保存录制视频", "usedScene": { "ability": ["com.example.videorecorder.MainAbility"], "when": "always" } },
      { "name": "ohos.permission.READ_MEDIA", "reason": "需要读取媒体库验证文件保存状态", "usedScene": { "ability": ["com.example.videorecorder.MainAbility"], "when": "always" } }
    ]
  }
}

2.2 第二步:初始化录制器与配置参数

录制器初始化需完成两件核心事:一是获取合法的视频输出路径(通过 mediaLibrary 创建媒体资源),二是配置录制参数(如分辨率、编码格式、比特率等)。

import avRecorder from '@ohos.multimedia.avRecorder';
import mediaLibrary from '@ohos.multimedia.mediaLibrary';
import { BusinessError } from '@ohos.base';
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';

@Component
struct VideoRecorderPage {
  // 录制器实例
  private recorder: avRecorder.Recorder | null = null;
  // 录制状态管理
  @State isRecording: boolean = false;
  @State isPaused: boolean = false;
  // 视频输出路径
  private outputUrl: string = '';
  // 权限申请结果标记
  @State hasPermission: boolean = false;

  // 页面加载时检查并申请权限
  aboutToAppear() {
    this.checkAndRequestPermissions();
  }

  // 检查并申请必要权限
  private async checkAndRequestPermissions() {
    const permissions = [
      'ohos.permission.CAMERA',
      'ohos.permission.MICROPHONE',
      'ohos.permission.WRITE_MEDIA',
      'ohos.permission.READ_MEDIA'
    ];
    const atManager = abilityAccessCtrl.createAtManager();
    const tokenId = abilityAccessCtrl.getTokenId();

    try {
      // 检查权限状态
      const statusList = await atManager.checkPermissions(tokenId, permissions);
      const needRequest: string[] = [];
      statusList.forEach((status, index) => {
        if (status !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
          needRequest.push(permissions[index]);
        }
      });
      // 申请未授予的权限
      if (needRequest.length > 0) {
        await atManager.requestPermissionsFromUser(tokenId, needRequest);
      }
      // 再次检查权限,确认全部授予
      const finalStatus = await atManager.checkPermissions(tokenId, permissions);
      this.hasPermission = finalStatus.every(status => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED);
      if (!this.hasPermission) {
        promptAction.showToast({ message: '缺少必要权限,无法启动录制' });
      }
    } catch (err) {
      console.error('权限检查/申请失败:', (err as BusinessError).message);
    }
  }

  // 初始化录制器配置
  private async initRecorder() {
    if (!this.hasPermission) return;

    try {
      // 1. 获取媒体库实例,创建视频文件(避免覆盖已有文件,用时间戳命名)
      const mediaLib = mediaLibrary.getMediaLibrary();
      const fileName = `Video_${Date.now()}.mp4`;
      const assetUri = await mediaLib.createAsset(mediaLibrary.MediaType.VIDEO, fileName);
      this.outputUrl = assetUri;

      // 2. 配置录制参数(根据需求调整分辨率、比特率等)
      const recorderConfig: avRecorder.RecorderConfig = {
        audioSourceType: avRecorder.AudioSourceType.AUDIO_SOURCE_MIC, // 音频源:麦克风
        videoSourceType: avRecorder.VideoSourceType.VIDEO_SOURCE_CAMERA, // 视频源:相机
        outputUrl: this.outputUrl, // 输出路径
        videoEncoder: 'video/avc', // 视频编码:H.264(兼容性好)
        audioEncoder: 'audio/mp4a-latm', // 音频编码:AAC
        videoSize: { width: 1920, height: 1080 }, // 分辨率:1080P
        bitrate: 10 * 1024 * 1024, // 比特率:10Mbps(平衡画质与文件大小)
        frameRate: 30 // 帧率:30fps(流畅度适中)
      };

      // 3. 创建录制器实例并准备
      this.recorder = await avRecorder.createRecorder(recorderConfig);
      await this.recorder.prepare(); // 进入 prepared 状态
      console.log('录制器初始化完成,输出路径:', this.outputUrl);
    } catch (err) {
      console.error('录制器初始化失败:', (err as BusinessError).message);
      promptAction.showToast({ message: '录制初始化失败,请重试' });
    }
  }
}

2.3 第三步:实现录制控制逻辑(开始 / 暂停 / 恢复 / 停止)

基于 AVRecorder 的状态流转,实现核心控制方法,确保每个操作仅在合法状态下执行:

// 开始录制
private async startRecording() {
  if (!this.hasPermission) {
    promptAction.showToast({ message: '缺少权限,无法开始录制' });
    return;
  }

  try {
    // 若录制器未初始化,先执行初始化
    if (!this.recorder) {
      await this.initRecorder();
    }
    // 仅在未录制状态下执行 start()
    if (this.recorder && !this.isRecording) {
      await this.recorder.start();
      this.isRecording = true;
      this.isPaused = false;
      promptAction.showToast({ message: '开始录制' });
    }
  } catch (err) {
    console.error('开始录制失败:', (err as BusinessError).message);
    promptAction.showToast({ message: '录制启动失败' });
  }
}

// 暂停录制
private async pauseRecording() {
  try {
    // 仅在“录制中且未暂停”状态下执行 pause()
    if (this.recorder && this.isRecording && !this.isPaused) {
      await this.recorder.pause();
      this.isPaused = true;
      promptAction.showToast({ message: '已暂停录制' });
    }
  } catch (err) {
    console.error('暂停录制失败:', (err as BusinessError).message);
    promptAction.showToast({ message: '暂停失败,请重试' });
  }
}

// 恢复录制
private async resumeRecording() {
  try {
    // 仅在“录制中且已暂停”状态下执行 resume()
    if (this.recorder && this.isRecording && this.isPaused) {
      await this.recorder.resume();
      this.isPaused = false;
      promptAction.showToast({ message: '恢复录制' });
    }
  } catch (err) {
    console.error('恢复录制失败:', (err as BusinessError).message);
    promptAction.showToast({ message: '恢复失败,请重试' });
  }
}

// 停止录制并释放资源
private async stopRecording() {
  try {
    // 仅在录制状态下执行 stop()
    if (this.recorder && this.isRecording) {
      await this.recorder.stop(); // 停止录制,生成完整文件
      promptAction.showToast({ message: `录制完成,文件已保存` });
      console.log('录制文件路径:', this.outputUrl);

      // 释放资源(避免内存泄漏)
      await this.recorder.release();
      this.recorder = null;
      this.isRecording = false;
      this.isPaused = false;
    }
  } catch (err) {
    console.error('停止录制失败:', (err as BusinessError).message);
    promptAction.showToast({ message: '录制停止失败,文件可能损坏' });
  }
}

2.4 第四步:设计 UI 交互与状态反馈

为让用户清晰感知录制状态,设计简洁的 UI 界面,包含 “开始 / 暂停 / 恢复 / 停止” 按钮,并根据状态禁用无效操作:

build() {
  Column({ space: 30 }) {
    // 标题
    Text('灵活视频录制')
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
      .textColor('#18181B')

    // 录制状态提示
    Text(this.getRecordingStatusText())
      .fontSize(16)
      .textColor(this.isRecording ? '#DC2626' : '#4B5563')

    // 录制控制按钮组
    Row({ space: 20 }) {
      Button('开始')
        .width(120)
        .height(45)
        .fontSize(16)
        .backgroundColor('#2563EB')
        .onClick(() => this.startRecording())
        .enabled(!this.isRecording) // 录制中时禁用“开始”

      Button('暂停')
        .width(120)
        .height(45)
        .fontSize(16)
        .backgroundColor('#F59E0B')
        .onClick(() => this.pauseRecording())
        .enabled(this.isRecording && !this.isPaused) // 仅录制中且未暂停时可用

      Button('恢复')
        .width(120)
        .height(45)
        .fontSize(16)
        .backgroundColor('#10B981')
        .onClick(() => this.resumeRecording())
        .enabled(this.isRecording && this.isPaused) // 仅录制中且已暂停时可用

      Button('停止')
        .width(120)
        .height(45)
        .fontSize(16)
        .backgroundColor('#EF4444')
        .onClick(() => this.stopRecording())
        .enabled(this.isRecording) // 仅录制中时可用
    }
  }
  .width('100%')
  .height('100%')
  .padding(40)
  .backgroundColor('#F9FAFB')
}

// 获取录制状态文本
private getRecordingStatusText(): string {
  if (!this.isRecording) {
    return '未开始录制,点击“开始”启动';
  } else if (this.isPaused) {
    return '已暂停录制,点击“恢复”继续或“停止”结束';
  } else {
    return '正在录制中,点击“暂停”中断或“停止”结束';
  }
}

三、优化点:提升功能稳定性与用户体验

完成基础功能后,还需关注以下优化点,避免常见问题并提升用户体验:

3.1 异常处理增强

除了核心流程的 try-catch,还需处理特殊场景:

  • 录制中断恢复:若录制中出现临时错误(如相机被占用),可在 onError 回调中释放资源,提示用户重试;​
  • 文件验证:停止录制后,通过 mediaLibrary 检查文件是否存在且大小合理(避免生成 0KB 无效文件)。​

3.2 低版本兼容性

AVRecorder 的 pause() 和 resume() 方法从 API 9 开始支持,若需适配 API 8 及以下版本,需通过 canIUse 做兼容性判断:

// 检查是否支持暂停/恢复方法
private isPauseResumeSupported(): boolean {
  return typeof avRecorder.Recorder.prototype.pause === 'function' 
    && typeof avRecorder.Recorder.prototype.resume === 'function';
}

// 在初始化时判断
private async initRecorder() {
  if (!this.isPauseResumeSupported()) {
    promptAction.showToast({ message: '当前系统版本不支持暂停/恢复录制' });
    return;
  }
  // 后续初始化逻辑...
}

3.3 性能优化

  • 资源及时释放:录制停止后必须调用 release() 释放相机、麦克风等硬件资源,避免占用导致其他应用无法使用;​
  • 参数动态调整:根据设备性能动态调整录制参数(如低端设备降低分辨率至 720P),避免卡顿。

四、总结

最后简单总结一下,通过 HarmonyOS 的 AVRecorder 接口,我们无需手动处理音视频帧拼接,即可轻松实现视频录制的暂停与恢复功能,最终生成单个完整视频文件。当然关键在于严格遵循录制器的状态流转,做好权限管理与异常处理,并结合用户体验设计合理的交互反馈。我们后续还可扩展功能,如添加录制时长显示、视频预览等,进一步提升应用的竞争力。希望文章可以帮助到大家,谢谢!!!

分类
标签
收藏
回复
举报
回复
    相关推荐