HarmonyOS APP慢动作开发小知识

举报
Jack20 发表于 2026/06/25 21:30:28 2026/06/25
【摘要】 HarmonyOS开发中的慢动作:高帧率录制、慢动作回放、帧率配置、慢动作区间选择、慢动作导出核心要点:掌握 HarmonyOS 慢动作视频开发的完整链路,从高帧率录制原理到慢动作回放控制,从帧率参数配置到慢动作区间精确选择,最终实现专业级慢动作视频导出。 一、背景与动机你看过那些酷炫的慢动作视频吗?水滴落入杯中溅起水花、运动员腾空翻转的瞬间、花瓣在风中缓缓飘落……这些画面之所以震撼,是因...

HarmonyOS开发中的慢动作:高帧率录制、慢动作回放、帧率配置、慢动作区间选择、慢动作导出

核心要点:掌握 HarmonyOS 慢动作视频开发的完整链路,从高帧率录制原理到慢动作回放控制,从帧率参数配置到慢动作区间精确选择,最终实现专业级慢动作视频导出。


一、背景与动机

你看过那些酷炫的慢动作视频吗?水滴落入杯中溅起水花、运动员腾空翻转的瞬间、花瓣在风中缓缓飘落……这些画面之所以震撼,是因为它们展示了我们肉眼无法捕捉的细节。

慢动作的原理其实很简单:用更高的帧率录制,再用正常的帧率播放。比如用 240fps 录制,30fps 播放,时间就被拉长了 8 倍——1 秒的动作变成了 8 秒的慢放。

但实现起来并不简单。高帧率录制对硬件要求很高——传感器需要更快的读出速度,ISP 需要更强的处理能力,存储需要更高的写入带宽。而且不是所有设备都支持所有帧率,你需要查询设备能力、选择合适的参数。

更进阶的功能是慢动作区间选择——不是整段视频都慢放,而是只在精彩瞬间慢放,其余部分正常速度。这就像电影中的"子弹时间"效果。

今天我们就来深入探讨这些技术细节。


二、核心原理

2.1 慢动作录制与回放原理

flowchart LR
    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[高帧率录制]:::primary --> B[240fps 原始帧序列]:::primary
    B --> C{回放模式}:::purple
    C --> D[全慢放: 240→30fps]:::info
    C --> E[区间慢放: 指定区间降速]:::info
    C --> F[变速回放: 动态帧率调整]:::info
    D --> G[8x 慢动作效果]:::warning
    E --> H[精彩瞬间慢放]:::warning
    F --> I[平滑变速效果]:::warning
    G --> J[导出视频]:::error
    H --> J
    I --> J

2.2 帧率与慢放倍率的关系

慢放倍率 = 录制帧率 / 播放帧率。常见的配置:

录制帧率 播放帧率 慢放倍率 效果描述
60 fps 30 fps 2x 轻度慢放,适合运动镜头
120 fps 30 fps 4x 中度慢放,适合水花、跳跃
240 fps 30 fps 8x 高度慢放,适合细节展示
960 fps 30 fps 32x 超级慢放,适合子弹、闪电

2.3 高帧率录制的硬件限制

高帧率录制不是想录多快就能录多快的,主要受以下因素限制:

  1. 传感器读出速度:传感器需要逐行读出像素数据,帧率越高,每行可用的读出时间越短
  2. ISP 处理能力:每帧都需要 ISP 处理(降噪、色彩校正等),帧率翻倍意味着 ISP 负载翻倍
  3. 分辨率限制:高帧率通常需要降低分辨率。比如 4K 只能录 60fps,1080p 可以录 240fps,720p 才能录 960fps
  4. 存储带宽:240fps 的数据量是 30fps 的 8 倍,存储写入速度必须跟上
  5. 发热问题:高帧率持续录制会导致芯片发热,可能触发温控降频

2.4 慢动作区间选择原理

慢动作区间选择的核心是时间重映射(Time Remapping)

  • 正常区间:以录制帧率直接播放(或按比例抽帧)
  • 慢放区间:从高帧率帧序列中取出更多帧,以正常帧率播放,实现时间拉伸
  • 过渡区间:在正常和慢放之间做平滑过渡,避免速度突变

三、代码实战

3.1 高帧率录制:查询能力与配置录制

首先查询设备支持的高帧率能力,然后配置并启动高帧率录制。

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

/**
 * 高帧率录制配置
 */
