【HarmonyOS】使用AVPlayer播放音乐,导致系统其它应用音乐播放暂停 - 播放音频焦点管理

【HarmonyOS】使用AVPlayer播放音乐,导致系统其它应用音乐播放暂停 - 播放音频焦点管理

一、前言

在鸿蒙系统中,对于音乐播放分为几种场景。音乐,电影,音效,闹钟等。当使用AVPlayer播放音乐时,如果不处理播放焦点模式,默认会交给系统处理。系统处理多个音乐播放时,会按照触发顺序依次暂停当前,再继续下一个。

例如当华为音乐应用正在播放音乐,此时你的应用使用AVPlayer进行音乐播放,就会导致华为音乐播放暂停,开始播放你的音乐。如果你的是音乐应用,默认这样处理是OK的。但是如果你使用AVPlayer播放一个短时音乐或者音效。那这样处理就不好了。这个问题实际上是播放焦点管理,如果不管理就会造成冲突。

此时我们的预期可以是,播放完短时音乐或者音效后,继续播放华为音乐 。或者当我们播放短时音乐或者音效时,声音大。将华为音乐播放声音降低 。亦或者是,两个同时播放

不同的业务场景,需求不同。根据应用产品调性来决定播放处理模式。

二、如何解决播放焦点冲突?

这需要根据你的应用场景来决定。

1.SoundPool 音频池 【完整代码参见章节三】

当你的应用只是播放短时音乐或者音效,那可以不使用AVPlayer,使用SoundPool音频池来处理该场景。效果就是同时播放音乐。你的应用音效播放,并不会干扰其他应用音乐的播放。

只需要设置SoundPool的AudioRendererInfo,配置usage字段为STREAM_USAGE_UNKNOWN,

STREAM_USAGE_MUSIC,

STREAM_USAGE_MOVIE,

STREAM_USAGE_AUDIOBOOK时,为混音模式,不会打断其他音频播放。

dart 复制代码
    let audioRendererInfo: audio.AudioRendererInfo = {
      usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
      rendererFlags: 1
    }
   let soundPool: media.SoundPool = await media.createSoundPool(5, audioRendererInfo);

SoundPool的使用极其简单,只需要关心音频资源的获取,拿到SoundPool实例后,设置播放的参数,例如播放次数,音量,优先级等。将音频资源赋值后,就可以进行播放。

dart 复制代码
   public async PlaySoundPool() {
    // 开始播放,这边play也可带播放播放的参数PlayParameters,请在音频资源加载完毕,即收到loadComplete回调之后再执行play操作
     this.soundPool?.play(this.soundId, this.playParameters, (error, streamID: number) => {
      if (error) {
        console.info(this.TAG,`play sound Error: errCode is ${error.code}, errMessage is ${error.message}`)
      } else {
        this.streamId = streamID;
        console.info(this.TAG, 'play success streamID:' + streamID);
      }
    });
     // 设置声道播放音量
     await this.soundPool?.setVolume(this.streamId, 1, 1);
    // 设置循环播放次数
    await this.soundPool?.setLoop(this.streamId, 3); // 播放3次
    // 设置对应流的优先级
    await this.soundPool?.setPriority(this.streamId, 1);
  }

2.播放焦点管理 AudioSessionManager 【完整代码参见章节三】

当你的应用是播放音乐时,需要设置音频会话管理的模式,来设置兼容同时播放,还是暂停其他,优先播放当前。亦或者是播放自己的声音大,其他声音小。

如上图可见,AudioSessionManager的创建实际上相当于对播放AVplayer的初始化和播放控制做了一层包裹。包裹之后就可以针对播放进行播放音频焦点的管理。管理具体的播放模式,并发播放,优先播放,声音大播放等。

AudioSessionManager的使用也很简单,只需要拿到实例后,开启会话和关闭会话两个处理即可。开始会话时,需要配置音频焦点模式。

Strategy - concurrencyMode 共有四种模式:

默认模式(CONCURRENCY_DEFAULT ):即系统默认的音频焦点策略。

并发模式(CONCURRENCY_MIX_WITH_OTHERS ):和其它音频流并发。

降低音量模式(CONCURRENCY_DUCK_OTHERS ):和其他音频流并发,并且降低其他音频流的音量。

暂停模式(CONCURRENCY_PAUSE_OTHERS):暂停其他音频流,待释放焦点后通知其他音频流恢复。

