HarmonyOS 鸿蒙Next基于AVPlayer音频后台播放

发布于 1周前 作者 vueper 来自 鸿蒙OS

HarmonyOS 鸿蒙Next基于AVPlayer音频后台播放
<markdown _ngcontent-yck-c237="" class="markdownPreContainer">

场景描述

音乐播放是媒体最重要的组成之一,以下是AVPlayer将Audio媒体资源(比如mp3等)转码为可听见的音频模拟信号,并通过输出设备进行播放。

场景一:使用avPlayer进行后台播放音乐

想要实现应用后台播放,那么接入AVSession是必须的,否则业务的正常功能会同时受到限制,也必须有BackgroundTasks Kit(后台任务管理)的能力,申请对应的长时任务,避免进入挂起(Suspend)状态。

应用不申请后台任务会被冻结,不注册AVSession会被暂停。

步骤一:创建avPlayer实现音频播放

创建avPlayer并加载音频资源

async avPlayerFdSrcDemo() {
  // 创建avPlayer实例对象
  avPlayer = await media.createAVPlayer();
  // 创建状态机变化回调函数
  this.setAVPlayerCallback(avPlayer);
  // 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址
  let context = getContext(this) as common.UIAbilityContext;
  let fileDescriptor = await context.resourceManager.getRawFd('123.mp3');
  // 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度
  let avFileDescriptor: media.AVFileDescriptor =
    { fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };
  this.isSeek = true; // 支持seek操作
  // 为fdSrc赋值触发initialized状态机上报
  avPlayer.fdSrc = avFileDescriptor;
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

注册avPlayer回调函数

// 注册avplayer回调函数
setAVPlayerCallback(avPlayer: media.AVPlayer) {
  // seek操作结果回调函数
  avPlayer.on('seekDone', (seekDoneTime: number) => {
    console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
  })
  // error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程
  avPlayer.on('error', (err: BusinessError) => {
    console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
    avPlayer.reset(); // 调用reset重置资源,触发idle状态
  })
  // 状态机变化回调函数
  avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
    switch (state) {
      case 'idle': // 成功调用reset接口后触发该状态机上报
        console.info('AVPlayer state idle called.');
        avPlayer.release(); // 调用release接口销毁实例对象
        break;
      case 'initialized': // avplayer 设置播放源后触发该状态上报
        console.info('AVPlayer state initialized called.');
        avPlayer.prepare();
        break;
      case 'prepared': // prepare调用成功后上报该状态机
        console.info('AVPlayer state prepared called.');
        avPlayer.audioInterruptMode=audio.InterruptMode.SHARE_MODE;
        avPlayer.play(); // 调用播放接口开始播放
        break;
      case 'playing': // play成功调用后触发该状态机上报
        console.info('AVPlayer state playing called.');
        break;
      case 'paused': // pause成功调用后触发该状态机上报
        console.info('AVPlayer state paused called.');
        avPlayer.play(); // 再次播放接口开始播放
        break;
      case 'completed': // 播放结束后触发该状态机上报
        console.info('AVPlayer state completed called.');
        avPlayer.stop(); //调用播放结束接口
        break;
      case 'stopped': // stop接口成功调用后触发该状态机上报
        console.info('AVPlayer state stopped called.');
        avPlayer.reset(); // 调用reset接口初始化avplayer状态
        break;
      case 'released':
        console.info('AVPlayer state released called.');
        break;
      default:
        console.info('AVPlayer state unknown called.');
        break;
    }
  })
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

步骤二:创建AVSession,使音频接入播控中心

AVSession在构造方法中支持不同的类型参数,由 AVSessionType 定义,不同的类型代表了不同场景的控制能力,对于播控中心来说,会展示不同的控制模版。

  • audio类型,播控中心的控制样式为:收藏,上一首,播放/暂停,下一首,循环模式。
  • video类型,播控中心的控制样式为:快退,上一首,播放/暂停,下一首,快进。
  • voice_call类型,通话类型。

