鸿蒙HarmonyOS开发:音频播放及长时任务(熄屏播放,后台播放)的应用

一、音频播放

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]]
    })
  }
}
4、效果展示

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/shanghai597/article/details/142859331