文章目录
一、音频播放
1、如何选择音频播放开发方式
系统提供了多样化的API,来帮助开发者完成音频播放的开发,不同的API适用于不同音频数据格式、音频资源来源、音频使用场景,甚至是不同开发语言。因此,选择合适的音频播放API,有助于降低开发工作量,实现更佳的音频播放效果。
-
AudioRenderer:用于音频输出的ArkTS/JS API,仅支持PCM格式,需要应用持续写入音频数据进行工作。应用可以在输入前添加数据预处理,如设定音频文件的采样率、位宽等,要求开发者具备音频处理的基础知识,适用于更专业、更多样化的媒体播放应用开发。
-
AudioHaptic:用于音振协同播放的ArkTS/JS API,适用于需要在播放音频时同步发起振动的场景,如来电铃声随振、键盘按键反馈、消息通知反馈等。
-
OpenSL ES:一套跨平台标准化的音频Native API,同样提供音频输出能力,仅支持PCM格式,适用于从其他嵌入式平台移植,或依赖在Native层实现音频输出功能的播放应用使用。
-
OHAudio:用于音频输出的Native API,此API在设计上实现归一,同时支持普通音频通路和低时延通路。仅支持PCM格式,适用于依赖Native层实现音频输出功能的场景。
-
AVPlayer:用于音频播放的ArkTS/JS API,集成了流媒体和本地资源解析、媒体资源解封装、音频解码和音频输出功能。可用于直接播放mp3、m4a等格式的音频文件,不支持直接播放PCM格式文件。
-
SoundPool:低时延的短音播放ArkTS/JS API,适用于播放急促简短的音效,如相机快门音效、按键音效、游戏射击音效等。
二、Media Kit简介
Media Kit(媒体服务)提供了AVPlayer和AVRecorder用于播放、录制音视频。
在Media Kit的开发指导中,将介绍各种涉及音频、视频播放或录制功能场景的开发方式,指导开发者如何使用系统提供的音视频API实现对应功能。比如使用SoundPool实现简单的提示音,当设备接收到新消息时,会发出短促的“滴滴”声;使用AVPlayer实现音乐播放器,循环播放一首音乐。
1、亮点/特征
-
使用轻量媒体引擎
使用较少的系统资源(线程、内存),可支持音视频播放/录制,支持pipeline灵活拼装,支持插件化扩展source/demuxer/codec。 -
支持HDR视频
系统原生数据结构与接口支持hdr vivid的采集与播放,方便三方应用在业务中使用系统的HDR能力,为用户带来更炫彩的体验。 -
支持音频池
针对开发中常用的短促音效播放场景,如相机快门音效、系统通知音效等,应用可调用SoundPool,实现一次加载,多次低时延播放。
2、AVPlayer
AVPlayer主要工作是将Audio/Video媒体资源(比如mp4/mp3/mkv/mpeg-ts等)转码为可供渲染的图像和可听见的音频模拟信号,并通过输出设备进行播放。
AVPlayer提供功能完善一体化播放能力,应用只需要提供流媒体来源,不负责数据解析和解码就可达成播放效果。
3、使用AVPlayer开发音频播放功能(ArkTS)
使用AVPlayer可以实现端到端播放原始媒体资源,本开发指导将以完整地播放一首音乐作为示例,向开发者讲解AVPlayer音频播放相关功能。
播放的全流程包含:创建AVPlayer,设置播放资源,设置播放参数(音量/倍速/焦点模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。
在进行应用开发的过程中,开发者可以通过AVPlayer的state属性主动获取当前状态或使用on(‘stateChange’)方法监听状态变化。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。
4、开发步骤
-
创建实例createAVPlayer(),AVPlayer初始化idle状态。
-
设置业务需要的监听事件,搭配全流程场景使用。
-
设置资源:设置属性url,AVPlayer进入initialized状态。
-
准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置音量。
-
音频播控:播放play(),暂停pause(),跳转seek(),停止stop() 等操作。
-
更换资源:调用reset()重置资源,AVPlayer重新进入idle状态,允许更换资源url。
-
退出播放:调用release()销毁实例,AVPlayer进入released状态,退出播放。
支持的监听事件包括
事件类型 | 说明 |
---|---|
stateChange | 必要事件,监听播放器的state属性改变。 |
error | 必要事件,监听播放器的错误信息。 |
durationUpdate | 用于进度条,监听进度条长度,刷新资源时长。 |
timeUpdate | 用于进度条,监听进度条当前位置,刷新当前时间。 |
seekDone | 响应API调用,监听seek()请求完成情况。当使用seek()跳转到指定播放位置后,如果seek操作成功,将上报该事件。 |
speedDone | 响应API调用,监听setSpeed()请求完成情况。当使用setSpeed()设置播放倍速后,如果setSpeed操作成功,将上报该事件。 |
volumeChange | 响应API调用,监听setVolume()请求完成情况。当使用setVolume()调节播放音量后,如果setVolume操作成功,将上报该事件。 |
bufferingUpdate | 用于网络播放,监听网络播放缓冲信息,用于上报缓冲百分比以及缓存播放进度。 |
audioInterrupt | 监听音频焦点切换信息,搭配属性audioInterruptMode使用。如果当前设备存在多个音频正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。 |
5、后台播放
应用如果要实现后台播放或熄屏播放,需要同时满足:
-
使用媒体会话功能注册到系统内统一管理,否则在应用进入后台时,播放将被强制停止。具体参考AVSession Kit开发指导。
-
申请长时任务避免进入挂起(Suspend)状态。具体参考长时任务开发指导。
当应用进入后台,播放被中断,如果被媒体会话管控,将打印日志“pause id”;如果没有该日志,则说明被长时任务管控。
三、长时任务
应用退至后台后,在后台需要长时间运行用户可感知的任务,如播放音乐、导航等。为防止应用进程被挂起,导致对应功能异常,可以申请长时任务,使应用在后台长时间运行。
申请长时任务后,系统会做相应的校验,确保应用在执行相应的长时任务。同时,系统有与长时任务相关联的通知栏消息,用户删除通知栏消息时,系统会自动停止长时任务。
1、使用场景
下表给出了当前长时任务支持的类型,包含数据传输、音视频播放、录制、定位导航、蓝牙相关、多设备互联、WLAN相关、音视频通话和计算任务。可以参考下表中的场景举例,选择合适的长时任务类型。
参数名 | 描述 | 配置项 | 场景举例 |
---|---|---|---|
DATA_TRANSFER | 数据传输 | dataTransfer | 后台下载大文件,如浏览器后台下载等。 |
AUDIO_PLAYBACK | 音视频播放 | audioPlayback | 音乐类应用在后台播放音乐。 |
AUDIO_RECORDING | 录制 | audioRecording | 录音机在后台录音。 |
LOCATION | 定位导航 | location | 导航类应用后台导航。 |
BLUETOOTH_INTERACTION | 蓝牙相关 | bluetoothInteraction | 通过蓝牙传输分享的文件。 |
MULTI_DEVICE_CONNECTION | 多设备互联 | multiDeviceConnection | 分布式业务连接。 |
TASK_KEEPING | 计算任务(仅对2IN1开放) | taskKeeping | 杀毒软件。 |
2、接口说明
接口名 | 描述 |
---|---|
startBackgroundRunning(context: Context, bgMode: BackgroundMode, wantAgent: WantAgent): Promise | 申请长时任务 |
stopBackgroundRunning(context: Context): Promise | 取消长时任务 |
3、开发步骤
- 需要申请ohos.permission.KEEP_BACKGROUND_RUNNING权限。
// module.json5
"requestPermissions": [
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}
]
- 声明后台模式类型,以及添加uris等配置。
声明后台模式类型(必填项):在 module.json5 配置文件中为需要使用长时任务的UIAbility声明相应的长时任务类型(配置文件中填写长时任务类型的配置项)。
// module.json5
"module": {
"abilities": [
{
"backgroundModes": [
// 长时任务类型的配置项
"audioRecording"
],
"skills": [
// 必填项:申请长时任务时entities和actions值
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
},
// 可选项:添加deeplink、applink等跳转功能
{
"entities": [
"test"
],
"actions": [
"test"
],
"uris": [
{
"scheme": "test"
}
]
}
]
}
],
...
}
- 导入模块。
长时任务相关的模块为@ohos.resourceschedule.backgroundTaskManager和@ohos.app.ability.wantAgent,其余模块按实际需要导入。
- 申请和取消长时任务。
四、完整示例
本文以来电接听为例,播放铃声-循环播放,支持熄屏播放。
点击“接听”按钮播放电话内容录音,单次播放。
1、封装音频播放
// AVPlayerManager.ets
import {
media } from '@kit.MediaKit';
class AVPlayerManager {
private avPlayer: media.AVPlayer | null = null
private loop: boolean = false
async getAVPlayerInstance() {
// 如果已存在,直接返回
if (this.avPlayer !== null) {
return this.avPlayer
}
// 初始化播放器
const player = await media.createAVPlayer()
player.on('stateChange', (state) => {
switch (state) {
case 'initialized':
player.prepare()
break;
case 'prepared':
player.play()
break;
case 'playing':
player.play()
break;
case 'paused':
player.pause()
break;
case 'completed':
if (this.loop === true) {
player.play() // 播放结束继续播放:循环播放
} else {
player.stop() // 播放结束
}
break;
case 'stopped':
player.reset() // stop 时 reset -> 释放音频资源
break;
default:
break;
}
})
this.avPlayer = player
return this.avPlayer
}
// 加载 src/main/resources/rawfile 的文件
async playByRawSrc(rawFdPath: string) {
const player = await this.getAVPlayerInstance()
// 先释放原来的资源
await player.reset()
// 获取文件信息
const context = getContext()
// 加载 src/main/resources/rawfile 文件夹中的文件
const fileDescriptor = await context.resourceManager.getRawFd(rawFdPath)
// 设置播放路径
player.fdSrc = fileDescriptor
// 播放
player.play()
}
// 停止播放
async stop() {
const player = await this.getAVPlayerInstance()
this.loop = false
player.stop()
}
// 设置循环播放
async setLoop(isLoop: boolean) {
this.loop = isLoop
}
}
export const avPlayerManager = new AVPlayerManager()
2、封装熄屏播放
// BackgroundRunningManager.ets
import {
bundleManager, wantAgent } from '@kit.AbilityKit'
import {
avSession } from '@kit.AVSessionKit'
import {
backgroundTaskManager } from '@kit.BackgroundTasksKit'
class BackgroundRunningManager {
// 申请长时任务
async startBackgroundRunning() {
const context = getContext()
// 重点1: 提供音频后台约束能力,音频接入AVSession后,可以进行后台音频播放
const session = await avSession.createAVSession(context, 'guardianSession', 'audio')
await session.activate()
// 获取 bundle 应用信息
const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
// 通过wantAgent模块下getWantAgent方法获取WantAgent对象
const wantAgentObj = await wantAgent.getWantAgent({
// 添加需要被拉起应用的bundleName和abilityName
wants: [{
bundleName: bundleInfo.name, abilityName: "EntryAbility" }],
// 使用者自定义的一个私有值
requestCode: 0,
})
// 重点2: 创建后台任务
await backgroundTaskManager.startBackgroundRunning(context,
backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgentObj)
}
// 停止后台任务
async stopBackgroundRunning() {
backgroundTaskManager.stopBackgroundRunning(getContext())
}
}
export const backgroundRunningManager = new BackgroundRunningManager()
3、结合使用
// index.ets
import {
avPlayerManager } from './avPlayerManager'
import {
backgroundRunningManager } from './backgroundRunningManager'
interface ButtonItem {
name: string
icon: Resource
}
@Entry
@Component
struct Index {
@State isAnswering: boolean = false
@State buttonList: ButtonItem[] = [
{
name: '静音', icon: $r('app.media.ic_fake_tel_jy') },
{
name: '拨号键盘', icon: $r('app.media.ic_fake_tel_bhjp') },
{
name: '免提', icon: $r('app.media.ic_fake_tel_mt') },
{
name: '添加通话', icon: $r("app.media.ic_fake_tel_tjth") },
{
name: '视频通话', icon: $r("app.media.ic_fake_tel_spth") },
{
name: '通讯录', icon: $r('app.media.ic_fake_tel_txl') },
]
// 页面加载
aboutToAppear() {
// 播放来电音频
this.playCallRing()
}
// 页面卸载
aboutToDisappear() {
this.stopCallRing()
}
// 播放来电音频
playCallRing() {
// 播放本地音频
avPlayerManager.playByRawSrc('lab_call_ring.mp3')
// 循环播放
avPlayerManager.setLoop(true)
// 开启后台播放(熄屏播放)
backgroundRunningManager.startBackgroundRunning()
}
// 模拟接听电话
onAnswering() {
// 显示接听界面
this.isAnswering = true
// 播放预先录制好的声音
avPlayerManager.playByRawSrc('lab_voice_trick_5.m4a')
// 取消循环播放
avPlayerManager.setLoop(false)
}
// 挂断
stopCallRing() {
// 停止音频播放
avPlayerManager.stop()
// 关闭后台任务,释放资源
backgroundRunningManager.stopBackgroundRunning()
// 隐藏接听界面
this.isAnswering=false
}
build() {
Column() {
Column() {
// 顶部
Column({
space: 10 }) {
Text('未知')
.fontSize(32)
.fontColor('#fff')
Text('中国移动')
.fontSize(16)
.fontColor('#fff')
}
// 占剩余空间
Blank()
// 是否接听
if (this.isAnswering) {
GridRow({
columns: 3 }) {
ForEach(this.buttonList, (item: ButtonItem) => {
GridCol() {
Column({
space: 10 }) {
Image(item.icon)
.height(72)
Text(item.name)
.fontSize(14)
.fontColor('#fff')
}
.width('100%')
.padding(10)
}
})
}
.padding({
left: 20, right: 20 })
// 挂断
Image($r('app.media.ic_fake_tel_gd'))
.width(72)
.margin({
top: 100, bottom: 50 })
.onClick(() => {
this.stopCallRing()
})
} else {
Column({
space: 40 }) {
Row() {
Column({
space: 6 }) {
Image($r('app.media.ic_fake_tel_txw'))
.height(28)
Text('提醒我')
.fontSize(14)
.fontColor('#fff')
}
Column({
space: 6 }) {
Image($r('app.media.ic_fake_tel_fxx'))
.height(28)
Text('发消息')
.fontSize(14)
.fontColor('#fff')
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
Row() {
// 挂断
Column({
space: 6 }) {
Image($r("app.media.ic_fake_tel_gd"))
.height(60)
Text('挂断')
.fontSize(14)
.fontColor('#fff')
}
.onClick(() => {
this.stopCallRing()
})
// 接听
Column({
space: 6 }) {
Image($r("app.media.ic_fake_tel_jt"))
.height(60)
Text('接听')
.fontSize(14)
.fontColor('#fff')
}
.onClick(() => {
this.onAnswering()
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
}
}
}
.height('100%')
.padding({
top: 50, bottom: 50 })
}
.height('100%')
.width('100%')
.linearGradient({
angle: 180,
colors: [['#132631', 0], ['#173749', 0.25], ['#183E52', 0.5], ['#273046', 0.75], ['#162634', 1]]
})
}
}