场景描述

音乐播放是媒体最重要的组成之一,以下是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;
}

注册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;
    }
  })
}

步骤二:创建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}`);
}

注:播控中心的显示必须要配上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(`on seek , the seek time is ${time}`);
    // 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('seek')取消监听
    // 处理完毕后,请使用SetAVPlayState上报播放状态和播放position
  });
  session.on('setSpeed', (speed) => {
    console.info(`on setSpeed , the speed is ${speed}`);
    // do some tasks ···
  });
  session.on('setLoopMode', (mode) => {
    console.info(`on setLoopMode , the loop mode is ${mode}`);
    // 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('setLoopMode')取消监听
    // 应用自定下一个模式,处理完毕后,请使用SetAVPlayState上报切换后的LoopMode
  });
  session.on('toggleFavorite', (assetId) => {
    console.info(`on toggleFavorite , the target asset Id is ${assetId}`);
    // 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('toggleFavorite')取消监听
    // 处理完毕后,请使用SetAVPlayState上报收藏结果isFavorite
  });
}

步骤三:创建长时任务

在module.json5申请ohos.permission.KEEP\_BACKGROUND\_RUNNING权限:

"requestPermissions": [
  {
    "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
    "reason": "$string:app_name",
    "usedScene": {
      "abilities": [
        "FormAbility"
      ],
      "when":"always"
    }
  },
]

声明后台模式类型

在对应的UIAbility下配置backgroundModes

"backgroundModes": [
  // 长时任务类型的配置项
  "audioPlayback"
]

配置长时任务信息

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]
};

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}`);
  });
});

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

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

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

步骤二:设置焦点模式。

  • 共享焦点模式(SHARE\_MODE):由同一应用创建的多个音频流,共享一个音频焦点。这些音频流之间的并发规则由应用自主决定,音频打断策略不会介入。当其他应用创建的音频流与该应用的音频流并发播放时,才会触发音频打断策略的管控。
  • 独立焦点模式(INDEPENDENT\_MODE):应用创建的每一个音频流均会独立拥有一个音频焦点,当多个音频流并发播放时,会触发音频打断策略的管控。
avPlayer.audioInterruptMode=audio.InterruptMode.SHARE_MODE;

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

步骤三:设置音频类型。

let audioRendererInfo: audio.AudioRendererInfo = {
  usage: audio.StreamUsage.STREAM_USAGE_NAVIGATION, // 音频流使用类型
  rendererFlags: 0 // 音频渲染器标志,0代表普通音频渲染器,1代表低时延音频渲染器。ArkTS接口暂不支持低时延音频渲染器。
}
avPlayer.audioRendererInfo=audioRendererInfo;

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

调用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;
    }
  }
});

场景:

      <p id="p16113103615519">新播放的音频流</p>
      <p id="p833117523013"><span>voip</span><span>通话</span></p> <p id="p113311052607"><span>voip</span><span>消息</span></p> <p id="p1733111521104"><span>音乐</span></p> <p id="p1533265215013"><span>视频</span></p> <p id="p933214521905"><span>游戏</span></p> <p id="p7639345215"><span>听书</span><span>/听新闻</span></p> <p id="p1163913415215"><span>导航</span></p>
<p id="p20247162961"></p> <p id="p660532666"></p> <p id="p4771031616"></p> <p id="p6479436620"></p> <p id="p8857412618"></p> <p id="p111161336105110">正在播放的音频流</p> <p id="p20114436195116">voip通话</p> <p id="p1711414364518">拒绝新的voip通话</p> <p id="p10844183416213"><span>降低</span><span>voip消息音量</span></p> <p id="p188454342216"><span>降低音乐音量</span></p> <p id="p9845834527"><span>降低视频音量</span></p> <p id="p18845113414212"><span>降低游戏音量</span></p> <p id="p188451234028"><span>降低听书</span><span>/听新闻音量</span></p> <p id="p984514341829"><span>降低导航音量</span></p>
<p id="p11141736165115">voip消息</p> <p id="p5114436145117">停止正在播放的voip消息</p> <p id="p10966144220216"><span>停止正在播放的</span><span>voip</span><span>消息</span></p> <p id="p17967154216213"><span>停止</span><span>voip消息</span></p> <p id="p29679421123"><span>停止</span><span>voip消息</span></p> <p id="p209674421211"><span>同时播放</span></p> <p id="p296714421325"><span>停止</span><span>voip消息</span></p> <p id="p1796718421216"><span>降低导航音量</span></p>
<p id="p15115153613514">音乐</p> <p id="p13115123605118">暂停音乐</p> <p id="p29676421324"><span>音乐</span></p> <p id="p49671642428"><span>暂停音乐</span></p> <p id="p1496719421723"><span>停止音乐</span></p> <p id="p39673428218"><span>停止正在播放的音乐</span></p> <p id="p0967144212215"><span>停止音乐</span></p> <p id="p189679423210"><span>同时播放</span></p>
<p id="p911511366510"><span>视频</span></p> <p id="p11115193695114">暂停视频</p> <p id="p1496710421625"><span>视频</span></p> <p id="p8967144216217"><span>暂停视频</span></p> <p id="p296713422215"><span>停止视频</span></p> <p id="p1496713421528"><span>停止视频</span></p> <p id="p129678421827"><span>停止正在播放的视频</span></p> <p id="p20968164216217"><span>停止视频</span></p>
<p id="p2115936185115">游戏</p> <p id="p1811513614512">暂停游戏</p> <p id="p69689421520"><span>游戏</span></p> <p id="p199689427217"><span>暂停游戏</span></p> <p id="p129681421322"><span>同时播放</span></p> <p id="p1596812426219"><span>同时播放</span></p> <p id="p13968942525"><span>暂停游戏</span></p> <p id="p14968742727"><span>停止正在播放的游戏</span></p>
<p id="p19115153617519">听书/听新闻</p> <p id="p11116113610510">暂停听书/听新闻</p> <p id="p596819429215"><span>听书</span><span>/听新闻</span></p> <p id="p296820425219"><span>暂停听书</span><span>/听新闻</span></p> <p id="p196814422023"><span>停止听书</span><span>/听新闻</span></p> <p id="p10968104212219"><span>停止听书</span><span>/听新闻</span></p> <p id="p796814215210"><span>停止听书</span><span>/听新闻</span></p> <p id="p1596812423217"><span>停止听书</span><span>/听新闻</span></p>
<p id="p61162367511">导航</p> <p id="p8116193615115">降低导航音量</p> <p id="p189692042726"><span>导航</span></p> <p id="p7969342325"><span>降低导航音量</span></p> <p id="p13969242620"><span>降低导航音量</span></p> <p id="p8969242625"><span>降低音乐音量</span></p> <p id="p59695420213"><span>降低视频音量</span></p> <p id="p49691642727"><span>降低游戏音量</span></p>

HarmonyOS码上奇行
12.1k 声望5.1k 粉丝

欢迎关注 HarmonyOS 开发者社区:[链接]