一、音頻播放
1、如何選擇音頻播放開發(fā)方式
系統(tǒng)提供了多樣化的API,來(lái)幫助開發(fā)者完成音頻播放的開發(fā),不同的API適用于不同音頻數(shù)據(jù)格式、音頻資源來(lái)源、音頻使用場(chǎng)景,甚至是不同開發(fā)語(yǔ)言。因此,選擇合適的音頻播放API,有助于降低開發(fā)工作量,實(shí)現(xiàn)更佳的音頻播放效果。
AudioRenderer:用于音頻輸出的ArkTS/JS API,僅支持PCM格式,需要應(yīng)用持續(xù)寫入音頻數(shù)據(jù)進(jìn)行工作。應(yīng)用可以在輸入前添加數(shù)據(jù)預(yù)處理,如設(shè)定音頻文件的采樣率、位寬等,要求開發(fā)者具備音頻處理的基礎(chǔ)知識(shí),適用于更專業(yè)、更多樣化的媒體播放應(yīng)用開發(fā)。
AudioHaptic:用于音振協(xié)同播放的ArkTS/JS API,適用于需要在播放音頻時(shí)同步發(fā)起振動(dòng)的場(chǎng)景,如來(lái)電鈴聲隨振、鍵盤按鍵反饋、消息通知反饋等。
OpenSL ES:一套跨平臺(tái)標(biāo)準(zhǔn)化的音頻Native API,同樣提供音頻輸出能力,僅支持PCM格式,適用于從其他嵌入式平臺(tái)移植,或依賴在Native層實(shí)現(xiàn)音頻輸出功能的播放應(yīng)用使用。
OHAudio:用于音頻輸出的Native API,此API在設(shè)計(jì)上實(shí)現(xiàn)歸一,同時(shí)支持普通音頻通路和低時(shí)延通路。僅支持PCM格式,適用于依賴Native層實(shí)現(xiàn)音頻輸出功能的場(chǎng)景。
AVPlayer:用于音頻播放的ArkTS/JS API,集成了流媒體和本地資源解析、媒體資源解封裝、音頻解碼和音頻輸出功能。可用于直接播放mp3、m4a等格式的音頻文件,不支持直接播放PCM格式文件。
SoundPool:低時(shí)延的短音播放ArkTS/JS API,適用于播放急促簡(jiǎn)短的音效,如相機(jī)快門音效、按鍵音效、游戲射擊音效等。
二、Media Kit簡(jiǎn)介
Media Kit(媒體服務(wù))提供了AVPlayer和AVRecorder用于播放、錄制音視頻。
在Media Kit的開發(fā)指導(dǎo)中,將介紹各種涉及音頻、視頻播放或錄制功能場(chǎng)景的開發(fā)方式,指導(dǎo)開發(fā)者如何使用系統(tǒng)提供的音視頻API實(shí)現(xiàn)對(duì)應(yīng)功能。比如使用SoundPool實(shí)現(xiàn)簡(jiǎn)單的提示音,當(dāng)設(shè)備接收到新消息時(shí),會(huì)發(fā)出短促的“滴滴”聲;使用AVPlayer實(shí)現(xiàn)音樂播放器,循環(huán)播放一首音樂。
1、亮點(diǎn)/特征
使用輕量媒體引擎
使用較少的系統(tǒng)資源(線程、內(nèi)存),可支持音視頻播放/錄制,支持pipeline靈活拼裝,支持插件化擴(kuò)展source/demuxer/codec。支持HDR視頻
系統(tǒng)原生數(shù)據(jù)結(jié)構(gòu)與接口支持hdr vivid的采集與播放,方便三方應(yīng)用在業(yè)務(wù)中使用系統(tǒng)的HDR能力,為用戶帶來(lái)更炫彩的體驗(yàn)。支持音頻池
針對(duì)開發(fā)中常用的短促音效播放場(chǎng)景,如相機(jī)快門音效、系統(tǒng)通知音效等,應(yīng)用可調(diào)用SoundPool,實(shí)現(xiàn)一次加載,多次低時(shí)延播放。
2、AVPlayer
AVPlayer主要工作是將Audio/Video媒體資源(比如mp4/mp3/mkv/mpeg-ts等)轉(zhuǎn)碼為可供渲染的圖像和可聽見的音頻模擬信號(hào),并通過(guò)輸出設(shè)備進(jìn)行播放。
AVPlayer提供功能完善一體化播放能力,應(yīng)用只需要提供流媒體來(lái)源,不負(fù)責(zé)數(shù)據(jù)解析和解碼就可達(dá)成播放效果。
3、使用AVPlayer開發(fā)音頻播放功能(ArkTS)
使用AVPlayer可以實(shí)現(xiàn)端到端播放原始媒體資源,本開發(fā)指導(dǎo)將以完整地播放一首音樂作為示例,向開發(fā)者講解AVPlayer音頻播放相關(guān)功能。
播放的全流程包含:創(chuàng)建AVPlayer,設(shè)置播放資源,設(shè)置播放參數(shù)(音量/倍速/焦點(diǎn)模式),播放控制(播放/暫停/跳轉(zhuǎn)/停止),重置,銷毀資源。
在進(jìn)行應(yīng)用開發(fā)的過(guò)程中,開發(fā)者可以通過(guò)AVPlayer的state屬性主動(dòng)獲取當(dāng)前狀態(tài)或使用on('stateChange')方法監(jiān)聽狀態(tài)變化。如果應(yīng)用在音頻播放器處于錯(cuò)誤狀態(tài)時(shí)執(zhí)行操作,系統(tǒng)可能會(huì)拋出異?;蛏善渌炊x的行為。

