HarmonyOS APP专属的录像开发小实践
核心要点:掌握 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 防抖技术
录像防抖有两种实现方式:
- OIS(光学防抖):通过物理镜头位移补偿抖动,效果最好但硬件成本高
- 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 中,暂停恢复后可能出现短暂的不同步。建议:
- 尽量减少暂停/恢复的次数
- 暂停后等待 100ms 再恢复,给底层缓冲留出处理时间
- 如果对同步要求极高,考虑使用分段录制(每次暂停停止录制,恢复时重新开始)
4.5 高分辨率录像发热
坑:4K 30fps 录像 5 分钟后,手机烫得像暖手宝,系统开始降频,录像帧率骤降。
解:
- 长时间录像建议使用 1080p 而非 4K
- 监听设备温度,温度过高时自动降低分辨率
- 开启防抖时注意,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 迁移要点
- 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
}
}
};
- 预设模式:6.0 中 AVRecorder 新增了预设配置,简化参数设置:
// HarmonyOS 6.0 预设模式
const config: media.AVRecorderConfig = {
preset: media.AVRecorderPreset.HIGH_QUALITY, // 高质量预设
url: `fd://${file.fd}`,
};
- 电影级防抖: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,码率配分辨。
下篇文章我们将深入「相机配置」,讲解预览分辨率、帧率、对焦、曝光补偿、白平衡等参数的精细调控。
- 点赞
- 收藏
- 关注作者
评论(0)