1 注意事项
- DevEco Studio 4.0 Beta2(Build Version: 4.0.0.400)
- OpenHarmony SDK API 9
- 创建工程类型选择Application
- 修改entry/build-profile.json5配置文件中的targets>runtimeOS为OpenHarmony,然后进行Sync Now(同步)
2 工程概述
PPI有声是一款基于OpenHarmony API 9 开发的,运行于Purple Pi 开发板(安装OpenHarmony标准系统)的音频播放应用程序。
3 场景化
- 智慧家居类(电子门铃,温湿度显示仪,屏显灯控开关等)
- 智慧办公类(打卡机,大屏显示等)
- 智慧教育类(电子班牌,校园大屏,电子讲台等)
4 创建工程
- Project name:工程名称
- Bundle name:包名
- Save location:工程存储路径
- Compile SDK:编译API版本
- Compatible SDK:兼容的最新API版本
- Module name:模块名称
- Model:模型
- Enable Super Visual:是否启用低代码开发
- Device Type:设备类型
- Node:nodejs路径
5 媒体服务
媒体子系统为开发者提供一套简单且易于理解的接口,使得开发者能够方便接入系统使用系统的媒体资源。
媒体子系统包含了音视频相关媒体业务,提供以下常用共功能:
- 音视频播放(AVPlayer)
- 音视频录制(AVRecorder)
5.1 AVPlayer概述
AVPlayer主要工作是将Audio/Video媒体资源(比如mp4/mp3/mkv/mpeg-ts等)转码为可供渲染的图像或可听见的模拟信号,并通过输出设备进行播放。
使用AVPlayer可以实现端到端播放原始媒体资源,播放对的全流程包含:创建AVPlayer,设置播放资源,设置播放参数 (音量/倍速/焦点模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。
开发过程中开发者可以通过AVPlayer的state属性主动获取当前状态或使用on('stateChange')
方法监听状态变化。若应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。
主:当播放处于prepared/playing/paused/completed
状态时,播放引擎处于工作状态,需要占用系统较多的运行内容。当客户端暂时不适用播放器时,调用reset()或release()回收内存资源。
5.2 开发步骤
- 导入media模块,调用
createAVPlayer()
方法创建AVPlayer实例,AVPlayer初始化idle状态。
- 设置业务监听事件,搭配全流程场景使用,如监听播放器state属性改变的
stateChange
;监听播放器错误信息的error
;用于进度条,监听进度条长度,刷新资源时长的durationUpdate
等。
- 设置资源:设置属性url,AVPlayer进入initialized状态
- 准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置音量。
- 音频播控:播放play(),暂停pause(),跳转seek(),停止stop()等操作。
- 调用reset()重置资源,AVPlayer重新进入idle状态,此时可更换播放源url。
- 调用release()销毁实例,AVPlayer进入released状态,退出播放。
6 构建PPI有声
6.1 准备资源文件
- 音频文件拷贝到resources/rawfile目录
- 将拷贝到resources/base/mdiea目录
- 音频播放背景图audio_bg.png
- 音频播放旋转图audio.png
- 暂停ic_pause.svg
- 播放ic_play.svg
6.2 构建UI页面
整个UI以Flex弹性布局为主,子组件以列方式排列,分别为可旋转的音频播放控件,播放进度条以及播放控制按钮组成。
6.2.1 可旋转的音频播放控件
使用Stack堆叠布局容器为主,将旋转控件置于背景图之上。
Stack({ alignContent: Alignment.Center }) {
Image($r('app.media.audio_bg'))
.width(200).height(200)
Image($r('app.media.audio'))
.width(100).height(100)
.backgroundColor(Color.White)
.borderRadius(50)
.rotate({ angle: this.angleNum })
.animation({
duration: this.duration,
tempo: 1,
curve: Curve.Linear,
iterations: -1,
playMode: PlayMode.Normal
})
}
6.2.2 进度条
播放进度由置于上部的播放时长和总时长,底部的播放进度条组成,包裹在Column列容器中。
Column({ space: 4 }) {
Row() {
Text(this.msToS(this.currentProgress))
.fontSize(12)
.fontColor(0xc1c3c5)
Text(this.msToS(this.duration))
.fontSize(12)
.fontColor(0xc1c3c5)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Slider({
value: this.currentProgress,
min: 0,
max: this.duration,
style: SliderStyle.OutSet
})
.showTips(true)
.onChange((value: number, mode: SliderChangeMode) => {
this.currentProgress = value;
this.avPlayer.seek(value);
})
}
.width('90%')
6.2.3 播放控件
播放控件通过当前AVPlayer的状态判断显示播放/暂停图标按钮。
Row({ space: 10 }) {
if (this.state === 'playing') {
Image($r('app.media.ic_pause'))
.width(64).height(64)
.fillColor(0xff5722)
.onClick(() => {
this.avPlayer.pause().then(() => {
this.angleNum = 0;
})
})
} else {
Image($r('app.media.ic_play'))
.width(64).height(64)
.fillColor(0x00aaee)
.onClick(async () => {
if (this.avPlayer && this.avPlayer.state === "paused") {
this.avPlayer.play().then(() => {
this.angleNum = 360;
})
} else {
await this.initAVPlayer();
}
})
}
}
.width('100%')
.justifyContent(FlexAlign.Center)
6.3 实现音频播放
6.3.1 初始化AVPlayer
private avPlayer: media.AVPlayer = undefined;
async initAVPlayer() {
this.avPlayer = await media.createAVPlayer();
this.setAVPlayerCallback();
await this.loadingResourceFile();
}
6.3.2 加载HAP包资源文件
loadingResourceFile = async () => {
let context = getContext(this) as common.UIAbilityContext;
let fileDir = await context.resourceManager.getRawFd("audio.wav");
this.avPlayer.fdSrc = fileDir;
}
6.3.3 注册AVPlayer回调函数
setAVPlayerCallback = () => {
this.avPlayer.on('stateChange', async (state, reason) => {
this.state = this.avPlayer.state;
switch (state) {
case 'initialized':
this.avPlayer.prepare().then(() => {
this.duration = this.avPlayer.duration;
})
break;
case 'prepared':
this.avPlayer.play().then(() => {
this.angleNum = 360;
})
break;
}
})
this.avPlayer.on('error', (err) => {
console.error(`Error happened. Cause: ${JSON.stringify(err)}`);
})
this.avPlayer.on('timeUpdate', (time: number) => {
if (this.avPlayer.state === 'completed') {
this.currentProgress = 0;
this.duration = 0;
this.angleNum = 0;
} else {
this.currentProgress = time;
}
})
}
7 效果预览