鸿蒙开发系列 · 高级篇
前言
做完天气服务和推送系统,你可能觉得鸿蒙开发已经"够用"了。但当你接到一个真正的产品需求——“做个语音备忘录App,能录音、能播放、还要在锁屏界面显示播放控制”——你会发现事情远没有想象中简单。
音视频开发是移动端最复杂的领域之一。它涉及硬件采集、编解码、缓冲区管理、音频焦点竞争、后台播放保活、锁屏媒体控制等一系列问题。Android的MediaPlayer/MediaRecorder被诟病多年,iOS的AVFoundation相对完善但学习曲线陡峭。HarmonyOS的方案是分层设计:底层提供高性能的C++引擎,上层用ArkTS封装出简洁的API。
本文将带你构建一个完整的语音备忘录应用,覆盖音频播放、录音、媒体会话(Media Session)三大核心能力。
一、HarmonyOS音视频架构概览
1.1 分层架构
┌─────────────────────────────────────────────┐
│ 应用层(你的代码) │
│ AVPlayer / AVRecorder / AudioRecorder │
├─────────────────────────────────────────────┤
│ 框架层(ArkTS API) │
│ @kit.MediaKit / @kit.AudioKit │
├─────────────────────────────────────────────┤
│ 引擎层(C++) │
│ 播放器引擎 / 录音引擎 / 编解码器 │
├─────────────────────────────────────────────┤
│ 硬件抽象层(HDI) │
│ 音频HDI / 视频HDI / 编解码HDI │
├─────────────────────────────────────────────┤
│ 硬件层 │
│ 扬声器 / 麦克风 / 摄像头 / GPU │
└─────────────────────────────────────────────┘
开发者主要与框架层交互,但理解分层有助于排查问题。比如播放卡顿,可能是网络层(上)的问题,也可能是音频缓冲区(下)的问题。
1.2 核心Kit对照表
二、音频播放:AVPlayer 深度实战
2.1 AVPlayer 状态机
AVPlayer采用状态机模型,理解状态流转是避免Bug的关键:
initialize()
[Idle] ──────────→ [Initialized]
│
│ prepare()
▼
[Prepared] ←───────
│ │
┌───────────────┼──────────────┤
│ │ │
▼ ▼ │
[Playing] ←─► [Paused] │
│ │ │
│ stop() │ │
└───────┬───────┘ │
▼ │
[Stopped] ──────────────────┘
│
│ release()
▼
[Released]
关键规则:
prepare()是异步的,必须在状态回调中确认准备完成才能调用play()stop()后不能直接从Stopped回到Playing,需要重新prepare()release()后播放器对象不可再用,需要重新创建
2.2 完整播放器封装
// entry/src/main/ets/utils/AudioPlayer.ets
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
export enum PlayerState {
IDLE = 'idle',
INITIALIZED = 'initialized',
PREPARED = 'prepared',
PLAYING = 'playing',
PAUSED = 'paused',
STOPPED = 'stopped',
RELEASED = 'released',
ERROR = 'error'
}
export class AudioPlayer {
private avPlayer: media.AVPlayer | null = null;
private currentState: PlayerState = PlayerState.IDLE;
private stateCallbacks: Set<(state: PlayerState) => void> = new Set();
private progressCallbacks: Set<(current: number, duration: number) => void> = new Set();
private progressTimer: number = -1;
// 状态监听
onStateChange(callback: (state: PlayerState) => void): () => void {
this.stateCallbacks.add(callback);
return () => this.stateCallbacks.delete(callback);
}
// 进度监听
onProgress(callback: (current: number, duration: number) => void): () => void {
this.progressCallbacks.add(callback);
return () => this.progressCallbacks.delete(callback);
}
private setState(state: PlayerState) {
this.currentState = state;
this.stateCallbacks.forEach(cb => cb(state));
}
// 初始化播放器
async initialize(): Promise<void> {
if (this.avPlayer) {
await this.release();
}
this.avPlayer = await media.createAVPlayer();
this.setupCallbacks();
this.setState(PlayerState.IDLE);
}
private setupCallbacks() {
if (!this.avPlayer) return;
// 状态变更回调
this.avPlayer.on('stateChange', (state) => {
const stateMap: Record<string, PlayerState> = {
'idle': PlayerState.IDLE,
'initialized': PlayerState.INITIALIZED,
'prepared': PlayerState.PREPARED,
'playing': PlayerState.PLAYING,
'paused': PlayerState.PAUSED,
'stopped': PlayerState.STOPPED,
'released': PlayerState.RELEASED,
'error': PlayerState.ERROR
};
this.setState(stateMap[state] || PlayerState.ERROR);
if (state === 'playing') {
this.startProgressTimer();
} else if (state === 'paused' || state === 'stopped') {
this.stopProgressTimer();
}
});
// 错误回调
this.avPlayer.on('error', (err: BusinessError) => {
console.error('播放器错误:', err.code, err.message);
this.setState(PlayerState.ERROR);
});
// 播放完成
this.avPlayer.on('endOfStream', () => {
console.info('播放完成');
this.stopProgressTimer();
// 可以自动播放下一首,或回到起始位置
this.seek(0);
});
}
// 设置数据源
async setSource(uri: string): Promise<void> {
if (!this.avPlayer) throw new Error('播放器未初始化');
// 支持网络URL、本地文件路径、FD(文件描述符)
this.avPlayer.url = uri;
this.setState(PlayerState.INITIALIZED);
}
// 准备播放(异步,缓冲数据)
async prepare(): Promise<void> {
if (!this.avPlayer) throw new Error('播放器未初始化');
await this.avPlayer.prepare();
}
// 播放
async play(): Promise<void> {
if (!this.avPlayer) throw new Error('播放器未初始化');
if (this.currentState === PlayerState.PAUSED) {
await this.avPlayer.play();
} else if (this.currentState === PlayerState.PREPARED ||
this.currentState === PlayerState.STOPPED) {
await this.avPlayer.play();
} else {
throw new Error(`当前状态 ${this.currentState} 不支持播放`);
}
}
// 暂停
async pause(): Promise<void> {
if (!this.avPlayer || this.currentState !== PlayerState.PLAYING) return;
await this.avPlayer.pause();
}
// 停止
async stop(): Promise<void> {
if (!this.avPlayer) return;
await this.avPlayer.stop();
this.stopProgressTimer();
}
// 跳转
async seek(timeMs: number): Promise<void> {
if (!this.avPlayer) return;
await this.avPlayer.seek(timeMs);
}
// 释放资源
async release(): Promise<void> {
this.stopProgressTimer();
if (this.avPlayer) {
await this.avPlayer.release();
this.avPlayer = null;
}
this.setState(PlayerState.RELEASED);
}
// 获取总时长
getDuration(): number {
return this.avPlayer?.duration ?? 0;
}
// 获取当前进度
getCurrentTime(): number {
return this.avPlayer?.currentTime ?? 0;
}
// 设置音量(0-1)
setVolume(volume: number): void {
if (this.avPlayer) {
this.avPlayer.setVolume(volume);
}
}
// 设置倍速
setSpeed(speed: number): void {
if (this.avPlayer) {
this.avPlayer.setSpeed(speed);
}
}
private startProgressTimer() {
this.stopProgressTimer();
this.progressTimer = setInterval(() => {
const current = this.getCurrentTime();
const duration = this.getDuration();
this.progressCallbacks.forEach(cb => cb(current, duration));
}, 500);
}
private stopProgressTimer() {
if (this.progressTimer !== -1) {
clearInterval(this.progressTimer);
this.progressTimer = -1;
}
}
getState(): PlayerState {
return this.currentState;
}
}
2.3 播放页面UI
// entry/src/main/ets/pages/PlayerPage.ets
import { AudioPlayer, PlayerState } from '../utils/AudioPlayer';
@Entry
@Component
struct PlayerPage {
private player: AudioPlayer = new AudioPlayer();
@State currentState: PlayerState = PlayerState.IDLE;
@State currentTime: number = 0;
@State duration: number = 0;
@State playUrl: string = 'https://example.com/audio/sample.mp3';
aboutToAppear() {
this.player.onStateChange((state) => {
this.currentState = state;
});
this.player.onProgress((current, duration) => {
this.currentTime = current;
this.duration = duration;
});
this.initPlayer();
}
async initPlayer() {
await this.player.initialize();
await this.player.setSource(this.playUrl);
await this.player.prepare();
}
aboutToDisappear() {
this.player.release();
}
private formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
build() {
Column({ space: 24 }) {
// 封面区域
Column({ space: 12 }) {
Image($r('app.media.ic_audio_cover'))
.width(200)
.height(200)
.borderRadius(12)
.shadow({ radius: 16, color: 'rgba(0,0,0,0.15)' })
Text('语音备忘录 #1')
.fontSize(20)
.fontWeight(FontWeight.Bold)
Text('2026-09-01 录制')
.fontSize(14)
.fontColor('#888')
}
.padding(24)
// 进度条
Column({ space: 8 }) {
Slider({
value: this.duration > 0 ? this.currentTime / this.duration * 100 : 0,
min: 0,
max: 100
})
.onChange((value) => {
const targetTime = (value / 100) * this.duration;
this.player.seek(targetTime);
})
Row() {
Text(this.formatTime(this.currentTime))
.fontSize(12)
.fontColor('#666')
Text(this.formatTime(this.duration))
.fontSize(12)
.fontColor('#666')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.padding({ left: 24, right: 24 })
// 控制按钮
Row({ space: 32 }) {
Button() {
Image($r('app.media.ic_skip_back'))
.width(24)
.height(24)
}
.width(48)
.height(48)
.backgroundColor('#F0F0F0')
.type(ButtonType.Circle)
Button() {
Image(this.currentState === PlayerState.PLAYING
? $r('app.media.ic_pause')
: $r('app.media.ic_play'))
.width(32)
.height(32)
}
.width(64)
.height(64)
.backgroundColor('#2196F3')
.type(ButtonType.Circle)
.onClick(() => {
if (this.currentState === PlayerState.PLAYING) {
this.player.pause();
} else {
this.player.play();
}
})
Button() {
Image($r('app.media.ic_skip_forward'))
.width(24)
.height(24)
}
.width(48)
.height(48)
.backgroundColor('#F0F0F0')
.type(ButtonType.Circle)
}
.padding(24)
}
.width('100%')
.height('100%')
.backgroundColor('#FAFAFA')
}
}
三、音频录制:AudioRecorder
3.1 录音实现
// entry/src/main/ets/utils/AudioRecorder.ets
import { audio } from '@kit.AudioKit';
import { fileIo } from '@kit.CoreFileKit';
export enum RecorderState {
IDLE = 'idle',
PREPARING = 'preparing',
RECORDING = 'recording',
PAUSED = 'paused', // 部分格式支持暂停
STOPPED = 'stopped',
ERROR = 'error'
}
export class VoiceRecorder {
private audioRecorder: audio.AudioRecorder | null = null;
private currentState: RecorderState = RecorderState.IDLE;
private outputFile: string = '';
private stateCallbacks: Set<(state: RecorderState) => void> = new Set();
private amplitudeCallbacks: Set<(amp: number) => void> = new Set();
private amplitudeTimer: number = -1;
onStateChange(callback: (state: RecorderState) => void): () => void {
this.stateCallbacks.add(callback);
return () => this.stateCallbacks.delete(callback);
}
onAmplitude(callback: (amp: number) => void): () => void {
this.amplitudeCallbacks.add(callback);
return () => this.amplitudeCallbacks.delete(callback);
}
private setState(state: RecorderState) {
this.currentState = state;
this.stateCallbacks.forEach(cb => cb(state));
}
// 初始化并准备录音
async prepare(outputPath: string): Promise<void> {
this.outputFile = outputPath;
// 确保目录存在
const dir = outputPath.substring(0, outputPath.lastIndexOf('/'));
try {
fileIo.accessSync(dir);
} catch {
fileIo.mkdirSync(dir, true);
}
// 创建录音器
const audioSourceInfo: audio.AudioSourceInfo = {
source: audio.SourceType.SOURCE_TYPE_MIC
};
const audioEncoderInfo: audio.AudioEncoderInfo = {
mimeType: 'audio/mp4a-latm', // AAC编码
sampleRate: 44100,
channels: 1,
bitRate: 128000,
bitWidth: 16
};
this.audioRecorder = await audio.createAudioRecorder();
// 配置录音参数
await this.audioRecorder.prepare({
audioSourceInfo,
audioEncoderInfo,
fileInfo: {
filePath: outputPath
}
});
this.setState(RecorderState.PREPARING);
}
// 开始录音
async start(): Promise<void> {
if (!this.audioRecorder) throw new Error('录音器未初始化');
await this.audioRecorder.start();
this.setState(RecorderState.RECORDING);
this.startAmplitudeMonitor();
}
// 停止录音
async stop(): Promise<void> {
if (!this.audioRecorder) return;
await this.audioRecorder.stop();
this.stopAmplitudeMonitor();
this.setState(RecorderState.STOPPED);
}
// 释放资源
async release(): Promise<void> {
this.stopAmplitudeMonitor();
if (this.audioRecorder) {
await this.audioRecorder.release();
this.audioRecorder = null;
}
this.setState(RecorderState.IDLE);
}
// 获取录音文件路径
getOutputFile(): string {
return this.outputFile;
}
private startAmplitudeMonitor() {
this.stopAmplitudeMonitor();
this.amplitudeTimer = setInterval(() => {
// 实际项目中可通过AudioKit获取实时音量
// 此处模拟波形数据
const amplitude = Math.random() * 100;
this.amplitudeCallbacks.forEach(cb => cb(amplitude));
}, 100);
}
private stopAmplitudeMonitor() {
if (this.amplitudeTimer !== -1) {
clearInterval(this.amplitudeTimer);
this.amplitudeTimer = -1;
}
}
}
3.2 录音页面
// entry/src/main/ets/pages/RecorderPage.ets
import { VoiceRecorder, RecorderState } from '../utils/AudioRecorder';
import { fileUri } from '@kit.CoreFileKit';
@Entry
@Component
struct RecorderPage {
private recorder: VoiceRecorder = new VoiceRecorder();
@State state: RecorderState = RecorderState.IDLE;
@State amplitude: number = 0;
@State recordTime: number = 0;
@State recordFile: string = '';
private timer: number = -1;
aboutToAppear() {
this.recorder.onStateChange((s) => this.state = s);
this.recorder.onAmplitude((amp) => this.amplitude = amp);
}
aboutToDisappear() {
this.recorder.release();
if (this.timer !== -1) clearInterval(this.timer);
}
async startRecording() {
const context = getContext();
const filesDir = context.filesDir;
const fileName = `record_${Date.now()}.m4a`;
const outputPath = `${filesDir}/${fileName}`;
await this.recorder.prepare(outputPath);
await this.recorder.start();
this.recordTime = 0;
this.timer = setInterval(() => {
this.recordTime++;
}, 1000);
}
async stopRecording() {
await this.recorder.stop();
if (this.timer !== -1) {
clearInterval(this.timer);
this.timer = -1;
}
this.recordFile = this.recorder.getOutputFile();
}
private formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
build() {
Column({ space: 32 }) {
// 波形可视化
Row() {
ForEach(Array.from({ length: 20 }, (_, i) => i), (index) => {
Column()
.width(6)
.height(this.state === RecorderState.RECORDING
? Math.max(10, this.amplitude * (0.5 + Math.random() * 0.5))
: 10)
.backgroundColor(this.state === RecorderState.RECORDING ? '#2196F3' : '#DDD')
.borderRadius(3)
.animation({
duration: 100,
curve: Curve.EaseInOut
})
})
}
.width('100%')
.height(120)
.padding(24)
.justifyContent(FlexAlign.SpaceAround)
.alignItems(VerticalAlign.Center)
// 计时
Text(this.formatTime(this.recordTime))
.fontSize(48)
.fontWeight(FontWeight.Light)
.fontColor(this.state === RecorderState.RECORDING ? '#2196F3' : '#333')
// 录音按钮
Button(this.state === RecorderState.RECORDING ? '停止录音' : '开始录音')
.width(160)
.height(56)
.fontSize(18)
.backgroundColor(this.state === RecorderState.RECORDING ? '#F44336' : '#2196F3')
.onClick(() => {
if (this.state === RecorderState.RECORDING) {
this.stopRecording();
} else {
this.startRecording();
}
})
// 已录制文件
if (this.recordFile) {
Text(`已保存: ${this.recordFile.split('/').pop()}`)
.fontSize(14)
.fontColor('#666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
}
.width('100%')
.height('100%')
.padding(24)
.backgroundColor('#FAFAFA')
}
}
四、媒体会话:AVSession
4.1 为什么需要媒体会话?
当你播放音频时,用户期望:
锁屏界面显示当前播放信息和控制按钮
控制中心能看到播放卡片
耳机线控可以暂停/播放
与其他音频应用"抢焦点"时行为正确
这些功能不是播放器自带的,而是通过**媒体会话(AVSession)**与系统协作实现的。
4.2 AVSession 接入
// entry/src/main/ets/utils/PlaybackSession.ets
import { avSession } from '@kit.AVSessionKit';
export class PlaybackSession {
private session: avSession.AVSession | null = null;
// 创建媒体会话
async create(context: Context, tag: string): Promise<void> {
this.session = await avSession.createAVSession(context, tag, 'audio');
this.setupCallbacks();
}
private setupCallbacks() {
if (!this.session) return;
// 播放控制
this.session.on('play', () => {
// 收到系统播放指令(如点击锁屏播放按钮)
this.emit('play');
});
this.session.on('pause', () => {
this.emit('pause');
});
this.session.on('stop', () => {
this.emit('stop');
});
this.session.on('seek', (time) => {
this.emit('seek', time);
});
// 上一首/下一首
this.session.on('playPrevious', () => {
this.emit('previous');
});
this.session.on('playNext', () => {
this.emit('next');
});
}
// 更新播放元数据(封面、标题等)
async setMetadata(metadata: {
title: string;
artist: string;
album: string;
duration: number;
mediaImage?: string;
}): Promise<void> {
if (!this.session) return;
await this.session.setAVMetadata({
assetId: metadata.title,
title: metadata.title,
artist: metadata.artist,
album: metadata.album,
duration: metadata.duration,
mediaImage: metadata.mediaImage || '',
mediaUri: ''
});
}
// 更新播放状态
async setPlaybackState(state: 'playing' | 'paused' | 'stopped', position: number): Promise<void> {
if (!this.session) return;
const stateMap: Record<string, avSession.PlaybackState> = {
'playing': avSession.PlaybackState.PLAYBACK_STATE_PLAY,
'paused': avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
'stopped': avSession.PlaybackState.PLAYBACK_STATE_STOP
};
await this.session.setAVPlaybackState({
state: stateMap[state],
position: { elapsedTime: position, updateTime: Date.now() }
});
}
// 激活会话(开始接收系统控制指令)
async activate(): Promise<void> {
await this.session?.activate();
}
// 释放会话
async destroy(): Promise<void> {
await this.session?.destroy();
this.session = null;
}
private eventHandlers: Map<string, Set<Function>> = new Map();
on(event: string, handler: Function): void {
if (!this.eventHandlers.has(event)) {
this.eventHandlers.set(event, new Set());
}
this.eventHandlers.get(event)!.add(handler);
}
private emit(event: string, ...args: unknown[]): void {
this.eventHandlers.get(event)?.forEach(h => h(...args));
}
}
4.3 播放器与AVSession整合
// 在AudioPlayer中集成AVSession
async initialize(context: Context): Promise<void> {
// 原有初始化...
await this.player.initialize();
// 创建媒体会话
this.avSession = new PlaybackSession();
await this.avSession.create(context, 'VoiceMemoPlayer');
// 将系统控制转发给播放器
this.avSession.on('play', () => this.play());
this.avSession.on('pause', () => this.pause());
this.avSession.on('seek', (time: number) => this.seek(time));
await this.avSession.activate();
}
// 播放时更新会话状态
async play(): Promise<void> {
await this.player.play();
await this.avSession?.setPlaybackState('playing', this.player.getCurrentTime());
}
async pause(): Promise<void> {
await this.player.pause();
await this.avSession?.setPlaybackState('paused', this.player.getCurrentTime());
}
五、小结与下篇预告
本文构建了一个完整的语音备忘录应用,覆盖三大核心能力:
AVPlayer音频播放:状态机管理、完整封装、进度监听、UI联动
AudioRecorder录音:AAC编码、振幅可视化、文件管理
AVSession媒体会话:锁屏控制、系统焦点、播放元数据同步
音视频开发的核心难点不在于API调用,而在于状态管理的复杂性和资源生命周期控制。AVPlayer的状态机、录音器的权限申请、媒体会话的系统协作,任何一环处理不当都会导致体验问题。
地图和位置服务是另一个高频领域能力。从定位获取到地图渲染,从POI搜索到轨迹绘制,涉及的Kit和API同样复杂。第15篇将深入鸿蒙地图与位置服务,讲解Map Kit的集成、定位精度控制、地理编码与逆编码,以及如何在元服务中优雅地使用地图能力。
参考阅读
本系列第13篇:《推送服务与后台任务:Push Kit与Work Scheduler实战》