interface HighFrameRateConfig {
  /** 录制帧率 */
  frameRate: number;
  /** 录制分辨率宽度 */
  width: number;
  /** 录制分辨率高度 */
  height: number;
}

/**
 * 高帧率录制管理器
 * 支持查询设备帧率能力、配置高帧率录制参数
 */
class SlowMotionRecorder {
  private cameraManager: camera.CameraManager | null = null;
  private captureSession: camera.CaptureSession | null = null;
  private videoOutput: camera.VideoOutput | null = null;
  private avRecorder: media.AVRecorder | null = null;
  private isRecording: boolean = false;
  private recordingStartTime: number = 0;

  /** 录制状态回调 */
  onRecordingStart?: (config: HighFrameRateConfig) => void;
  onRecordingStop?: (duration: number) => void;
  onRecordingError?: (error: BusinessError) => void;

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

  /**
   * 查询设备支持的高帧率配置
   * 不同设备支持的帧率和分辨率组合不同
   */
  async queryHighFrameRateSupport(): Promise<HighFrameRateConfig[]> {
    if (!this.cameraManager) {
      await this.init();
    }

    const supportedConfigs: HighFrameRateConfig[] = [];

    try {
      const cameras = this.cameraManager!.getSupportedCameras();
      const backCamera = cameras.find(
        c => c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK
      );

      if (!backCamera) {
        console.error('[SlowMotion] 找不到后置摄像头');
        return supportedConfigs;
      }

      const capability = this.cameraManager!.getSupportedOutputCapability(backCamera);
      const videoProfiles = capability.videoProfiles;

      console.info(`[SlowMotion] 设备支持 ${videoProfiles.length} 种视频配置:`);

      // 遍历所有视频配置,筛选高帧率选项
      for (const profile of videoProfiles) {
        const fpsRange = profile.frameRateRange;
        const maxFps = fpsRange.max;
        const width = profile.size.width;
        const height = profile.size.height;

        console.info(`  - ${width}x${height} @ ${fpsRange.min}-${maxFps}fps, 格式: ${profile.format}`);

        // 只关注 60fps 以上的配置
        if (maxFps >= 60) {
          supportedConfigs.push({
            frameRate: maxFps,
            width: width,
            height: height,
          });
        }
      }

      // 按帧率降序排列
      supportedConfigs.sort((a, b) => b.frameRate - a.frameRate);

      console.info(`[SlowMotion] 高帧率配置: ${supportedConfigs.length}`);
      supportedConfigs.forEach((config, i) => {
        console.info(`  [${i}] ${config.width}x${config.height} @ ${config.frameRate}fps (${config.frameRate / 30}x 慢放)`);
      });

      return supportedConfigs;
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[SlowMotion] 查询帧率支持失败: ${err.code} - ${err.message}`);
      return supportedConfigs;
    }
  }

  /**
   * 启动高帧率录制
   * @param config 高帧率配置
   * @param outputUri 输出文件 URI
   */
  async startRecording(config: HighFrameRateConfig, outputUri: string): Promise<boolean> {
    if (this.isRecording) {
      console.warn('[SlowMotion] 正在录制中');
      return false;
    }

    try {
      if (!this.cameraManager) {
        await this.init();
      }

      const cameras = this.cameraManager!.getSupportedCameras();
      const backCamera = cameras.find(
        c => c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK
      );

      if (!backCamera) return false;

      // 创建相机输入
      const cameraInput = this.cameraManager!.createCameraInput(backCamera);
      await cameraInput.open();

      // 创建预览输出(同时预览和录制)
      const capability = this.cameraManager!.getSupportedOutputCapability(backCamera);

      // 查找匹配的视频 Profile
      const matchedProfile = capability.videoProfiles.find(p =>
        p.size.width === config.width &&
        p.size.height === config.height &&
        p.frameRateRange.max >= config.frameRate
      );

      if (!matchedProfile) {
        console.error('[SlowMotion] 找不到匹配的视频配置');
        return false;
      }

      // 创建视频输出
      this.videoOutput = this.cameraManager!.createVideoOutput(matchedProfile, outputUri);

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

      // 配置录制参数
      const avConfig: media.AVRecorderConfig = {
        audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
        videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
        profile: {
          audioBitrate: 128000,
          audioChannels: 2,
          audioCodec: media.CodecMimeType.AUDIO_AAC,
          audioSampleRate: 48000,
          videoBitrate: 20000000, // 20Mbps,高帧率需要高码率
          videoCodec: media.CodecMimeType.VIDEO_AVC,
          videoFrameWidth: config.width,
          videoFrameHeight: config.height,
          videoFrameRate: config.frameRate,
        },
        url: outputUri,
      };

      await this.avRecorder.prepare(avConfig);

      // 创建捕获会话
      this.captureSession = this.cameraManager!.createCaptureSession();
      this.captureSession.beginConfig();
      this.captureSession.addInput(cameraInput);
      this.captureSession.addOutput(this.videoOutput);

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

      // 开始录制
      await this.avRecorder.start();
      this.isRecording = true;
      this.recordingStartTime = Date.now();

      this.onRecordingStart?.(config);
      console.info(`[SlowMotion] 开始录制: ${config.width}x${config.height} @ ${config.frameRate}fps`);

      return true;
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[SlowMotion] 启动录制失败: ${err.code} - ${err.message}`);
      this.onRecordingError?.(err);
      return false;
    }
  }

  /**
   * 停止录制
   */
  async stopRecording(): Promise<string | null> {
    if (!this.isRecording || !this.avRecorder) {
      console.warn('[SlowMotion] 未在录制中');
      return null;
    }

    try {
      await this.avRecorder.stop();
      await this.avRecorder.release();

      const duration = (Date.now() - this.recordingStartTime) / 1000;
      this.isRecording = false;

      this.onRecordingStop?.(duration);
      console.info(`[SlowMotion] 录制完成,时长: ${duration.toFixed(1)}`);

      return `录制完成,时长 ${duration.toFixed(1)}`;
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[SlowMotion] 停止录制失败: ${err.code} - ${err.message}`);
      return null;
    }
  }

  /**
   * 获取录制状态
   */
  getIsRecording(): boolean {
    return this.isRecording;
  }

  /**
   * 释放资源
   */
  async release(): Promise<void> {
    try {
      if (this.isRecording) {
        await this.stopRecording();
      }
      if (this.captureSession) {
        await this.captureSession.stop();
        this.captureSession.release();
      }
      console.info('[SlowMotion] 资源已释放');
    } catch (error) {
      console.error('[SlowMotion] 释放资源失败');
    }
  }
}

3.2 慢动作回放:帧级精确控制

录制完成后,需要对高帧率视频进行慢动作回放。这里使用 AVPlayer 实现变速回放。

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

/**
 * 慢动作回放模式
 */
enum SlowMotionPlaybackMode {
  /** 全程慢放 */
  FULL_SLOW = 'full_slow',
  /** 区间慢放 */
  INTERVAL_SLOW = 'interval_slow',
  /** 自定义变速 */
  CUSTOM_SPEED = 'custom_speed',
}

/**
 * 慢动作区间定义
 * @param startTime 区间开始时间(秒)
 * @param endTime 区间结束时间(秒)
 * @param speedRatio 慢放倍率(1=正常,0.125=8倍慢放)
 */
interface SlowMotionInterval {
  startTime: number;
  endTime: number;
  speedRatio: number;
}

/**
 * 慢动作回放控制器
 * 支持变速回放、区间慢放、帧级精确控制
 */
class SlowMotionPlayer {
  private avPlayer: media.AVPlayer | null = null;
  private playbackMode: SlowMotionPlaybackMode = SlowMotionPlaybackMode.FULL_SLOW;
  private slowIntervals: SlowMotionInterval[] = [];
  private recordedFrameRate: number = 240;
  private playbackFrameRate: number = 30;
  private currentSpeed: number = 1.0;
  private isPlaying: boolean = false;

  /** 播放状态回调 */
  onPlaybackStateChanged?: (isPlaying: boolean) => void;
  onCurrentTimeChanged?: (currentTime: number) => void;
  onIntervalChanged?: (interval: SlowMotionInterval | null) => void;

  /**
   * 初始化播放器
   * @param videoUri 视频文件 URI
   * @param recordedFrameRate 录制帧率
   */
  async init(videoUri: string, recordedFrameRate: number): Promise<void> {
    this.recordedFrameRate = recordedFrameRate;

    try {
      this.avPlayer = await media.createAVPlayer();

      // 设置播放状态回调
      this.avPlayer.on('stateChange', (state: string) => {
        console.info(`[SlowPlayer] 播放状态: ${state}`);
        if (state === 'playing') {
          this.isPlaying = true;
          this.onPlaybackStateChanged?.(true);
        } else if (state === 'paused' || state === 'stopped') {
          this.isPlaying = false;
          this.onPlaybackStateChanged?.(false);
        }
      });

      // 设置时间更新回调
      this.avPlayer.on('timeUpdate', (time: number) => {
        const currentTime = time / 1000; // 转换为秒
        this.onCurrentTimeChanged?.(currentTime);

        // 检查是否进入/离开慢动作区间
        if (this.playbackMode === SlowMotionPlaybackMode.INTERVAL_SLOW) {
          this.checkSlowMotionInterval(currentTime);
        }
      });

      // 设置数据源
      this.avPlayer.url = videoUri;

      console.info('[SlowPlayer] 播放器初始化完成');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[SlowPlayer] 初始化失败: ${err.code} - ${err.message}`);
    }
  }

  /**
   * 设置全程慢放模式
   * 整段视频以指定倍率慢放
   * @param slowRatio 慢放倍率(如 8 表示 8 倍慢放)
   */
  setFullSlowMode(slowRatio: number): void {
    this.playbackMode = SlowMotionPlaybackMode.FULL_SLOW;
    this.currentSpeed = 1 / slowRatio;

    if (this.avPlayer) {
      this.avPlayer.setSpeed(this.getAvPlayerSpeed(this.currentSpeed));
    }

    console.info(`[SlowPlayer] 全程慢放模式,倍率: ${slowRatio}x,速度: ${this.currentSpeed}`);
  }

  /**
   * 设置区间慢放模式
   * 只在指定区间慢放,其余部分正常速度
   * @param intervals 慢动作区间数组
   */
  setIntervalSlowMode(intervals: SlowMotionInterval[]): void {
    this.playbackMode = SlowMotionPlaybackMode.INTERVAL_SLOW;
    this.slowIntervals = intervals.sort((a, b) => a.startTime - b.startTime);

    console.info(`[SlowPlayer] 区间慢放模式,${this.slowIntervals.length} 个慢放区间:`);
    this.slowIntervals.forEach((interval, i) => {
      console.info(`  [${i}] ${interval.startTime}s - ${interval.endTime}s, ${1 / interval.speedRatio}x 慢放`);
    });
  }

  /**
   * 添加慢动作区间
   */
  addSlowInterval(startTime: number, endTime: number, speedRatio: number): void {
    this.slowIntervals.push({ startTime, endTime, speedRatio });
    this.slowIntervals.sort((a, b) => a.startTime - b.startTime);
    console.info(`[SlowPlayer] 添加慢放区间: ${startTime}s - ${endTime}s`);
  }

  /**
   * 移除慢动作区间
   */
  removeSlowInterval(index: number): void {
    if (index >= 0 && index < this.slowIntervals.length) {
      this.slowIntervals.splice(index, 1);
      console.info(`[SlowPlayer] 移除慢放区间 [${index}]`);
    }
  }

  /**
   * 检查当前时间是否在慢动作区间内
   * 动态调整播放速度
   */
  private checkSlowMotionInterval(currentTime: number): void {
    const matchingInterval = this.slowIntervals.find(
      interval => currentTime >= interval.startTime && currentTime <= interval.endTime
    );

    if (matchingInterval) {
      // 在慢放区间内,降低播放速度
      if (this.currentSpeed !== matchingInterval.speedRatio) {
        this.currentSpeed = matchingInterval.speedRatio;
        this.avPlayer?.setSpeed(this.getAvPlayerSpeed(this.currentSpeed));
        this.onIntervalChanged?.(matchingInterval);
      }
    } else {
      // 不在慢放区间,恢复正常速度
      if (this.currentSpeed !== 1.0) {
        this.currentSpeed = 1.0;
        this.avPlayer?.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
        this.onIntervalChanged?.(null);
      }
    }
  }

  /**
   * 将速度比例转换为 AVPlayer 支持的枚举值
   * AVPlayer 只支持有限的几个速度值
   */
  private getAvPlayerSpeed(speedRatio: number): media.PlaybackSpeed {
    // AVPlayer 支持的速度有限,选择最接近的
    if (speedRatio <= 0.125) {
      return media.PlaybackSpeed.SPEED_FORWARD_0_125_X;
    } else if (speedRatio <= 0.25) {
      return media.PlaybackSpeed.SPEED_FORWARD_0_25_X;
    } else if (speedRatio <= 0.5) {
      return media.PlaybackSpeed.SPEED_FORWARD_0_50_X;
    } else if (speedRatio <= 0.75) {
      return media.PlaybackSpeed.SPEED_FORWARD_0_75_X;
    } else if (speedRatio <= 1.0) {
      return media.PlaybackSpeed.SPEED_FORWARD_1_00_X;
    } else if (speedRatio <= 1.25) {
      return media.PlaybackSpeed.SPEED_FORWARD_1_25_X;
    } else if (speedRatio <= 1.75) {
      return media.PlaybackSpeed.SPEED_FORWARD_1_75_X;
    } else {
      return media.PlaybackSpeed.SPEED_FORWARD_2_00_X;
    }
  }

  /**
   * 开始播放
   */
  async play(): Promise<void> {
    if (this.avPlayer) {
      await this.avPlayer.play();
    }
  }

  /**
   * 暂停播放
   */
  async pause(): Promise<void> {
    if (this.avPlayer) {
      await this.avPlayer.pause();
    }
  }

  /**
   * 跳转到指定时间
   * @param time 目标时间(秒)
   */
  async seekTo(time: number): Promise<void> {
    if (this.avPlayer) {
      await this.avPlayer.seek(time * 1000, media.SeekMode.SEEK_PREV_SYNC);
    }
  }

  /**
   * 获取视频总时长
   */
  getDuration(): number {
    if (this.avPlayer) {
      return this.avPlayer.duration / 1000;
    }
    return 0;
  }

  /**
   * 获取当前播放时间
   */
  getCurrentTime(): number {
    if (this.avPlayer) {
      return this.avPlayer.currentTime / 1000;
    }
    return 0;
  }

  /**
   * 获取慢放区间列表
   */
  getSlowIntervals(): SlowMotionInterval[] {
    return [...this.slowIntervals];
  }

  /**
   * 释放播放器资源
   */
  async release(): Promise<void> {
    try {
      if (this.avPlayer) {
        await this.avPlayer.release();
        this.avPlayer = null;
      }
      console.info('[SlowPlayer] 播放器资源已释放');
    } catch (error) {
      console.error('[SlowPlayer] 释放资源失败');
    }
  }
}