dart 复制代码
  private mAudioSessionManager: audio.AudioSessionManager | null = null;
  private mStrategy: audio.AudioSessionStrategy | null = null;

   private initAudioSession(){
    let audioManager = audio.getAudioManager();
    this.mAudioSessionManager = audioManager.getSessionManager();
    this.mStrategy = {
      // 和其它音频流并发。
      concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_MIX_WITH_OTHERS
    };
    this.mAudioSessionManager.on('audioSessionDeactivated', (audioSessionDeactivatedEvent: audio.AudioSessionDeactivatedEvent) => {
      console.info(this.TAG,`reason of audioSessionDeactivated: ${audioSessionDeactivatedEvent.reason} `);
    });
  }

在播放音乐前,需要调用开始会话。才能生效音频管理的焦点模式:

如果不做其他处理,在音乐播放完后,一分钟后会话会自动关闭。

dart 复制代码
    // 设置并发播放音频
    await this.mAudioSessionManager?.activateAudioSession(this.mStrategy);
    let isActivated = this.mAudioSessionManager?.isAudioSessionActivated();
    console.log(this.TAG, "play isActivated: " + isActivated);

你也可以在音乐结束后,手动结束会话:

dart 复制代码
    await this.mAudioSessionManager?.deactivateAudioSession();

三、源码示例:

SoundPool 实现短时音乐或者音效播放

SoundPoolMgr.ets

dart 复制代码
import { audio } from '@kit.AudioKit';
import { media } from '@kit.MediaKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { resourceManager } from '@kit.LocalizationKit';

export class SoundPoolMgr {
  private TAG: string = 'SoundPoolMgr';

  // 单例对象
  private static mSoundPoolMgr: SoundPoolMgr | null = null;

  // 创建单例
  public static Ins(): SoundPoolMgr{
    if(!SoundPoolMgr.mSoundPoolMgr){
      SoundPoolMgr.mSoundPoolMgr = new SoundPoolMgr();
    }
    return SoundPoolMgr.mSoundPoolMgr;
  }

  private soundPool: media.SoundPool | null = null;
  private streamId: number = 0;
  private soundId: number = 0;
  private playParameters: media.PlayParameters | null = null;

  public async init(){
    // audioRenderInfo中的参数usage取值为STREAM_USAGE_UNKNOWN,STREAM_USAGE_MUSIC,STREAM_USAGE_MOVIE,
    // STREAM_USAGE_AUDIOBOOK时,SoundPool播放短音时为混音模式,不会打断其他音频播放。
    let audioRendererInfo: audio.AudioRendererInfo = {
      usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
      rendererFlags: 1
    }
    this.playParameters = {
      loop: 3, // 循环4次
      rate: audio.AudioRendererRate.RENDER_RATE_NORMAL, // 正常倍速
      leftVolume: 1, // range = 0.0-1.0
      rightVolume: 1, // range = 0.0-1.0
      priority: 0, // 最低优先级
    }

    //创建soundPool实例
    this.soundPool = await media.createSoundPool(5, audioRendererInfo);

    let uri: string = "";
    // // 加载音频资源
    // await fs.open('/test_01.mp3', fs.OpenMode.READ_ONLY).then((file: fs.File) => {
    //   console.info("file fd: " + file.fd);
    //   uri = 'fd://' + (file.fd).toString()
    // }); // '/test_01.mp3' 作为样例,使用时需要传入文件对应路径。
    // this.soundId = await this.soundPool.load(uri);
    try {
      let value: resourceManager.RawFileDescriptor = await getContext().resourceManager.getRawFd("test.mp3");
      let fd = value.fd;
      let offset = value.offset;
      let length = value.length;
      this.soundId = await this.soundPool.load(fd, offset, length);
    } catch (error) {
      let code = (error as BusinessError).code;
      let message = (error as BusinessError).message;
      console.error(this.TAG,`callback getRawFd failed, error code: ${code}, message: ${message}.`);
    }


    // 加载完成回调
    this.soundPool.on('loadComplete', (soundId_: number) => {
      console.info(this.TAG, 'loadComplete, soundId: ' + soundId_);
    })
    // 播放完成回调
    this.soundPool.on('playFinished', () => {
      console.info(this.TAG,"receive play finished message");
      // 可进行下次播放
    })
    //设置错误类型监听
    this.soundPool.on('error', (error: BusinessError) => {
      console.info(this.TAG, 'error happened,message is :' + error.message);
    })
  }