4、開發(fā)步驟
創(chuàng)建實(shí)例createAVPlayer(),AVPlayer初始化idle狀態(tài)。
設(shè)置業(yè)務(wù)需要的監(jiān)聽事件,搭配全流程場(chǎng)景使用。
設(shè)置資源:設(shè)置屬性u(píng)rl,AVPlayer進(jìn)入initialized狀態(tài)。
準(zhǔn)備播放:調(diào)用prepare(),AVPlayer進(jìn)入prepared狀態(tài),此時(shí)可以獲取duration,設(shè)置音量。
音頻播控:播放play(),暫停pause(),跳轉(zhuǎn)seek(),停止stop() 等操作。
更換資源:調(diào)用reset()重置資源,AVPlayer重新進(jìn)入idle狀態(tài),允許更換資源url。
退出播放:調(diào)用release()銷毀實(shí)例,AVPlayer進(jìn)入released狀態(tài),退出播放。
支持的監(jiān)聽事件包括
| 事件類型 | 說(shuō)明 |
|---|---|
| stateChange | 必要事件,監(jiān)聽播放器的state屬性改變。 |
| error | 必要事件,監(jiān)聽播放器的錯(cuò)誤信息。 |
| durationUpdate | 用于進(jìn)度條,監(jiān)聽進(jìn)度條長(zhǎng)度,刷新資源時(shí)長(zhǎng)。 |
| timeUpdate | 用于進(jìn)度條,監(jiān)聽進(jìn)度條當(dāng)前位置,刷新當(dāng)前時(shí)間。 |
| seekDone | 響應(yīng)API調(diào)用,監(jiān)聽seek()請(qǐng)求完成情況。當(dāng)使用seek()跳轉(zhuǎn)到指定播放位置后,如果seek操作成功,將上報(bào)該事件。 |
| speedDone | 響應(yīng)API調(diào)用,監(jiān)聽setSpeed()請(qǐng)求完成情況。當(dāng)使用setSpeed()設(shè)置播放倍速后,如果setSpeed操作成功,將上報(bào)該事件。 |
| volumeChange | 響應(yīng)API調(diào)用,監(jiān)聽setVolume()請(qǐng)求完成情況。當(dāng)使用setVolume()調(diào)節(jié)播放音量后,如果setVolume操作成功,將上報(bào)該事件。 |
| bufferingUpdate | 用于網(wǎng)絡(luò)播放,監(jiān)聽網(wǎng)絡(luò)播放緩沖信息,用于上報(bào)緩沖百分比以及緩存播放進(jìn)度。 |
| audioInterrupt | 監(jiān)聽音頻焦點(diǎn)切換信息,搭配屬性audioInterruptMode使用。如果當(dāng)前設(shè)備存在多個(gè)音頻正在播放,音頻焦點(diǎn)被切換(即播放其他媒體如通話等)時(shí)將上報(bào)該事件,應(yīng)用可以及時(shí)處理。 |
5、后臺(tái)播放
應(yīng)用如果要實(shí)現(xiàn)后臺(tái)播放或熄屏播放,需要同時(shí)滿足:
使用媒體會(huì)話功能注冊(cè)到系統(tǒng)內(nèi)統(tǒng)一管理,否則在應(yīng)用進(jìn)入后臺(tái)時(shí),播放將被強(qiáng)制停止。具體參考AVSession Kit開發(fā)指導(dǎo)。
申請(qǐng)長(zhǎng)時(shí)任務(wù)避免進(jìn)入掛起(Suspend)狀態(tài)。具體參考長(zhǎng)時(shí)任務(wù)開發(fā)指導(dǎo)。
當(dāng)應(yīng)用進(jìn)入后臺(tái),播放被中斷,如果被媒體會(huì)話管控,將打印日志“pause id”;如果沒有該日志,則說(shuō)明被長(zhǎng)時(shí)任務(wù)管控。
三、長(zhǎng)時(shí)任務(wù)
應(yīng)用退至后臺(tái)后,在后臺(tái)需要長(zhǎng)時(shí)間運(yùn)行用戶可感知的任務(wù),如播放音樂、導(dǎo)航等。為防止應(yīng)用進(jìn)程被掛起,導(dǎo)致對(duì)應(yīng)功能異常,可以申請(qǐng)長(zhǎng)時(shí)任務(wù),使應(yīng)用在后臺(tái)長(zhǎng)時(shí)間運(yùn)行。
申請(qǐng)長(zhǎng)時(shí)任務(wù)后,系統(tǒng)會(huì)做相應(yīng)的校驗(yàn),確保應(yīng)用在執(zhí)行相應(yīng)的長(zhǎng)時(shí)任務(wù)。同時(shí),系統(tǒng)有與長(zhǎng)時(shí)任務(wù)相關(guān)聯(lián)的通知欄消息,用戶刪除通知欄消息時(shí),系統(tǒng)會(huì)自動(dòng)停止長(zhǎng)時(shí)任務(wù)。
1、使用場(chǎng)景
下表給出了當(dāng)前長(zhǎng)時(shí)任務(wù)支持的類型,包含數(shù)據(jù)傳輸、音視頻播放、錄制、定位導(dǎo)航、藍(lán)牙相關(guān)、多設(shè)備互聯(lián)、WLAN相關(guān)、音視頻通話和計(jì)算任務(wù)??梢詤⒖枷卤碇械膱?chǎng)景舉例,選擇合適的長(zhǎng)時(shí)任務(wù)類型。
| 參數(shù)名 | 描述 | 配置項(xiàng) | 場(chǎng)景舉例 |
|---|---|---|---|
| DATA_TRANSFER | 數(shù)據(jù)傳輸 | dataTransfer | 后臺(tái)下載大文件,如瀏覽器后臺(tái)下載等。 |
| AUDIO_PLAYBACK | 音視頻播放 | audioPlayback | 音樂類應(yīng)用在后臺(tái)播放音樂。 |
| AUDIO_RECORDING | 錄制 | audioRecording | 錄音機(jī)在后臺(tái)錄音。 |
| LOCATION | 定位導(dǎo)航 | location | 導(dǎo)航類應(yīng)用后臺(tái)導(dǎo)航。 |
| BLUETOOTH_INTERACTION | 藍(lán)牙相關(guān) | bluetoothInteraction | 通過(guò)藍(lán)牙傳輸分享的文件。 |
| MULTI_DEVICE_CONNECTION | 多設(shè)備互聯(lián) | multiDeviceConnection | 分布式業(yè)務(wù)連接。 |
| TASK_KEEPING | 計(jì)算任務(wù)(僅對(duì)2IN1開放) | taskKeeping | 殺毒軟件。 |
2、接口說(shuō)明
| 接口名 | 描述 |
|---|---|
| startBackgroundRunning(context: Context, bgMode: BackgroundMode, wantAgent: WantAgent): Promise<void> | 申請(qǐng)長(zhǎng)時(shí)任務(wù) |
| stopBackgroundRunning(context: Context): Promise<void> | 取消長(zhǎng)時(shí)任務(wù) |
3、開發(fā)步驟
- 需要申請(qǐng)ohos.permission.KEEP_BACKGROUND_RUNNING權(quán)限。
// module.json5
"requestPermissions": [
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}
]
- 聲明后臺(tái)模式類型,以及添加uris等配置。
聲明后臺(tái)模式類型(必填項(xiàng)):在 module.json5 配置文件中為需要使用長(zhǎng)時(shí)任務(wù)的UIAbility聲明相應(yīng)的長(zhǎng)時(shí)任務(wù)類型(配置文件中填寫長(zhǎng)時(shí)任務(wù)類型的配置項(xiàng))。
// module.json5
"module": {
"abilities": [
{
"backgroundModes": [
// 長(zhǎng)時(shí)任務(wù)類型的配置項(xiàng)
"audioRecording"
],
"skills": [
// 必填項(xiàng):申請(qǐng)長(zhǎng)時(shí)任務(wù)時(shí)entities和actions值
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
},
// 可選項(xiàng):添加deeplink、applink等跳轉(zhuǎn)功能
{
"entities": [
"test"
],
"actions": [
"test"
],
"uris": [
{
"scheme": "test"
}
]
}
]
}
],
...
}
- 導(dǎo)入模塊。
長(zhǎng)時(shí)任務(wù)相關(guān)的模塊為@ohos.resourceschedule.backgroundTaskManager和@ohos.app.ability.wantAgent,其余模塊按實(shí)際需要導(dǎo)入。
- 申請(qǐng)和取消長(zhǎng)時(shí)任務(wù)。
四、完整示例
本文以來(lái)電接聽為例,播放鈴聲-循環(huán)播放,支持熄屏播放。
點(diǎn)擊“接聽”按鈕播放電話內(nèi)容錄音,單次播放。
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() // 播放結(jié)束繼續(xù)播放:循環(huán)播放
} else {
player.stop() // 播放結(jié)束
}
break;
case 'stopped':
player.reset() // stop 時(shí) reset -> 釋放音頻資源
break;
default:
break;
}
})
this.avPlayer = player
return this.avPlayer
}
// 加載 src/main/resources/rawfile 的文件
async playByRawSrc(rawFdPath: string) {
const player = await this.getAVPlayerInstance()
// 先釋放原來(lái)的資源
await player.reset()
// 獲取文件信息
const context = getContext()
// 加載 src/main/resources/rawfile 文件夾中的文件
const fileDescriptor = await context.resourceManager.getRawFd(rawFdPath)
// 設(shè)置播放路徑
player.fdSrc = fileDescriptor
// 播放
player.play()
}
// 停止播放
async stop() {
const player = await this.getAVPlayerInstance()
this.loop = false
player.stop()
}
// 設(shè)置循環(huán)播放
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 {
// 申請(qǐng)長(zhǎng)時(shí)任務(wù)
async startBackgroundRunning() {
const context = getContext()
// 重點(diǎn)1: 提供音頻后臺(tái)約束能力,音頻接入AVSession后,可以進(jìn)行后臺(tái)音頻播放
const session = await avSession.createAVSession(context, 'guardianSession', 'audio')
await session.activate()
// 獲取 bundle 應(yīng)用信息
const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
// 通過(guò)wantAgent模塊下getWantAgent方法獲取WantAgent對(duì)象
const wantAgentObj = await wantAgent.getWantAgent({
// 添加需要被拉起應(yīng)用的bundleName和abilityName
wants: [{ bundleName: bundleInfo.name, abilityName: "EntryAbility" }],
// 使用者自定義的一個(gè)私有值
requestCode: 0,
})
// 重點(diǎn)2: 創(chuàng)建后臺(tái)任務(wù)
await backgroundTaskManager.startBackgroundRunning(context,
backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgentObj)
}
// 停止后臺(tái)任務(wù)
async stopBackgroundRunning() {
backgroundTaskManager.stopBackgroundRunning(getContext())
}
}
export const backgroundRunningManager = new BackgroundRunningManager()
3、結(jié)合使用
// 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: '撥號(hào)鍵盤', 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') },
]
// 頁(yè)面加載
aboutToAppear() {
// 播放來(lái)電音頻
this.playCallRing()
}
// 頁(yè)面卸載
aboutToDisappear() {
this.stopCallRing()
}
// 播放來(lái)電音頻
playCallRing() {
// 播放本地音頻
avPlayerManager.playByRawSrc('lab_call_ring.mp3')
// 循環(huán)播放
avPlayerManager.setLoop(true)
// 開啟后臺(tái)播放(熄屏播放)
backgroundRunningManager.startBackgroundRunning()
}
// 模擬接聽電話
onAnswering() {
// 顯示接聽界面
this.isAnswering = true
// 播放預(yù)先錄制好的聲音
avPlayerManager.playByRawSrc('lab_voice_trick_5.m4a')
// 取消循環(huán)播放
avPlayerManager.setLoop(false)
}
// 掛斷
stopCallRing() {
// 停止音頻播放
avPlayerManager.stop()
// 關(guān)閉后臺(tái)任務(wù),釋放資源
backgroundRunningManager.stopBackgroundRunning()
// 隱藏接聽界面
this.isAnswering=false
}
build() {
Column() {
Column() {
// 頂部
Column({ space: 10 }) {
Text('未知')
.fontSize(32)
.fontColor('#fff')
Text('中國(guó)移動(dòng)')
.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('發(fā)消息')
.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]]
})
}
}
4、效果展示