3.3 慢动作区间选择 UI 组件

可视化的慢动作区间选择器,用户可以在时间轴上标记慢放区间。

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

@Entry
@Component
struct SlowMotionEditorPage {
  private player: SlowMotionPlayer | null = null;
  private videoDuration: number = 0;
  private recordedFrameRate: number = 240;

  @State isPlaying: boolean = false;
  @State currentTime: number = 0;
  @State slowIntervals: SlowMotionInterval[] = [];
  @State isSelectingInterval: boolean = false;
  @State selectionStart: number = 0;
  @State selectionEnd: number = 0;
  @State slowRatio: number = 8; // 慢放倍率
  @State playbackSpeed: number = 1.0;

  aboutToAppear(): void {
    this.player = new SlowMotionPlayer();
  }

  /**
   * 加载视频
   */
  async loadVideo(uri: string): Promise<void> {
    if (this.player) {
      await this.player.init(uri, this.recordedFrameRate);
      this.videoDuration = this.player.getDuration();

      // 设置回调
      this.player.onCurrentTimeChanged = (time: number) => {
        this.currentTime = time;
      };
      this.player.onPlaybackStateChanged = (playing: boolean) => {
        this.isPlaying = playing;
      };
    }
  }

  build() {
    Column() {
      // 标题
      Row() {
        Text('慢动作编辑')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        Blank()
        Text(`${this.recordedFrameRate}fps → ${this.recordedFrameRate / this.slowRatio / 30}x`)
          .fontSize(14)
          .fontColor('#4FC3F7')
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 12 })

      // 视频预览区域
      Stack() {
        // 视频画面(使用 XComponent 渲染)
        XComponent({
          id: 'slowMotionVideo',
          type: XComponentType.SURFACE,
          libraryname: ''
        })
          .width('100%')
          .aspectRatio(16 / 9)
          .borderRadius(12)

        // 慢放标识
        if (this.playbackSpeed < 1.0) {
          Text('SLOW')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFB74D')
            .padding({ left: 8, right: 8, top: 4, bottom: 4 })
            .borderRadius(4)
            .backgroundColor('#99FFB74D')
            .position({ x: 12, y: 12 })
        }
      }
      .width('100%')
      .padding({ left: 16, right: 16 })

      // 时间轴与区间选择器
      Column() {
        // 时间显示
        Row() {
          Text(this.formatTime(this.currentTime))
            .fontSize(16)
            .fontColor('#4FC3F7')
            .fontFamily('monospace')
          Blank()
          Text(this.formatTime(this.videoDuration))
            .fontSize(16)
            .fontColor('#AAAAAA')
            .fontFamily('monospace')
        }
        .width('100%')
        .margin({ bottom: 8 })

        // 时间轴
        Stack() {
          // 底部时间轴轨道
          Row()
            .width('100%')
            .height(40)
            .borderRadius(8)
            .backgroundColor('#333333')

          // 慢放区间标记
          ForEach(this.slowIntervals, (interval: SlowMotionInterval, index: number) => {
            Row()
              .width(`${((interval.endTime - interval.startTime) / this.videoDuration) * 100}%`)
              .height(40)
              .borderRadius(8)
              .backgroundColor('#33FFB74D')
              .position({
                x: `${(interval.startTime / this.videoDuration) * 100}%`,
                y: 0
              })
          })

          // 当前播放位置指示器
          Row()
            .width(2)
            .height(48)
            .backgroundColor('#4FC3F7')
            .position({
              x: `${(this.currentTime / this.videoDuration) * 100}%`,
              y: -4
            })

          // 选择区间预览
          if (this.isSelectingInterval) {
            Row()
              .width(`${((this.selectionEnd - this.selectionStart) / this.videoDuration) * 100}%`)
              .height(40)
              .borderRadius(8)
              .backgroundColor('#334FC3F7')
              .border({ width: 1, color: '#4FC3F7' })
              .position({
                x: `${(this.selectionStart / this.videoDuration) * 100}%`,
                y: 0
              })
          }
        }
        .width('100%')
        .height(48)
        .clip(true)

        // 操作提示
        Text(this.isSelectingInterval ? '拖动选择慢放区间,松开确认' : '长按时间轴选择慢放区间')
          .fontSize(12)
          .fontColor('#888888')
          .margin({ top: 4 })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 16 })

