HarmonyOS APP慢动作开发小知识
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 高帧率录制的硬件限制
高帧率录制不是想录多快就能录多快的,主要受以下因素限制:
- 传感器读出速度:传感器需要逐行读出像素数据,帧率越高,每行可用的读出时间越短
- ISP 处理能力:每帧都需要 ISP 处理(降噪、色彩校正等),帧率翻倍意味着 ISP 负载翻倍
- 分辨率限制:高帧率通常需要降低分辨率。比如 4K 只能录 60fps,1080p 可以录 240fps,720p 才能录 960fps
- 存储带宽:240fps 的数据量是 30fps 的 8 倍,存储写入速度必须跟上
- 发热问题:高帧率持续录制会导致芯片发热,可能触发温控降频
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封装
编码器选择
码率与质量
核心知识点回顾:
- 慢动作 = 高帧率录制 + 低帧率播放:录制帧率越高,慢放效果越明显
- 高帧率有硬件限制:帧率和分辨率不能兼得,必须查询设备能力
- 区间慢放是进阶功能:只在精彩瞬间慢放,其余正常速度,需要时间重映射
- 变速播放精度有限:AVPlayer 只支持固定几档速度,精确控制需要逐帧操作
- 导出是最终环节:将编辑结果编码为标准视频,注意码率和质量平衡
慢动作视频是手机相机中最炫酷的功能之一。掌握了这些技术,你就能让用户拍出令人惊叹的慢动作大片!
- 点赞
- 收藏
- 关注作者
评论(0)