鴻蒙HarmonyOS NEXT開發(fā):音頻播放及長(zhǎng)時(shí)任務(wù)(熄屏播放,后臺(tái)播放)的應(yīng)用

一、音頻播放

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的行為。

image.png

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、效果展示

image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容