您的当前位置:首页正文

HarmonyOS 实战开发 —— 基于AVPlayer音频后台播放

2024-11-28 来源:个人技术集锦

?往期笔录记录?:

?
?
?
?
?
?
?
?


场景描述

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

申请长时任务

// 通过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;
    }
  }
});

场景:

显示全文