      // 慢放倍率选择
      Row() {
        Text('慢放倍率')
          .fontSize(14)
          .fontColor('#CCCCCC')

        ForEach([2, 4, 8, 16], (ratio: number) => {
          Text(`${ratio}x`)
            .fontSize(14)
            .fontColor(this.slowRatio === ratio ? '#4FC3F7' : '#AAAAAA')
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .borderRadius(16)
            .backgroundColor(this.slowRatio === ratio ? '#1A4FC3F7' : '#333333')
            .onClick(() => {
              this.slowRatio = ratio;
            })
        })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 16 })

      // 慢放区间列表
      if (this.slowIntervals.length > 0) {
        Column() {
          Text('慢放区间')
            .fontSize(14)
            .fontColor('#CCCCCC')
            .margin({ bottom: 8 })

          ForEach(this.slowIntervals, (interval: SlowMotionInterval, index: number) => {
            Row() {
              Text(`${this.formatTime(interval.startTime)} - ${this.formatTime(interval.endTime)}`)
                .fontSize(13)
                .fontColor('#FFB74D')
              Text(`${1 / interval.speedRatio}x 慢放`)
                .fontSize(13)
                .fontColor('#AAAAAA')
                .margin({ left: 8 })
              Blank()
              Text('删除')
                .fontSize(13)
                .fontColor('#EF5350')
                .onClick(() => {
                  this.slowIntervals.splice(index, 1);
                })
            }
            .width('100%')
            .padding(8)
            .borderRadius(8)
            .backgroundColor('#1A1A1A')
            .margin({ bottom: 4 })
          })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 8 })
      }

      Blank()

      // 底部控制栏
      Row() {
        // 播放/暂停
        Button() {
          Text(this.isPlaying ? '暂停' : '播放')
            .fontSize(14)
            .fontColor('#FFFFFF')
        }
        .backgroundColor('#4FC3F7')
        .borderRadius(20)
        .onClick(async () => {
          if (this.isPlaying) {
            await this.player?.pause();
          } else {
            await this.player?.play();
          }
        })

        Blank()

        // 导出按钮
        Button() {
          Text('导出慢动作视频')
            .fontSize(14)
            .fontColor('#FFFFFF')
        }
        .backgroundColor('#FFB74D')
        .borderRadius(20)
        .onClick(() => {
          this.exportSlowMotionVideo();
        })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 16, bottom: 32 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0D0D0D')
  }

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

  /**
   * 导出慢动作视频
   */
  private async exportSlowMotionVideo(): Promise<void> {
    console.info('[SlowEditor] 开始导出慢动作视频');
    // 导出逻辑在下一节实现
  }
}