   public async PlaySoundPool() {
    // 开始播放,这边play也可带播放播放的参数PlayParameters,请在音频资源加载完毕,即收到loadComplete回调之后再执行play操作
     this.soundPool?.play(this.soundId, this.playParameters, (error, streamID: number) => {
      if (error) {
        console.info(this.TAG,`play sound Error: errCode is ${error.code}, errMessage is ${error.message}`)
      } else {
        this.streamId = streamID;
        console.info(this.TAG, 'play success streamID:' + streamID);
      }
    });
     // 设置声道播放音量
     await this.soundPool?.setVolume(this.streamId, 1, 1);
    // 设置循环播放次数
    await this.soundPool?.setLoop(this.streamId, 3); // 播放3次
    // 设置对应流的优先级
    await this.soundPool?.setPriority(this.streamId, 1);
    // 设置音量
    await this.soundPool?.setVolume(this.streamId, 0.5, 0.5);
  }

  public async release() {
    // 终止指定流的播放
    await this.soundPool?.stop(this.streamId);
    // 卸载音频资源
    await this.soundPool?.unload(this.streamId);
    //关闭监听
    this.soundPool?.off('loadComplete');
    this.soundPool?.off('playFinished');
    this.soundPool?.off('error');

    // 释放SoundPool
    await this.soundPool?.release();

  }

}

音效播放管理类 添加了AudioSessionManager 播放焦点管理逻辑

AudioMgr .ets

dart 复制代码
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { audio } from '@kit.AudioKit';

/**
 * 音效播放管理类
 */
export class AudioMgr {
  private TAG: string = 'AudioMgr';

  // 单例对象
  private static mAudioMgr: AudioMgr | null = null;

  // 播放器实例
  private mAVPlayer: media.AVPlayer | undefined = undefined;

  // 是否初始化
  private isInit: boolean = false;

  private mAudioSessionManager: audio.AudioSessionManager | null = null;
  private mStrategy: audio.AudioSessionStrategy | null = null;

  // 创建单例
  public static Ins(): AudioMgr{
    if(!AudioMgr.mAudioMgr){
      AudioMgr.mAudioMgr = new AudioMgr();
    }
    return AudioMgr.mAudioMgr;
  }

  /**
   * 初始化接口(可以提前初始化,也可以直接调用play接口,使用时初始化)
   */
  public async init() {
    console.log(this.TAG, "play init start");

    let audioManager = audio.getAudioManager();
    this.mAudioSessionManager = audioManager.getSessionManager();
    this.mStrategy = {
      // 和其它音频流并发。
      concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_MIX_WITH_OTHERS
    };
    this.mAudioSessionManager.on('audioSessionDeactivated', (audioSessionDeactivatedEvent: audio.AudioSessionDeactivatedEvent) => {
      console.info(this.TAG,`reason of audioSessionDeactivated: ${audioSessionDeactivatedEvent.reason} `);
    });

    // 创建avPlayer实例对象
    this.mAVPlayer = await media.createAVPlayer();
    // 创建状态机变化回调函数
    this.registerStateChange(this.mAVPlayer);
    // error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程
    this.registerErrorCall(this.mAVPlayer);
    // 获取raw音效资源
    let fileDescriptor = await getContext(this).resourceManager.getRawFd("test.mp3");
    this.mAVPlayer.fdSrc = {
      fd: fileDescriptor.fd,
      offset: fileDescriptor.offset,
      length: fileDescriptor.length
    };
    this.isInit = true;
    console.log(this.TAG, "play init end");
    return this.mAVPlayer;
  }

  /**
   * 注册异常回调
   * @param avPlayer
   */
  private registerErrorCall(avPlayer: media.AVPlayer){
    avPlayer.on('error', (err: BusinessError) => {
      console.log(this.TAG, " err:" + JSON.stringify(err));
      // 调用reset重置资源,触发idle状态
      avPlayer.reset();
    })
  }