创建AVSession

// 创建session
async  createSession() {
  let type: AVSessionManager.AVSessionType = 'audio';
  /*
   * context:应用上下文,提供获取应用程序环境信息的能力。
   * tag:会话的自定义名称。
   *type:会话类型。
   */
  let session = await AVSessionManager.createAVSession(context,'SESSION_NAME', type);
  // 设置必要的媒体信息
  let metadata: AVSessionManager.AVMetadata = {
    assetId: '0', // 由应用指定,用于标识应用媒体库里的媒体
    title: 'TITLE',
    mediaImage: 'IMAGE',
    artist: 'ARTIST',
  };
  session.setAVMetadata(metadata).then(() => {
    console.info(`SetAVMetadata successfully`);
  }).catch((err: BusinessError) => {
    console.error(`Failed to set AVMetadata. Code: ${err.code}, message: ${err.message}`);
  });
  //监听事件
  this.setListenerForMesFromController(session)
  // 激活接口要在元数据、控制命令注册完成之后再执行
  await session.activate();
  console.info(`session create done : sessionId : ${session.sessionId}`);
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

注:播控中心的显示必须要配上session.on控制命令的监听

async  setListenerForMesFromController(session: avSession.AVSession) {
  // 一般在监听器中会对播放器做相应逻辑处理
  // 不要忘记处理完后需要通过set接口同步播放相关信息,参考上面的用例
  session.on('play', () => {
    console.info(`on play , do play task`);
    // 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('play')取消监听
    // 处理完毕后,请使用SetAVPlayState上报播放状态
  });
  session.on('pause', () => {
    console.info(`on pause , do pause task`);
    // 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('pause')取消监听
    // 处理完毕后,请使用SetAVPlayState上报播放状态
  });
  session.on('stop', () => {
    console.info(`on stop , do stop task`);
    // 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('stop')取消监听
    // 处理完毕后,请使用SetAVPlayState上报播放状态
  });
  session.on('playNext', () => {
    console.info(`on playNext , do playNext task`);
    // 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('playNext')取消监听
    // 处理完毕后,请使用SetAVPlayState上报播放状态,使用SetAVMetadata上报媒体信息
  });
  session.on('playPrevious', () => {
    console.info(`on playPrevious , do playPrevious task`);
    // 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('playPrevious')取消监听
    // 处理完毕后,请使用SetAVPlayState上报播放状态,使用SetAVMetadata上报媒体信息
  });
  session.on('fastForward', () => {
    console.info(`on fastForward , do fastForward task`);
    // 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('fastForward')取消监听
    // 处理完毕后,请使用SetAVPlayState上报播放状态和播放position
  });
  session.on('rewind', () => {
    console.info(`on rewind , do rewind task`);
    // 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('rewind')取消监听
    // 处理完毕后,请使用SetAVPlayState上报播放状态和播放position
  });

session.on(‘seek’, (time) => { console.info(</span></span><span class="javascript"><span class="hljs-function"><span class="hljs-params"><span class="javascript">on seek , the seek time is ${time}</span></span></span></span><span class="hljs-function"><span class="hljs-params">); // 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off(‘seek’)取消监听 // 处理完毕后,请使用SetAVPlayState上报播放状态和播放position }); session.on(‘setSpeed’, (speed) => { console.info(</span></span><span class="javascript"><span class="hljs-function"><span class="hljs-params"><span class="javascript">on setSpeed , the speed is ${speed}</span></span></span></span><span class="hljs-function"><span class="hljs-params">); // do some tasks ··· }); session.on(‘setLoopMode’, (mode) => { console.info(</span></span><span class="javascript"><span class="hljs-function"><span class="hljs-params"><span class="javascript">on setLoopMode , the loop mode is ${mode}</span></span></span></span><span class="hljs-function"><span class="hljs-params">); // 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off(‘setLoopMode’)取消监听 // 应用自定下一个模式,处理完毕后,请使用SetAVPlayState上报切换后的LoopMode }); session.on(‘toggleFavorite’, (assetId) => { console.info(</span></span><span class="javascript"><span class="hljs-function"><span class="hljs-params"><span class="javascript">on toggleFavorite , the target asset Id is ${assetId}</span></span></span></span><span class="hljs-function"><span class="hljs-params">); // 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off(‘toggleFavorite’)取消监听 // 处理完毕后,请使用SetAVPlayState上报收藏结果isFavorite }); } <button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

步骤三:创建长时任务

在module.json5申请ohos.permission.KEEP_BACKGROUND_RUNNING权限:

"requestPermissions": [
  {
    "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
    "reason": "$string:app_name",
    "usedScene": {
      "abilities": [
        "FormAbility"
      ],
      "when":"always"
    }
  },
]
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

声明后台模式类型

在对应的UIAbility下配置backgroundModes

"backgroundModes": [
  // 长时任务类型的配置项
  "audioPlayback"
]
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

配置长时任务信息

let wantAgentInfo: wantAgent.WantAgentInfo = {
  // 点击通知后,将要执行的动作列表
  // 添加需要被拉起应用的bundleName和abilityName
  wants: [
    {
      bundleName: "com.example.avplayerdemo",
      abilityName: "com.example.avplayerdemo.EntryAbility"
    }
  ],
  // 指定点击通知栏消息后的动作是拉起ability
  actionType: wantAgent.OperationType.START_ABILITY,
  // 使用者自定义的一个私有值
  requestCode: 0,
  // 点击通知后,动作执行属性
  wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
};
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

wantAgentInfo配置信息链接:WantAgentInfo

申请长时任务

// 通过wantAgent模块下getWantAgent方法获取WantAgent对象
wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
  backgroundTaskManager.startBackgroundRunning(context,
    backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgentObj).then(() => {
    console.info(`Succeeded in operationing startBackgroundRunning.`);
  }).catch((err: BusinessError) => {
    console.error(`Failed to operation startBackgroundRunning. Code is ${err.code}, message is ${err.message}`);
  });
});
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

场景二:在播放音乐过程中,有其它音频流(如:导航音、电话)进入,进行相关处理

在多个音频流同时播放场景下,如果系统不加管控,会造成多个音频流混音播放,容易让用户感到嘈杂,造成不好的用户体验。为了解决这个问题,系统预设了音频打断(InterruptEvent)策略,对多音频播放的并发进行管控。为满足应用对多音频并发策略的不同需求,音频打断策略预设了两种焦点模式,针对同一应用创建的多个音频流,应用可通过设置焦点模式,选择由应用自主管控或由系统统一管控。

步骤一:完成上述场景一。

步骤二:设置焦点模式。

  • 共享焦点模式(SHARE_MODE):由同一应用创建的多个音频流,共享一个音频焦点。这些音频流之间的并发规则由应用自主决定,音频打断策略不会介入。当其他应用创建的音频流与该应用的音频流并发播放时,才会触发音频打断策略的管控。
  • 独立焦点模式(INDEPENDENT_MODE):应用创建的每一个音频流均会独立拥有一个音频焦点,当多个音频流并发播放时,会触发音频打断策略的管控。
avPlayer.audioInterruptMode=audio.InterruptMode.SHARE_MODE;
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

注:只允许在prepared/playing/paused/completed状态下设置。

步骤三:设置音频类型。

let audioRendererInfo: audio.AudioRendererInfo = {
  usage: audio.StreamUsage.STREAM_USAGE_NAVIGATION, // 音频流使用类型
  rendererFlags: 0 // 音频渲染器标志,0代表普通音频渲染器,1代表低时延音频渲染器。ArkTS接口暂不支持低时延音频渲染器。
}
avPlayer.audioRendererInfo=audioRendererInfo;
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

步骤四:创建监听音频焦点打断。

调用avPlayer的on('audioInterrupt')函数进行监听,当收到音频打断事件(InterruptEvent)时,应用需根据其内容,做出相应的调整。

avPlayer.on('audioInterrupt', async(interruptEvent: audio.InterruptEvent) => {
  // 先读取interruptEvent.forceType的类型,判断系统是否已强制执行相应操作
  // 再读取interruptEvent.hintType的类型,做出相应的处理
  if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_FORCE) {
    // 强制打断类型(INTERRUPT_FORCE):音频相关处理已由系统执行,应用需更新自身状态,做相应调整
    switch (interruptEvent.hintType) {
      case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
      // 此分支表示系统已将音频流暂停(临时失去焦点),为保持状态一致,应用需切换至音频暂停状态
      // 临时失去焦点:待其他音频流释放音频焦点后,本音频流会收到resume对应的音频打断事件,到时可自行继续播放
        isPlay = false; // 此句为简化处理,代表应用切换至音频暂停状态的若干操作
        break;
      case audio.InterruptHint.INTERRUPT_HINT_STOP:
      // 此分支表示系统已将音频流停止(永久失去焦点),为保持状态一致,应用需切换至音频暂停状态
      // 永久失去焦点:后续不会再收到任何音频打断事件,若想恢复播放,需要用户主动触发。
        isPlay = false; // 此句为简化处理,代表应用切换至音频暂停状态的若干操作
        break;
      case audio.InterruptHint.INTERRUPT_HINT_DUCK:
      // 此分支表示系统已将音频音量降低(默认降到正常音量的20%),为保持状态一致,应用需切换至降低音量播放状态
      // 若应用不接受降低音量播放,可在此处选择其他处理方式,如主动暂停等
        isDucked = true; // 此句为简化处理,代表应用切换至降低音量播放状态的若干操作
        break;
      case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
      // 此分支表示系统已将音频音量恢复正常,为保持状态一致,应用需切换至正常音量播放状态
        isDucked = false; // 此句为简化处理,代表应用切换至正常音量播放状态的若干操作
        break;
      default:
        break;
    }
  }
});
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

场景:

     

新播放的音频流

     

voip通话

voip消息

音乐

视频

游戏

听书/听新闻

导航

正在播放的音频流

voip通话

拒绝新的voip通话

降低voip消息音量

降低音乐音量

降低视频音量

降低游戏音量

降低听书/听新闻音量

降低导航音量

voip消息

停止正在播放的voip消息

停止正在播放的voip消息

停止voip消息

停止voip消息

同时播放

停止voip消息

降低导航音量

音乐

暂停音乐

音乐

暂停音乐

停止音乐

停止正在播放的音乐

停止音乐

同时播放

视频

暂停视频

视频

暂停视频

停止视频

停止视频

停止正在播放的视频

停止视频

游戏

暂停游戏

游戏

暂停游戏

同时播放

同时播放

暂停游戏

停止正在播放的游戏

听书/听新闻

暂停听书/听新闻

听书/听新闻

暂停听书/听新闻

停止听书/听新闻

停止听书/听新闻

停止听书/听新闻

停止听书/听新闻

导航

降低导航音量

导航

降低导航音量

降低导航音量

降低音乐音量

降低视频音量

降低游戏音量

</markdown>

关于HarmonyOS 鸿蒙Next基于AVPlayer音频后台播放的问题,您也可以访问:https://www.itying.com/category-93-b0.html 联系官网客服。

7 回复

HarmonyOS的开发者模式提供了很多实用的工具,方便我们进行调试和优化。

感谢,终于有思路了

找HarmonyOS工作还需要会Flutter的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:https://www.bilibili.com/video/BV1S4411E7LY/?p=17

感谢楼主分享

有要学HarmonyOS AI的同学吗,联系我:https://www.itying.com/goods-1206.html

感谢,马上要做该功能了
回到顶部