3.4 慢动作视频导出

将编辑好的慢动作视频(包含慢放区间信息)导出为最终视频文件。

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

/**
 * 慢动作视频导出器
 * 根据慢放区间配置,将高帧率视频导出为慢动作效果的标准帧率视频
 */
class SlowMotionExporter {
  private avMuxer: media.AVMuxer | null = null;
  private videoProcessor: media.AVImageGenerator | null = null;

  /** 导出进度回调 */
  onExportProgress?: (progress: number) => void;
  /** 导出完成回调 */
  onExportComplete?: (outputPath: string) => void;
  /** 导出错误回调 */
  onExportError?: (error: BusinessError) => void;

  /**
   * 导出慢动作视频
   * @param inputPath 输入视频路径
   * @param outputPath 输出视频路径
   * @param intervals 慢放区间配置
   * @param recordedFrameRate 录制帧率
   * @param outputFrameRate 输出帧率(通常 30fps)
   */
  async exportSlowMotion(
    inputPath: string,
    outputPath: string,
    intervals: SlowMotionInterval[],
    recordedFrameRate: number,
    outputFrameRate: number = 30
  ): Promise<boolean> {
    try {
      console.info('[SlowExporter] 开始导出慢动作视频');
      console.info(`  输入: ${inputPath}`);
      console.info(`  输出: ${outputPath}`);
      console.info(`  录制帧率: ${recordedFrameRate}fps`);
      console.info(`  输出帧率: ${outputFrameRate}fps`);
      console.info(`  慢放区间: ${intervals.length}`);

      // 第一步:获取输入视频信息
      const source = media.createAVImageGenerator();
      // 实际项目中,这里需要使用 AVImageGenerator 或 AVCodec 来逐帧处理

      // 第二步:创建输出 Muxer
      // 使用 mp4 格式封装
      this.avMuxer = media.createMuxer(outputPath, media.ContainerFormatType.CFT_MPEG_4);

      // 添加视频轨道
      const videoTrackConfig: media.VideoTrackProperty = {
        codecMimeType: media.CodecMimeType.VIDEO_AVC,
        width: 1920,
        height: 1080,
        frameRate: outputFrameRate,
      };

      const videoTrackId = this.avMuxer.addTrack(videoTrackConfig);
      await this.avMuxer.start();

      // 第三步:逐帧处理
      // 根据慢放区间配置,决定每帧的时间戳
      // 在慢放区间内,时间戳间隔拉大,实现慢放效果
      // 在正常区间,时间戳间隔正常

      const totalFrames = 100; // 示例值,实际需要从输入视频获取
      let outputTimestamp = 0; // 输出时间戳(微秒)
      const frameDuration = 1000000 / outputFrameRate; // 每帧时长(微秒)

      for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
        // 计算当前帧对应的原始时间
        const originalTime = frameIndex / recordedFrameRate;

        // 检查是否在慢放区间内
        const matchingInterval = intervals.find(
          interval => originalTime >= interval.startTime && originalTime <= interval.endTime
        );

        if (matchingInterval) {
          // 在慢放区间内,时间拉伸
          // 例如 8x 慢放意味着每帧的输出时间间隔是正常的 8 倍
          outputTimestamp += frameDuration;
          // 但只取部分帧,避免输出帧数过多
          if (frameIndex % Math.round(1 / matchingInterval.speedRatio) !== 0) {
            continue; // 跳过部分帧
          }
        } else {
          // 正常区间
          outputTimestamp += frameDuration;
        }

        // 更新进度
        this.onExportProgress?.(frameIndex / totalFrames);
      }

      // 第四步:完成封装
      await this.avMuxer.stop();
      this.avMuxer.release();

      this.onExportComplete?.(outputPath);
      console.info('[SlowExporter] 导出完成');
      return true;
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[SlowExporter] 导出失败: ${err.code} - ${err.message}`);
      this.onExportError?.(err);
      return false;
    }
  }

  /**
   * 释放资源
   */
  async release(): Promise<void> {
    try {
      if (this.avMuxer) {
        this.avMuxer.release();
      }
      console.info('[SlowExporter] 资源已释放');
    } catch (error) {
      console.error('[SlowExporter] 释放资源失败');
    }
  }
}

四、踩坑与注意事项

4.1 高帧率录制的分辨率限制

问题:用户期望 4K 240fps,但设备只支持 1080p 240fps。

解决方案

  • 在 UI 上明确标注每种帧率对应的最大分辨率
  • 查询 videoProfiles 获取设备实际支持的配置
  • 不支持的配置应该禁用或灰显,而不是让用户选择后报错

4.2 高帧率录制的发热问题

问题:240fps 持续录制 1 分钟以上,设备可能发热严重,触发温控降频。

解决方案

  • 限制高帧率录制的最大时长(如 240fps 最多录制 30 秒)
  • 监测设备温度,接近阈值时提醒用户
  • 在录制过程中显示剩余可用时间

4.3 AVPlayer 变速播放的精度限制

问题:AVPlayer 只支持几个固定的播放速度(0.125x, 0.25x, 0.5x, 0.75x, 1.0x, 1.25x, 1.75x, 2.0x),无法精确控制到任意速度。

解决方案

  • 选择最接近的可用速度
  • 对于精确变速需求,使用帧级手动控制:逐帧 seek + 定时器
  • 在导出时使用编码器实现精确的时间重映射

4.4 慢放区间的过渡平滑

问题:正常速度和慢放速度之间突然切换,观感不自然。

解决方案

  • 在区间边界添加 0.5-1 秒的过渡区间
  • 过渡区间内速度从正常渐变到慢放(ease-in/ease-out)
  • 使用三次贝塞尔曲线控制速度变化

4.5 导出视频的质量与大小

问题:慢动作视频导出后文件很大,或者画质下降明显。

解决方案

  • 选择合适的编码器和码率(H.264/H.265)
  • 高帧率录制时使用高码率(20Mbps+)
  • 导出时根据慢放倍率调整码率——慢放后帧数减少,码率可以适当降低
  • 提供多种导出质量选项

五、HarmonyOS 6 适配

5.1 API 变更

功能 HarmonyOS 5 HarmonyOS 6
高帧率录制 手动配置 VideoProfile 新增SlowMotionRecorder 专用类
变速播放 AVPlayer.setSpeed() 新增AVPlayer.setSpeedRatio() 支持任意速度
帧级控制 需手动 seek 新增AVPlayer.stepFrame() 逐帧控制
视频导出 手动 Muxer 新增SlowMotionExporter 专用类

5.2 新增特性

  • 智能慢动作:HarmonyOS 6 新增 AI 自动检测精彩瞬间,自动标记慢放区间
  • 超级慢动作:部分设备支持 960fps 甚至 1920fps 录制
  • HEIF 视频格式:支持 HEVC 编码的慢动作视频,文件更小
  • 实时慢动作预览:录制时即可看到慢放效果

5.3 迁移指南

// HarmonyOS 5 写法:手动配置高帧率录制
const videoProfile = capability.videoProfiles.find(p => p.frameRateRange.max >= 240);
const videoOutput = cameraManager.createVideoOutput(videoProfile, outputUri);

// HarmonyOS 6 推荐写法:专用慢动作录制器
const slowMotionRecorder = camera.createSlowMotionRecorder(backCamera);
slowMotionRecorder.setFrameRate(240);
slowMotionRecorder.setResolution(1920, 1080);
slowMotionRecorder.setMaxDuration(30); // 最长 30 秒
await slowMotionRecorder.start(outputUri);

六、总结

mindmap
  root((慢动作开发))
    高帧率录制
      帧率能力查询
      分辨率与帧率权衡
      VideoOutput配置
      AVRecorder参数
      发热与时长限制
    慢动作回放
      变速播放
      AVPlayer速度控制
      全程慢放模式
      区间慢放模式
      帧级精确控制
    帧率配置
      60/120/240/960fps
      分辨率限制
      码率配置
      设备能力适配
    慢动作区间选择
      时间轴可视化
      区间标记与编辑
      速度渐变过渡
      精彩瞬间检测
    慢动作导出
      时间重映射
      AVMuxer封装
      编码器选择
      码率与质量

核心知识点回顾

  1. 慢动作 = 高帧率录制 + 低帧率播放:录制帧率越高,慢放效果越明显
  2. 高帧率有硬件限制:帧率和分辨率不能兼得,必须查询设备能力
  3. 区间慢放是进阶功能:只在精彩瞬间慢放,其余正常速度,需要时间重映射
  4. 变速播放精度有限:AVPlayer 只支持固定几档速度,精确控制需要逐帧操作
  5. 导出是最终环节:将编辑结果编码为标准视频,注意码率和质量平衡

慢动作视频是手机相机中最炫酷的功能之一。掌握了这些技术,你就能让用户拍出令人惊叹的慢动作大片!

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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