  /**
   * 注册状态变化回调
   * @param avPlayer
   */
  private registerStateChange(avPlayer: media.AVPlayer){
    avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {

      switch (state) {
      // 成功调用reset接口后触发该状态机上报
        case 'idle':
          console.info(this.TAG, 'stateChange idle-release');
          avPlayer.release(); // 调用release接口销毁实例对象
          break;

      // avplayer 设置播放源后触发该状态上报
        case 'initialized':
          console.info(this.TAG, 'stateChange initialized-prepare');
          avPlayer.prepare();
          break;

      // prepare调用成功后上报该状态机
        case 'prepared':
          console.info(this.TAG, 'stateChange prepared-setVolume');
          avPlayer.setVolume(1); // The value ranges from 0.00 to 1.00.
          break;

      // play成功调用后触发该状态机上报
        case 'playing':
          console.info(this.TAG, 'stateChange playing');
          break;

      // pause成功调用后触发该状态机上报
        case 'paused':
          console.info(this.TAG, 'stateChange paused');
          break;

      // 播放结束后触发该状态机上报
        case 'completed':
          console.info(this.TAG, 'stateChange completed');
          break;

      // stop接口成功调用后触发该状态机上报
        case 'stopped':
          console.info(this.TAG, 'stateChange stopped');
          // avPlayer.reset(); // 调用reset接口初始化avplayer状态
          break;

        case 'released':
          console.info(this.TAG, 'stateChange released');
          break;

        default:
          console.info(this.TAG, 'stateChange default');
          break;
      }
    });
  }

  /**
   * 播放音效
   */
  public async play(){
    console.log(this.TAG, "play isInit " + this.isInit);
    // 设置并发播放音频
    await this.mAudioSessionManager?.activateAudioSession(this.mStrategy);
    let isActivated = this.mAudioSessionManager?.isAudioSessionActivated();
    console.log(this.TAG, "play isActivated: " + isActivated);
    
    
    if(this.isInit){
      await this.mAVPlayer?.play();
    }else{
      console.log(this.TAG, "play play-init start");
      this.mAVPlayer = await this.init();
      console.log(this.TAG, "play play start");
      await this.mAVPlayer?.play();
      console.log(this.TAG, "play play end");
    }
  }

  /**
   * 销毁音效管理工具
   */
  public async destroy(){
    console.log(this.TAG, "play destroy start");


    await this.mAVPlayer?.release();
    this.mAVPlayer = undefined;
    this.isInit = false;
    AudioMgr.mAudioMgr = null;
    console.log(this.TAG, "play destroy end");

    await this.mAudioSessionManager?.deactivateAudioSession();
  }

}

注意
当应用通过AudioSession使用上述各种模式时,系统将尽量满足其焦点策略,但在所有场景下可能无法保证完全满足。
如使用CONCURRENCY_PAUSE_OTHERS模式时,Movie流申请音频焦点,如果Music流正在播放,则Music流会被暂停。但是如果VoiceCommunication流正在播放,则VoiceCommunication流不会被暂停。

相关推荐
盐焗西兰花7 小时前
鸿蒙学习实战之路-Reader Kit修改翻页方式字体大小及行间距最佳实践
学习·华为·harmonyos
lbb 小魔仙11 小时前
【HarmonyOS实战】React Native 表单实战:在 OpenHarmony 上构建高性能表单
react native·华为·harmonyos
上海合宙LuatOS13 小时前
LuatOS核心库API——【audio 】
java·网络·单片机·嵌入式硬件·物联网·音视频·硬件工程
一只大侠的侠14 小时前
React Native开源鸿蒙跨平台训练营 Day16自定义 useForm 高性能验证
flutter·开源·harmonyos
早點睡39015 小时前
高级进阶 React Native 鸿蒙跨平台开发:@react-native-community-slider 滑块组件
react native·react.js·harmonyos
Android系统攻城狮15 小时前
Android16进阶之音频播放定位MediaPlayer.seekTo调用流程与实战(二百二十七)
音视频·mediaplayer·android16·音频进阶·音频性能实战
一只大侠的侠15 小时前
Flutter开源鸿蒙跨平台训练营 Day11从零开发商品详情页面
flutter·开源·harmonyos
一只大侠的侠15 小时前
React Native开源鸿蒙跨平台训练营 Day18自定义useForm表单管理实战实现
flutter·开源·harmonyos
一只大侠的侠15 小时前
React Native开源鸿蒙跨平台训练营 Day20自定义 useValidator 实现高性能表单验证
flutter·开源·harmonyos
晚霞的不甘16 小时前
Flutter for OpenHarmony 可视化教学:A* 寻路算法的交互式演示
人工智能·算法·flutter·架构·开源·音视频